From 7f02fb759a1b2f97cfb51552df4140387c0566db Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 10 Apr 2020 13:19:37 +0400 Subject: [PATCH] Assorted fixes --- .../Sources/ChatListController.swift | 8 +- submodules/ComposePollUI/BUCK | 1 + submodules/ComposePollUI/BUILD | 1 + .../Sources/CreatePollController.swift | 13 +- submodules/Display/Source/GridItemNode.swift | 3 + submodules/Display/Source/GridNode.swift | 2 + submodules/Display/Source/ListView.swift | 4 + .../Sources/ShareController.swift | 15 +- .../Sources/ShareControllerNode.swift | 40 +++- .../Sources/ShareInputFieldNode.swift | 16 ++ .../Sources/SharePeersContainerNode.swift | 11 +- submodules/ShimmerEffect/BUCK | 16 ++ submodules/ShimmerEffect/BUILD | 16 ++ .../ShimmerEffect/Sources/ShimmerEffect.swift | 200 ++++++++++++++++++ submodules/StickerPackPreviewUI/BUCK | 1 + submodules/StickerPackPreviewUI/BUILD | 1 + .../Sources/StickerPackPreviewGridItem.swift | 66 ++++-- .../Sources/StickerPackScreen.swift | 105 ++++++--- .../SyncCore/Sources/TelegramMediaPoll.swift | 30 ++- .../Sources/CachedStickerPack.swift | 6 +- .../PendingMessageUploadedContent.swift | 8 +- submodules/TelegramCore/Sources/Polls.swift | 9 +- .../Sources/TelegramMediaPoll.swift | 9 +- .../TelegramUI/Sources/ChatController.swift | 50 ++++- .../Sources/ChatControllerInteraction.swift | 4 +- .../ChatMessagePollBubbleContentNode.swift | 7 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 54 +++-- .../Sources/PeerInfo/PeerInfoData.swift | 5 +- .../Sources/PollBubbleTimerNode.swift | 3 +- submodules/TooltipUI/BUCK | 3 + submodules/TooltipUI/BUILD | 3 + .../TooltipUI/Sources/TooltipScreen.swift | 125 +++++++++-- 32 files changed, 688 insertions(+), 147 deletions(-) create mode 100644 submodules/ShimmerEffect/BUCK create mode 100644 submodules/ShimmerEffect/BUILD create mode 100644 submodules/ShimmerEffect/Sources/ShimmerEffect.swift diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 9c963b8f08..2f7a3e9983 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1198,16 +1198,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController, text = strongSelf.presentationData.strings.ChatList_TabIconFoldersTooltipEmptyFolders } - let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0), size: CGSize()) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 8.0), size: CGSize()) parentController.present(TooltipScreen(text: text, icon: .chatListPress, location: location, shouldDismissOnTouch: { point in guard let strongSelf = self, let parentController = strongSelf.parent as? TabBarController else { - return true + return .dismiss(consume: false) } if parentController.isPointInsideContentArea(point: point) { - return false + return .ignore } - return true + return .dismiss(consume: false) }), in: .current) } } diff --git a/submodules/ComposePollUI/BUCK b/submodules/ComposePollUI/BUCK index 3eec9a8776..7b414c169d 100644 --- a/submodules/ComposePollUI/BUCK +++ b/submodules/ComposePollUI/BUCK @@ -16,6 +16,7 @@ static_library( "//submodules/AccountContext:AccountContext", "//submodules/AlertUI:AlertUI", "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/TextFormat:TextFormat", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/ComposePollUI/BUILD b/submodules/ComposePollUI/BUILD index df9b19b552..7d37f6e963 100644 --- a/submodules/ComposePollUI/BUILD +++ b/submodules/ComposePollUI/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/AccountContext:AccountContext", "//submodules/AlertUI:AlertUI", "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/ComposePollUI/Sources/CreatePollController.swift b/submodules/ComposePollUI/Sources/CreatePollController.swift index 8777a747b9..56d2db3377 100644 --- a/submodules/ComposePollUI/Sources/CreatePollController.swift +++ b/submodules/ComposePollUI/Sources/CreatePollController.swift @@ -11,6 +11,7 @@ import PresentationDataUtils import AccountContext import AlertUI import PresentationDataUtils +import TextFormat private struct OrderedLinkedListItemOrderingId: RawRepresentable, Hashable { var rawValue: Int @@ -378,7 +379,7 @@ private enum CreatePollEntry: ItemListNodeEntry { case let .quizSolutionHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .quizSolutionText(placeholder, text): - return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 400, display: true), sectionId: self.section, style: .blocks, textUpdated: { text in + return ItemListMultilineInputItem(presentationData: presentationData, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 200, display: true), sectionId: self.section, style: .blocks, textUpdated: { text in arguments.updateSolutionText(text) }) case let .quizSolutionInfo(text): @@ -749,6 +750,9 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo if !hasSelectedOptions { enabled = false } + if state.solutionText.count > 200 { + enabled = false + } } if nonEmptyOptionCount < 2 { enabled = false @@ -774,11 +778,14 @@ public func createPollController(context: AccountContext, peer: Peer, isQuiz: Bo } else { publicity = .public } - var resolvedSolution: String? + var resolvedSolution: TelegramMediaPollResults.Solution? let kind: TelegramMediaPollKind if state.isQuiz { kind = .quiz - resolvedSolution = state.solutionText.isEmpty ? nil : state.solutionText + if !state.solutionText.isEmpty { + let entities = generateTextEntities(state.solutionText, enabledTypes: [.url]) + resolvedSolution = TelegramMediaPollResults.Solution(text: state.solutionText, entities: entities) + } } else { kind = .poll(multipleAnswers: state.isMultipleChoice) } diff --git a/submodules/Display/Source/GridItemNode.swift b/submodules/Display/Source/GridItemNode.swift index cfcf3d1384..e63784c359 100644 --- a/submodules/Display/Source/GridItemNode.swift +++ b/submodules/Display/Source/GridItemNode.swift @@ -18,4 +18,7 @@ open class GridItemNode: ASDisplayNode { open func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) { } + + open func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { + } } diff --git a/submodules/Display/Source/GridNode.swift b/submodules/Display/Source/GridNode.swift index ad983d263e..ee9ecfba01 100644 --- a/submodules/Display/Source/GridNode.swift +++ b/submodules/Display/Source/GridNode.swift @@ -902,6 +902,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { itemNode.frame = item.frame } itemNode.updateLayout(item: self.items[item.index], size: item.frame.size, isVisible: bounds.intersects(item.frame), synchronousLoads: synchronousLoads && itemInBounds) + itemNode.updateAbsoluteRect(item.frame, within: bounds.size) } else { let itemNode = self.items[item.index].node(layout: presentationLayoutTransition.layout.layout, synchronousLoad: synchronousLoads && itemInBounds) itemNode.frame = item.frame @@ -909,6 +910,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate { addedNodes = true itemNode.updateLayout(item: self.items[item.index], size: item.frame.size, isVisible: bounds.intersects(item.frame), synchronousLoads: synchronousLoads) self.setupNode?(itemNode) + itemNode.updateAbsoluteRect(item.frame, within: bounds.size) } } diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 221957c580..daa3c163d6 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -198,6 +198,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture } } + public final var didScrollWithOffset: ((CGFloat) -> Void)? + private var topItemOverscrollBackground: ListViewOverscrollBackgroundNode? private var bottomItemOverscrollBackground: ASDisplayNode? @@ -752,6 +754,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture //CATransaction.setDisableActions(true) let deltaY = scrollView.contentOffset.y - self.lastContentOffset.y + self.didScrollWithOffset?(deltaY) + self.generalAccumulatedDeltaY += deltaY if abs(self.generalAccumulatedDeltaY) > 14.0 { let direction: GeneralScrollDirection = self.generalAccumulatedDeltaY < 0 ? .up : .down diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index d2cf2dbc83..5c7ca39005 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -287,6 +287,7 @@ public final class ShareController: ViewController { private let externalShare: Bool private let immediateExternalShare: Bool private let subject: ShareControllerSubject + private let presetText: String? private let switchableAccounts: [AccountWithInfo] private let immediatePeerId: PeerId? @@ -299,15 +300,16 @@ public final class ShareController: ViewController { public var dismissed: ((Bool) -> Void)? - public convenience init(context: AccountContext, subject: ShareControllerSubject, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) { - self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, preferredAction: preferredAction, showInChat: showInChat, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId) + public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) { + self.init(sharedContext: context.sharedContext, currentContext: context, subject: subject, presetText: presetText, preferredAction: preferredAction, showInChat: showInChat, externalShare: externalShare, immediateExternalShare: immediateExternalShare, switchableAccounts: switchableAccounts, immediatePeerId: immediatePeerId) } - public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) { + public init(sharedContext: SharedAccountContext, currentContext: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil) { self.sharedContext = sharedContext self.currentContext = currentContext self.currentAccount = currentContext.account self.subject = subject + self.presetText = presetText self.externalShare = externalShare self.immediateExternalShare = immediateExternalShare self.switchableAccounts = switchableAccounts @@ -428,7 +430,7 @@ public final class ShareController: ViewController { } override public func loadDisplayNode() { - self.displayNode = ShareControllerNode(sharedContext: self.sharedContext, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in + self.displayNode = ShareControllerNode(sharedContext: self.sharedContext, presetText: self.presetText, defaultAction: self.defaultAction, requestLayout: { [weak self] transition in self?.requestLayout(transition: transition) }, presentError: { [weak self] title, text in guard let strongSelf = self else { @@ -457,9 +459,10 @@ public final class ShareController: ViewController { for peerId in peerIds { var messages: [EnqueueMessage] = [] if !text.isEmpty { - messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + messages.append(.message(text: url + "\n\n" + text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) + } else { + messages.append(.message(text: url, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) } - messages.append(.message(text: url, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)) shareSignals.append(enqueueMessages(account: strongSelf.currentAccount, peerId: peerId, messages: messages)) } case let .text(string): diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 2cbfe1bbe1..8d17ecde81 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -76,7 +76,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private var hapticFeedback: HapticFeedback? - init(sharedContext: SharedAccountContext, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?) { + private let presetText: String? + + init(sharedContext: SharedAccountContext, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?) { self.sharedContext = sharedContext self.presentationData = sharedContext.currentPresentationData.with { $0 } self.externalShare = externalShare @@ -84,6 +86,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.immediatePeerId = immediatePeerId self.presentError = presentError + self.presetText = presetText + self.defaultAction = defaultAction self.requestLayout = requestLayout @@ -139,6 +143,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.actionButtonNode.setBackgroundImage(highlightedHalfRoundedBackground, for: .highlighted) self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: self.presentationData.theme), placeholder: self.presentationData.strings.ShareMenu_Comment) + self.inputFieldNode.text = presetText ?? "" + self.inputFieldNode.preselectText() self.inputFieldNode.alpha = 0.0 self.actionSeparatorNode = ASDisplayNode() @@ -176,7 +182,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate strongSelf.peersContentNode?.updateFoundPeers() } - strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty, inputField: true, actions: strongSelf.defaultAction == nil) + strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty && strongSelf.presetText == nil, inputField: true, actions: strongSelf.defaultAction == nil) strongSelf.updateButton() @@ -228,6 +234,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } self.updateButton() + + if self.presetText != nil { + self.setActionNodesHidden(false, inputField: true, actions: true, animated: false) + } } deinit { @@ -279,13 +289,15 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.actionButtonNode.badgeTextColor = presentationData.theme.actionSheet.opaqueItemBackgroundColor } - func setActionNodesHidden(_ hidden: Bool, inputField: Bool = false, actions: Bool = false) { + func setActionNodesHidden(_ hidden: Bool, inputField: Bool = false, actions: Bool = false, animated: Bool = true) { func updateActionNodesAlpha(_ nodes: [ASDisplayNode], alpha: CGFloat) { for node in nodes { if !node.alpha.isEqual(to: alpha) { let previousAlpha = node.alpha node.alpha = alpha - node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: alpha.isZero ? 0.18 : 0.32) + if animated { + node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: alpha.isZero ? 0.18 : 0.32) + } if let inputNode = node as? ShareInputFieldNode, alpha.isZero { inputNode.deactivateInput() @@ -362,7 +374,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate if contentNode is ShareSearchContainerNode { self.setActionNodesHidden(true, inputField: true, actions: true) } else if !(contentNode is ShareLoadingContainerNode) { - self.setActionNodesHidden(false, inputField: !self.controllerInteraction!.selectedPeers.isEmpty, actions: true) + self.setActionNodesHidden(false, inputField: !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil, actions: true) } } else { if let contentNode = self.contentNode { @@ -409,13 +421,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate var bottomGridInset: CGFloat = 0 var actionButtonHeight: CGFloat = 0 - if self.defaultAction != nil || !self.controllerInteraction!.selectedPeers.isEmpty { + if self.defaultAction != nil || !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil { actionButtonHeight = buttonHeight bottomGridInset += actionButtonHeight } let inputHeight = self.inputFieldNode.updateLayout(width: contentContainerFrame.size.width, transition: transition) - if !self.controllerInteraction!.selectedPeers.isEmpty { + if !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil { bottomGridInset += inputHeight } @@ -503,7 +515,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } @objc func actionButtonPressed() { - if self.controllerInteraction!.selectedPeers.isEmpty { + if self.controllerInteraction!.selectedPeers.isEmpty && self.presetText == nil { if let defaultAction = self.defaultAction { defaultAction.action() } @@ -658,7 +670,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate let animated = self.peersContentNode == nil let peersContentNode = SharePeersContainerNode(sharedContext: self.sharedContext, context: context, switchableAccounts: switchableAccounts, theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peers: peers, accountPeer: accountPeer, controllerInteraction: self.controllerInteraction!, externalShare: self.externalShare, switchToAnotherAccount: { [weak self] in self?.switchToAnotherAccount?() - }) + }, extendedInitialReveal: self.presetText != nil) self.peersContentNode = peersContentNode peersContentNode.openSearch = { [weak self] in let _ = (recentlySearchedPeers(postbox: context.account.postbox) @@ -783,13 +795,21 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private func updateButton() { if self.controllerInteraction!.selectedPeers.isEmpty { - if let defaultAction = self.defaultAction { + if self.presetText != nil { + self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .normal) + self.actionButtonNode.isEnabled = false + self.actionButtonNode.badge = nil + } else if let defaultAction = self.defaultAction { self.actionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) + self.actionButtonNode.isEnabled = false self.actionButtonNode.badge = nil } else { self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.disabledActionTextColor, for: .normal) + self.actionButtonNode.isEnabled = false + self.actionButtonNode.badge = nil } } else { + self.actionButtonNode.isEnabled = true self.actionButtonNode.setTitle(self.presentationData.strings.ShareMenu_Send, with: Font.medium(20.0), with: self.presentationData.theme.actionSheet.standardActionTextColor, for: .normal) self.actionButtonNode.badge = "\(self.controllerInteraction!.selectedPeers.count)" } diff --git a/submodules/ShareController/Sources/ShareInputFieldNode.swift b/submodules/ShareController/Sources/ShareInputFieldNode.swift index 5a89d225c8..43ddd7f6c8 100644 --- a/submodules/ShareController/Sources/ShareInputFieldNode.swift +++ b/submodules/ShareController/Sources/ShareInputFieldNode.swift @@ -68,6 +68,8 @@ final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { private let inputInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 10.0, right: 16.0) private let accessoryButtonsWidth: CGFloat = 10.0 + private var selectTextOnce: Bool = false + var text: String { get { return self.textInputNode.attributedText?.string ?? "" @@ -124,9 +126,15 @@ final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { self.addSubnode(self.placeholderNode) self.addSubnode(self.clearButton) + self.textInputNode.textView.showsVerticalScrollIndicator = false + self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) } + func preselectText() { + self.selectTextOnce = true + } + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { let backgroundInsets = self.backgroundInsets let inputInsets = self.inputInsets @@ -165,6 +173,13 @@ final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { self.placeholderNode.isHidden = true self.clearButton.isHidden = false + + if self.selectTextOnce { + self.selectTextOnce = false + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: { + self.textInputNode.selectedRange = NSRange(self.text.startIndex ..< self.text.endIndex, in: self.text) + }) + } } func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { @@ -196,5 +211,6 @@ final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { @objc func clearPressed() { self.textInputNode.attributedText = nil self.deactivateInput() + self.updateHeight?() } } diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 1d587de8c2..2ee580c627 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -80,6 +80,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { private let nameDisplayOrder: PresentationPersonNameOrder private let controllerInteraction: ShareControllerInteraction private let switchToAnotherAccount: () -> Void + private let extendedInitialReveal: Bool let accountPeer: Peer private let foundPeers = Promise<[RenderedPeer]>([]) @@ -107,7 +108,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { let peersValue = Promise<[(RenderedPeer, PeerPresence?)]>() - init(sharedContext: SharedAccountContext, context: AccountContext, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void) { + init(sharedContext: SharedAccountContext, context: AccountContext, switchableAccounts: [AccountWithInfo], theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, peers: [(RenderedPeer, PeerPresence?)], accountPeer: Peer, controllerInteraction: ShareControllerInteraction, externalShare: Bool, switchToAnotherAccount: @escaping () -> Void, extendedInitialReveal: Bool) { self.sharedContext = sharedContext self.context = context self.theme = theme @@ -116,6 +117,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.controllerInteraction = controllerInteraction self.accountPeer = accountPeer self.switchToAnotherAccount = switchToAnotherAccount + self.extendedInitialReveal = extendedInitialReveal self.peersValue.set(.single(peers)) @@ -261,7 +263,12 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { var rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0) rowCount = max(rowCount, 4) - let minimallyRevealedRowCount: CGFloat = 3.7 + let minimallyRevealedRowCount: CGFloat + if self.extendedInitialReveal { + minimallyRevealedRowCount = 4.6 + } else { + minimallyRevealedRowCount = 3.7 + } let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0) diff --git a/submodules/ShimmerEffect/BUCK b/submodules/ShimmerEffect/BUCK new file mode 100644 index 0000000000..e30e043eab --- /dev/null +++ b/submodules/ShimmerEffect/BUCK @@ -0,0 +1,16 @@ +load("//Config:buck_rule_macros.bzl", "static_library") + +static_library( + name = "ShimmerEffect", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + ], + frameworks = [ + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], +) diff --git a/submodules/ShimmerEffect/BUILD b/submodules/ShimmerEffect/BUILD new file mode 100644 index 0000000000..5ac01ccfdd --- /dev/null +++ b/submodules/ShimmerEffect/BUILD @@ -0,0 +1,16 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ShimmerEffect", + module_name = "ShimmerEffect", + srcs = glob([ + "Sources/**/*.swift", + ]), + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift new file mode 100644 index 0000000000..656d822a65 --- /dev/null +++ b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift @@ -0,0 +1,200 @@ +import UIKit +import AsyncDisplayKit +import Display + +private final class ShimmerEffectForegroundNode: ASDisplayNode { + private var currentBackgroundColor: UIColor? + private var currentForegroundColor: UIColor? + private let imageNodeContainer: ASDisplayNode + private let imageNode: ASImageNode + + private var absoluteLocation: (CGRect, CGSize)? + private var isCurrentlyInHierarchy = false + private var shouldBeAnimating = false + + override init() { + self.imageNodeContainer = ASDisplayNode() + self.imageNodeContainer.isLayerBacked = true + + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + self.imageNode.contentMode = .scaleToFill + + super.init() + + self.isLayerBacked = true + self.clipsToBounds = true + + self.imageNodeContainer.addSubnode(self.imageNode) + self.addSubnode(self.imageNodeContainer) + } + + override func didEnterHierarchy() { + super.didEnterHierarchy() + + self.isCurrentlyInHierarchy = true + self.updateAnimation() + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.isCurrentlyInHierarchy = false + self.updateAnimation() + } + + func update(backgroundColor: UIColor, foregroundColor: UIColor) { + if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { + return + } + self.currentBackgroundColor = backgroundColor + self.currentForegroundColor = foregroundColor + + self.imageNode.image = generateImage(CGSize(width: 4.0, height: 320.0), opaque: true, scale: 1.0, rotatedContext: { size, context in + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor + let peakColor = foregroundColor.cgColor + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [transparentColor, peakColor, transparentColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { + return + } + let sizeUpdated = self.absoluteLocation?.1 != containerSize + let frameUpdated = self.absoluteLocation?.0 != rect + self.absoluteLocation = (rect, containerSize) + + if sizeUpdated { + if self.shouldBeAnimating { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + self.addImageAnimation() + } else { + self.updateAnimation() + } + } + + if frameUpdated { + self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) + } + } + + private func updateAnimation() { + let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil + if shouldBeAnimating != self.shouldBeAnimating { + self.shouldBeAnimating = shouldBeAnimating + if shouldBeAnimating { + self.addImageAnimation() + } else { + self.imageNode.layer.removeAnimation(forKey: "shimmer") + } + } + } + + private func addImageAnimation() { + guard let containerSize = self.absoluteLocation?.1 else { + return + } + let gradientHeight: CGFloat = 250.0 + self.imageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -gradientHeight), size: CGSize(width: containerSize.width, height: gradientHeight)) + let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.height + gradientHeight) as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.imageNode.layer.add(animation, forKey: "shimmer") + } +} + +public final class ShimmerEffectNode: ASDisplayNode { + public enum Shape: Equatable { + case circle(CGRect) + case roundedRectLine(startPoint: CGPoint, width: CGFloat, diameter: CGFloat) + case roundedRect(rect: CGRect, cornerRadius: CGFloat) + } + + private let backgroundNode: ASDisplayNode + private let effectNode: ShimmerEffectForegroundNode + private let foregroundNode: ASImageNode + + private var currentShapes: [Shape] = [] + private var currentBackgroundColor: UIColor? + private var currentForegroundColor: UIColor? + private var currentShimmeringColor: UIColor? + private var currentSize = CGSize() + + override public init() { + self.backgroundNode = ASDisplayNode() + + self.effectNode = ShimmerEffectForegroundNode() + + self.foregroundNode = ASImageNode() + self.foregroundNode.displaysAsynchronously = false + self.foregroundNode.displayWithoutProcessing = true + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.effectNode) + self.addSubnode(self.foregroundNode) + } + + public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.effectNode.updateAbsoluteRect(rect, within: containerSize) + } + + public func update(backgroundColor: UIColor, foregroundColor: UIColor, shimmeringColor: UIColor, shapes: [Shape], size: CGSize) { + if self.currentShapes == shapes, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), self.currentSize == size { + return + } + + self.currentBackgroundColor = backgroundColor + self.currentForegroundColor = foregroundColor + self.currentShimmeringColor = shimmeringColor + self.currentShapes = shapes + self.currentSize = size + + self.backgroundNode.backgroundColor = foregroundColor + + self.effectNode.update(backgroundColor: foregroundColor, foregroundColor: shimmeringColor) + + self.foregroundNode.image = generateImage(size, rotatedContext: { size, context in + context.setFillColor(backgroundColor.cgColor) + context.setBlendMode(.copy) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(UIColor.clear.cgColor) + for shape in shapes { + switch shape { + case let .circle(frame): + context.fillEllipse(in: frame) + case let .roundedRectLine(startPoint, width, diameter): + context.fillEllipse(in: CGRect(origin: startPoint, size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: startPoint.x + width - diameter, y: startPoint.y), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: startPoint.x + diameter / 2.0, y: startPoint.y), size: CGSize(width: width - diameter, height: diameter))) + case let .roundedRect(rect, radius): + let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: radius, height: radius)) + UIGraphicsPushContext(context) + path.fill() + UIGraphicsPopContext() + } + } + }) + + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + self.foregroundNode.frame = CGRect(origin: CGPoint(), size: size) + self.effectNode.frame = CGRect(origin: CGPoint(), size: size) + } +} diff --git a/submodules/StickerPackPreviewUI/BUCK b/submodules/StickerPackPreviewUI/BUCK index 4b0f121d0a..b24e205b94 100644 --- a/submodules/StickerPackPreviewUI/BUCK +++ b/submodules/StickerPackPreviewUI/BUCK @@ -25,6 +25,7 @@ static_library( "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/ArchivedStickerPacksNotice:ArchivedStickerPacksNotice", + "//submodules/ShimmerEffect:ShimmerEffect", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/StickerPackPreviewUI/BUILD b/submodules/StickerPackPreviewUI/BUILD index 0a13100ff6..71da5f8bf8 100644 --- a/submodules/StickerPackPreviewUI/BUILD +++ b/submodules/StickerPackPreviewUI/BUILD @@ -26,6 +26,7 @@ swift_library( "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/ArchivedStickerPacksNotice:ArchivedStickerPacksNotice", + "//submodules/ShimmerEffect:ShimmerEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index c7f4db7fbc..d8c5b05855 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -11,6 +11,7 @@ import AccountContext import AnimatedStickerNode import TelegramAnimatedStickerNode import TelegramPresentationData +import ShimmerEffect final class StickerPackPreviewInteraction { var previewedItem: StickerPreviewPeekItem? @@ -60,7 +61,9 @@ final class StickerPackPreviewGridItemNode: GridItemNode { private var isEmpty: Bool? private let imageNode: TransformImageNode private var animationNode: AnimatedStickerNode? - private let placeholderNode: ASDisplayNode + private var placeholderNode: ShimmerEffectNode? + + private var theme: PresentationTheme? override var isVisibleInGrid: Bool { didSet { @@ -83,23 +86,22 @@ final class StickerPackPreviewGridItemNode: GridItemNode { override init() { self.imageNode = TransformImageNode() self.imageNode.isLayerBacked = !smartInvertColorsEnabled() - self.placeholderNode = ASDisplayNode() + self.placeholderNode = ShimmerEffectNode() super.init() self.addSubnode(self.imageNode) - self.addSubnode(self.placeholderNode) + if let placeholderNode = self.placeholderNode { + self.addSubnode(placeholderNode) + } var firstTime = true self.imageNode.imageUpdated = { [weak self] image in guard let strongSelf = self else { return } - if image != nil, !strongSelf.placeholderNode.alpha.isZero { - strongSelf.placeholderNode.alpha = 0.0 - if !firstTime { - strongSelf.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) - } + if image != nil { + strongSelf.removePlaceholder(animated: !firstTime) } firstTime = false } @@ -109,6 +111,20 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.stickerFetchedDisposable.dispose() } + private func removePlaceholder(animated: Bool) { + if let placeholderNode = self.placeholderNode { + self.placeholderNode = nil + if !animated { + placeholderNode.removeFromSupernode() + } else { + placeholderNode.alpha = 0.0 + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in + placeholderNode?.removeFromSupernode() + }) + } + } + } + override func didLoad() { super.didLoad() @@ -117,8 +133,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { func setup(account: Account, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isEmpty: Bool) { self.interaction = interaction - - self.placeholderNode.backgroundColor = theme.list.mediaPlaceholderColor + self.theme = theme if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem || self.isEmpty != isEmpty { if let stickerItem = stickerItem { @@ -133,7 +148,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.addSubnode(animationNode) animationNode.started = { [weak self] in self?.imageNode.isHidden = true - self?.placeholderNode.alpha = 0.0 + self?.removePlaceholder(animated: false) } } let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) @@ -151,13 +166,15 @@ final class StickerPackPreviewGridItemNode: GridItemNode { } } } else { - if isEmpty { - if !self.placeholderNode.alpha.isZero { - self.placeholderNode.alpha = 0.0 - self.placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + if let placeholderNode = self.placeholderNode { + if isEmpty { + if !placeholderNode.alpha.isZero { + placeholderNode.alpha = 0.0 + placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } else { + placeholderNode.alpha = 1.0 } - } else { - self.placeholderNode.alpha = 1.0 } } self.currentState = (account, stickerItem) @@ -176,7 +193,14 @@ final class StickerPackPreviewGridItemNode: GridItemNode { let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0) let boundingSize = CGSize(width: boundsSide, height: boundsSide) - self.placeholderNode.frame = CGRect(origin: CGPoint(x: floor((bounds.width - boundingSize.width) / 2.0), y: floor((bounds.height - boundingSize.height) / 2.0)), size: boundingSize) + if let placeholderNode = self.placeholderNode { + let placeholderFrame = CGRect(origin: CGPoint(x: floor((bounds.width - boundingSize.width) / 2.0), y: floor((bounds.height - boundingSize.height) / 2.0)), size: boundingSize) + placeholderNode.frame = bounds + + if let theme = self.theme { + placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.roundedRect(rect: placeholderFrame, cornerRadius: 10.0)], size: bounds.size) + } + } if let (_, item) = self.currentState { if let item = item, let dimensions = item.file.dimensions?.cgSize { @@ -191,6 +215,12 @@ final class StickerPackPreviewGridItemNode: GridItemNode { } } + override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { + if let placeholderNode = self.placeholderNode { + placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize) + } + } + func transitionNode() -> ASDisplayNode? { return self } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 60938c8594..e5d85dca1a 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -10,6 +10,7 @@ import AccountContext import TelegramPresentationData import TelegramUIPreferences import MergeLists +import ShimmerEffect private struct StickerPackPreviewGridEntry: Comparable, Identifiable { let index: Int @@ -51,6 +52,7 @@ private enum StickerPackAction { private enum StickerPackNextAction { case navigatedNext case dismiss + case ignored } private final class StickerPackContainer: ASDisplayNode { @@ -68,7 +70,7 @@ private final class StickerPackContainer: ASDisplayNode { private let actionAreaSeparatorNode: ASDisplayNode private let buttonNode: HighlightableButtonNode private let titleNode: ImmediateTextNode - private let titlePlaceholderNode: ASDisplayNode + private var titlePlaceholderNode: ShimmerEffectNode? private let titleContainer: ASDisplayNode private let titleSeparatorNode: ASDisplayNode @@ -80,6 +82,7 @@ private final class StickerPackContainer: ASDisplayNode { private var itemsDisposable: Disposable? private(set) var currentStickerPack: (StickerPackCollectionInfo, [ItemCollectionItem], Bool)? + private var didReceiveStickerPackResult = false private let isReadyValue = Promise() private var didSetReady = false @@ -121,16 +124,13 @@ private final class StickerPackContainer: ASDisplayNode { self.actionAreaBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor self.actionAreaSeparatorNode = ASDisplayNode() - self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor + self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.buttonNode = HighlightableButtonNode() self.titleNode = ImmediateTextNode() - self.titlePlaceholderNode = ASDisplayNode() - self.titlePlaceholderNode.alpha = 0.0 - self.titlePlaceholderNode.backgroundColor = presentationData.theme.list.mediaPlaceholderColor self.titleContainer = ASDisplayNode() self.titleSeparatorNode = ASDisplayNode() - self.titleSeparatorNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemSeparatorColor + self.titleSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor self.interaction = StickerPackPreviewInteraction(playAnimatedStickers: true) @@ -143,7 +143,6 @@ private final class StickerPackContainer: ASDisplayNode { self.addSubnode(self.buttonNode) self.titleContainer.addSubnode(self.titleNode) - self.titleContainer.addSubnode(self.titlePlaceholderNode) self.addSubnode(self.titleContainer) self.addSubnode(self.titleSeparatorNode) @@ -354,13 +353,15 @@ private final class StickerPackContainer: ASDisplayNode { switch strongSelf.decideNextAction(strongSelf, installed ? .remove : .add) { case .dismiss: strongSelf.requestDismiss() - case .navigatedNext: + case .navigatedNext, .ignored: strongSelf.updateStickerPackContents(.result(info: info, items: items, installed: !installed)) } }) } private func updateStickerPackContents(_ contents: LoadedStickerPack) { + self.didReceiveStickerPackResult = true + var entries: [StickerPackPreviewGridEntry] = [] var updateLayout = false @@ -393,7 +394,11 @@ private final class StickerPackContainer: ASDisplayNode { self.nextStableId += 1 entries.append(StickerPackPreviewGridEntry(index: entries.count, stableId: resolvedStableId, stickerItem: nil, isEmpty: false)) } - self.titlePlaceholderNode.alpha = 1.0 + if self.titlePlaceholderNode == nil { + let titlePlaceholderNode = ShimmerEffectNode() + self.titlePlaceholderNode = titlePlaceholderNode + self.titleContainer.addSubnode(titlePlaceholderNode) + } case .none: entries = [] self.buttonNode.setTitle(self.presentationData.strings.Common_Close.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal) @@ -404,10 +409,9 @@ private final class StickerPackContainer: ASDisplayNode { self.nextStableId += 1 entries.append(StickerPackPreviewGridEntry(index: entries.count, stableId: resolvedStableId, stickerItem: nil, isEmpty: true)) } - if !self.titlePlaceholderNode.alpha.isZero { - self.titlePlaceholderNode.alpha = 0.0 - self.titlePlaceholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) - self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + if let titlePlaceholderNode = self.titlePlaceholderNode { + self.titlePlaceholderNode = nil + titlePlaceholderNode.removeFromSupernode() } case let .result(info, items, installed): if !items.isEmpty && self.currentStickerPack == nil { @@ -441,15 +445,12 @@ private final class StickerPackContainer: ASDisplayNode { context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) })?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25) self.buttonNode.setBackgroundImage(roundedAccentBackground, for: []) - if self.titleNode.attributedText == nil { - self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - } } if self.titleNode.attributedText == nil { - if !self.titlePlaceholderNode.alpha.isZero { - self.titlePlaceholderNode.alpha = 0.0 - self.titlePlaceholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + if let titlePlaceholderNode = self.titlePlaceholderNode { + self.titlePlaceholderNode = nil + titlePlaceholderNode.removeFromSupernode() } } @@ -480,6 +481,15 @@ private final class StickerPackContainer: ASDisplayNode { let previousEntries = self.currentEntries self.currentEntries = entries + if let titlePlaceholderNode = self.titlePlaceholderNode { + let fakeTitleSize = CGSize(width: 160.0, height: 22.0) + let titlePlaceholderFrame = CGRect(origin: CGPoint(x: floor((-fakeTitleSize.width) / 2.0), y: floor((-fakeTitleSize.height) / 2.0)), size: fakeTitleSize) + titlePlaceholderNode.frame = titlePlaceholderFrame + let theme = self.presentationData.theme + titlePlaceholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.roundedRect(rect: CGRect(origin: CGPoint(), size: titlePlaceholderFrame.size), cornerRadius: 4.0)], size: titlePlaceholderFrame.size) + updateLayout = true + } + if updateLayout, let (layout, _, _, _) = self.validLayout { let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 12.0 * 2.0, height: .greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: floor((-titleSize.width) / 2.0), y: floor((-titleSize.height) / 2.0)), size: titleSize) @@ -487,10 +497,6 @@ private final class StickerPackContainer: ASDisplayNode { self.updateLayout(layout: layout, transition: .immediate) } - let fakeTitleSize = CGSize(width: 160.0, height: 22.0) - self.titlePlaceholderNode.frame = CGRect(origin: CGPoint(x: floor((-fakeTitleSize.width) / 2.0), y: floor((-fakeTitleSize.height) / 2.0)), size: fakeTitleSize) - - let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme, scrollToItem: scrollToItem) self.enqueueTransaction(transaction) } @@ -571,12 +577,18 @@ private final class StickerPackContainer: ASDisplayNode { guard let strongSelf = self else { return } - if !strongSelf.didSetReady { - strongSelf.didSetReady = true - strongSelf.isReadyValue.set(.single(true)) + if strongSelf.didReceiveStickerPackResult { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf.isReadyValue.set(.single(true)) + } } }) + if let titlePlaceholderNode = self.titlePlaceholderNode { + titlePlaceholderNode.updateAbsoluteRect(titlePlaceholderNode.frame.offsetBy(dx: self.titleContainer.frame.minX, dy: self.titleContainer.frame.minY - gridInsets.top - gridFrame.minY), within: gridFrame.size) + } + if firstTime { while !self.enqueuedTransactions.isEmpty { self.dequeueTransaction() @@ -596,6 +608,7 @@ private final class StickerPackContainer: ASDisplayNode { let expandHeight: CGFloat = 100.0 let expandProgress = max(0.0, min(1.0, offsetFromInitialPosition / expandHeight)) let expandScrollProgress = 1.0 - max(0.0, min(1.0, presentationLayout.contentOffset.y / (-gridInsets.top))) + let modalProgress = max(0.0, min(1.0, expandScrollProgress)) let expandProgressTransition = transition var expandUpdated = false @@ -610,6 +623,11 @@ private final class StickerPackContainer: ASDisplayNode { expandUpdated = true } + if abs(self.modalProgress - modalProgress) > CGFloat.ulpOfOne { + self.modalProgress = modalProgress + expandUpdated = true + } + if expandUpdated { self.expandProgressUpdated(self, expandProgressTransition, self.isAnimatingAutoscroll ? transition : .immediate) } @@ -703,7 +721,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { self.sendSticker = sendSticker self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.25) self.dimNode.alpha = 0.0 self.containerContainingNode = ASDisplayNode() @@ -769,7 +787,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { switch action { case .add: var allAdded = true - for i in index + 1 ..< strongSelf.stickerPacks.count { + for _ in index + 1 ..< strongSelf.stickerPacks.count { if let container = strongSelf.containers[index], let (_, _, installed) = container.currentStickerPack { if !installed { allAdded = false @@ -784,6 +802,8 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { case .remove: if strongSelf.stickerPacks.count == 1 { return .dismiss + } else { + return .ignored } } } @@ -801,7 +821,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { let modalProgress = container.modalProgress strongSelf.modalProgressUpdated(modalProgress, transition) strongSelf.containerLayoutUpdated(layout, transition: expandTransition) - for (otherIndex, otherContainer) in strongSelf.containers { + for (_, otherContainer) in strongSelf.containers { if otherContainer !== container { otherContainer.syncExpandProgress(expandScrollProgress: container.expandScrollProgress, expandProgress: container.expandProgress, modalProgress: container.modalProgress, transition: expandTransition) } @@ -881,11 +901,16 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { let deltaOffset = self.relativeToSelectedStickerPackTransition self.relativeToSelectedStickerPackTransition = 0.0 if let layout = self.validLayout { - let existingIndices = Array(self.containers.keys) - let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) - self.containerLayoutUpdated(layout, transition: transition) + var previousFrames: [Int: CGRect] = [:] for (key, container) in self.containers { - if !existingIndices.contains(key) { + previousFrames[key] = container.frame + } + let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + self.containerLayoutUpdated(layout, transition: .immediate) + for (key, container) in self.containers { + if let previousFrame = previousFrames[key] { + transition.animatePositionAdditive(node: container, offset: CGPoint(x: previousFrame.minX - container.frame.minX, y: 0.0)) + } else { transition.animatePositionAdditive(node: container, offset: CGPoint(x: -deltaOffset, y: 0.0)) } } @@ -936,8 +961,18 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { } } - let result = super.hitTest(point, with: event) - return result + if let result = super.hitTest(point, with: event) { + for (index, container) in self.containers { + if result.isDescendant(of: container.view) { + if index != self.selectedStickerPackIndex { + return self.containerContainingNode.view + } + } + } + return result + } else { + return nil + } } @objc private func dimNodeTapGesture(_ recognizer: UITapGestureRecognizer) { diff --git a/submodules/SyncCore/Sources/TelegramMediaPoll.swift b/submodules/SyncCore/Sources/TelegramMediaPoll.swift index 3f8cb15f6d..b70b1876d6 100644 --- a/submodules/SyncCore/Sources/TelegramMediaPoll.swift +++ b/submodules/SyncCore/Sources/TelegramMediaPoll.swift @@ -50,12 +50,22 @@ public struct TelegramMediaPollOptionVoters: Equatable, PostboxCoding { } public struct TelegramMediaPollResults: Equatable, PostboxCoding { + public struct Solution: Equatable { + public let text: String + public let entities: [MessageTextEntity] + + public init(text: String, entities: [MessageTextEntity]) { + self.text = text + self.entities = entities + } + } + public let voters: [TelegramMediaPollOptionVoters]? public let totalVoters: Int32? public let recentVoters: [PeerId] - public let solution: String? + public let solution: TelegramMediaPollResults.Solution? - public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId], solution: String?) { + public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId], solution: TelegramMediaPollResults.Solution?) { self.voters = voters self.totalVoters = totalVoters self.recentVoters = recentVoters @@ -66,7 +76,12 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding { self.voters = decoder.decodeOptionalObjectArrayWithDecoderForKey("v") self.totalVoters = decoder.decodeOptionalInt32ForKey("t") self.recentVoters = decoder.decodeInt64ArrayForKey("rv").map(PeerId.init) - self.solution = decoder.decodeOptionalStringForKey("sol") + if let text = decoder.decodeOptionalStringForKey("sol") { + let entities: [MessageTextEntity] = decoder.decodeObjectArrayWithDecoderForKey("solent") + self.solution = TelegramMediaPollResults.Solution(text: text, entities: entities) + } else { + self.solution = nil + } } public func encode(_ encoder: PostboxEncoder) { @@ -82,7 +97,8 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding { } encoder.encodeInt64Array(self.recentVoters.map { $0.toInt64() }, forKey: "rv") if let solution = self.solution { - encoder.encodeString(solution, forKey: "sol") + encoder.encodeString(solution.text, forKey: "sol") + encoder.encodeObjectArray(solution.entities, forKey: "solent") } else { encoder.encodeNil(forKey: "sol") } @@ -248,11 +264,11 @@ public final class TelegramMediaPoll: Media, Equatable { } updatedResults = TelegramMediaPollResults(voters: updatedVoters.map({ voters in return TelegramMediaPollOptionVoters(selected: selectedOpaqueIdentifiers.contains(voters.opaqueIdentifier), opaqueIdentifier: voters.opaqueIdentifier, count: voters.count, isCorrect: correctOpaqueIdentifiers.contains(voters.opaqueIdentifier)) - }), totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution) + }), totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution) } else if let updatedVoters = results.voters { - updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution) + updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution) } else { - updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution) + updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution) } } else { updatedResults = results diff --git a/submodules/TelegramCore/Sources/CachedStickerPack.swift b/submodules/TelegramCore/Sources/CachedStickerPack.swift index 2355a47376..3309ba3795 100644 --- a/submodules/TelegramCore/Sources/CachedStickerPack.swift +++ b/submodules/TelegramCore/Sources/CachedStickerPack.swift @@ -14,7 +14,7 @@ public enum CachedStickerPackResult { func cacheStickerPack(transaction: Transaction, info: StickerPackCollectionInfo, items: [ItemCollectionItem], reference: StickerPackReference? = nil) { transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(info.id)), entry: CachedStickerPack(info: info, items: items.map { $0 as! StickerPackItem }, hash: info.hash), collectionSpec: collectionSpec) - transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: info.shortName)), entry: CachedStickerPack(info: info, items: items.map { $0 as! StickerPackItem }, hash: info.hash), collectionSpec: collectionSpec) + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: info.shortName.lowercased())), entry: CachedStickerPack(info: info, items: items.map { $0 as! StickerPackItem }, hash: info.hash), collectionSpec: collectionSpec) if let reference = reference { var namespace: Int32? @@ -62,7 +62,7 @@ public func cachedStickerPack(postbox: Postbox, network: Network, reference: Sti return (.fetching, true, nil) } case let .name(shortName): - if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName))) as? CachedStickerPack, let info = cached.info { + if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName.lowercased()))) as? CachedStickerPack, let info = cached.info { previousHash = cached.hash let current: CachedStickerPackResult = .result(info, cached.items, false) if cached.hash != info.hash { @@ -158,7 +158,7 @@ func cachedStickerPack(transaction: Transaction, reference: StickerPackReference } } } - if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName))) as? CachedStickerPack, let info = cached.info { + if let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks, key: CachedStickerPack.cacheKey(shortName: shortName.lowercased()))) as? CachedStickerPack, let info = cached.info { return (info, cached.items, false) } case .animatedEmoji: diff --git a/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift index fc13a3b243..2634addfe3 100644 --- a/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessageUploadedContent.swift @@ -178,10 +178,14 @@ func mediaContentToUpload(network: Network, postbox: Postbox, auxiliaryMethods: if poll.deadlineTimeout != nil { pollFlags |= 1 << 4 } - if poll.results.solution != nil { + var mappedSolution: String? + var mappedSolutionEntities: [Api.MessageEntity]? + if let solution = poll.results.solution { + mappedSolution = solution.text + mappedSolutionEntities = apiTextAttributeEntities(TextEntitiesMessageAttribute(entities: solution.entities), associatedPeers: SimpleDictionary()) pollMediaFlags |= 1 << 1 } - let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: poll.results.solution, solutionEntities: poll.results.solution != nil ? [] : nil) + let inputPoll = Api.InputMedia.inputMediaPoll(flags: pollMediaFlags, poll: Api.Poll.poll(id: 0, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities) return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputPoll, text), reuploadInfo: nil))) } else if let _ = media as? TelegramMediaDice { let input = Api.InputMedia.inputMediaDice diff --git a/submodules/TelegramCore/Sources/Polls.swift b/submodules/TelegramCore/Sources/Polls.swift index 42e18ae993..bf58d30c15 100644 --- a/submodules/TelegramCore/Sources/Polls.swift +++ b/submodules/TelegramCore/Sources/Polls.swift @@ -129,11 +129,16 @@ public func requestClosePoll(postbox: Postbox, network: Network, stateManager: A if poll.deadlineTimeout != nil { pollFlags |= 1 << 4 } - if poll.results.solution != nil { + + var mappedSolution: String? + var mappedSolutionEntities: [Api.MessageEntity]? + if let solution = poll.results.solution { + mappedSolution = solution.text + mappedSolutionEntities = apiTextAttributeEntities(TextEntitiesMessageAttribute(entities: solution.entities), associatedPeers: SimpleDictionary()) pollMediaFlags |= 1 << 1 } - return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: poll.results.solution, solutionEntities: poll.results.solution != nil ? [] : nil), replyMarkup: nil, entities: nil, scheduleDate: nil)) + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(flags: pollMediaFlags, poll: .poll(id: poll.pollId.id, flags: pollFlags, question: poll.text, answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil), correctAnswers: correctAnswers, solution: mappedSolution, solutionEntities: mappedSolutionEntities), replyMarkup: nil, entities: nil, scheduleDate: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramMediaPoll.swift b/submodules/TelegramCore/Sources/TelegramMediaPoll.swift index fc12a10e1e..34c84ec378 100644 --- a/submodules/TelegramCore/Sources/TelegramMediaPoll.swift +++ b/submodules/TelegramCore/Sources/TelegramMediaPoll.swift @@ -29,10 +29,15 @@ extension TelegramMediaPollOptionVoters { extension TelegramMediaPollResults { init(apiResults: Api.PollResults) { switch apiResults { - case let .pollResults(_, results, totalVoters, recentVoters, solution, _): + case let .pollResults(_, results, totalVoters, recentVoters, solution, solutionEntities): + var parsedSolution: TelegramMediaPollResults.Solution? + if let solution = solution, let solutionEntities = solutionEntities, !solution.isEmpty { + parsedSolution = TelegramMediaPollResults.Solution(text: solution, entities: messageTextEntitiesFromApiEntities(solutionEntities)) + } + self.init(voters: results.flatMap({ $0.map(TelegramMediaPollOptionVoters.init(apiVoters:)) }), totalVoters: totalVoters, recentVoters: recentVoters.flatMap { recentVoters in return recentVoters.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: $0) } - } ?? [], solution: solution) + } ?? [], solution: parsedSolution) } } } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 477ce7cfa6..527052a3ef 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -286,6 +286,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private weak var mediaRestrictedTooltipController: TooltipController? private var mediaRestrictedTooltipControllerMode = true + private var currentMessageTooltipScreens: [TooltipScreen] = [] + private weak var slowmodeTooltipController: ChatSlowmodeHintController? private weak var currentContextController: ContextController? @@ -1695,11 +1697,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let solution = resultPoll.results.solution { for contentNode in itemNode.contentNodes { if let contentNode = contentNode as? ChatMessagePollBubbleContentNode, let sourceNode = contentNode.solutionTipSourceNode { - let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view) - - strongSelf.present(TooltipScreen(text: solution, icon: .info, location: absoluteFrame, shouldDismissOnTouch: { _ in - return true - }), in: .current) + let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view).insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: 0.0, dy: 0.0) + let tooltipScreen = TooltipScreen(text: solution.text, textEntities: solution.entities, icon: nil, location: absoluteFrame, shouldDismissOnTouch: { point in + return .dismiss(consume: absoluteFrame.contains(point)) + }, openUrl: { url in + self?.openUrl(url, concealed: false) + }) + tooltipScreen.becameDismissed = { tooltipScreen in + guard let strongSelf = self else { + return + } + strongSelf.currentMessageTooltipScreens.removeAll(where: { $0 === tooltipScreen }) + } + strongSelf.currentMessageTooltipScreens.append(tooltipScreen) + strongSelf.present(tooltipScreen, in: .current) } } } @@ -1947,14 +1958,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } strongSelf.presentPollCreation(isQuiz: isQuiz) - }, displayPollSolution: { [weak self] text, sourceNode in + }, displayPollSolution: { [weak self] solution, sourceNode in guard let strongSelf = self else { return } - let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view).insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -12.0, dy: 0.0) - strongSelf.present(TooltipScreen(text: text, icon: .info, location: absoluteFrame, shouldDismissOnTouch: { _ in - return true - }), in: .current) + let absoluteFrame = sourceNode.view.convert(sourceNode.bounds, to: strongSelf.view).insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: 0.0, dy: 0.0) + let tooltipScreen = TooltipScreen(text: solution.text, textEntities: solution.entities, icon: nil, location: absoluteFrame, shouldDismissOnTouch: { point in + return .dismiss(consume: absoluteFrame.contains(point)) + }, openUrl: { url in + self?.openUrl(url, concealed: false) + }) + tooltipScreen.becameDismissed = { tooltipScreen in + guard let strongSelf = self else { + return + } + strongSelf.currentMessageTooltipScreens.removeAll(where: { $0 === tooltipScreen }) + } + strongSelf.currentMessageTooltipScreens.append(tooltipScreen) + strongSelf.present(tooltipScreen, in: .current) }, requestMessageUpdate: { [weak self] id in if let strongSelf = self { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) @@ -2668,6 +2689,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G override public func loadDisplayNode() { self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, controller: self) + self.chatDisplayNode.historyNode.didScrollWithOffset = { [weak self] offset in + guard let strongSelf = self else { + return + } + for tooltipScreen in strongSelf.currentMessageTooltipScreens { + tooltipScreen.addRelativeScrollingOffset(-offset) + } + } + self.chatDisplayNode.peerView = self.peerView let initialData = self.chatDisplayNode.historyNode.initialData diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index 0d7c39a17f..634d01edbf 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -107,7 +107,7 @@ public final class ChatControllerInteraction { let dismissReplyMarkupMessage: (Message) -> Void let openMessagePollResults: (MessageId, Data) -> Void let openPollCreation: (Bool?) -> Void - let displayPollSolution: (String, ASDisplayNode) -> Void + let displayPollSolution: (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void let requestMessageUpdate: (MessageId) -> Void let cancelInteractiveKeyboardGestures: () -> Void @@ -122,7 +122,7 @@ public final class ChatControllerInteraction { var searchTextHighightState: (String, [MessageIndex])? var seenOneTimeAnimatedMedia = Set() - init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (String, ASDisplayNode) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, displayPollSolution: @escaping (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index b82a91f689..d9ecf06d78 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -996,8 +996,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + let additionalTextRightInset: CGFloat = 24.0 + let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right - let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height) + let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset - additionalTextRightInset, height: constrainedSize.height) var edited = false if item.attributes.updatingMedia != nil { @@ -1142,6 +1144,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { statusFrame = statusFrame?.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) var boundingSize: CGSize = textFrameWithoutInsets.size + boundingSize.width += additionalTextRightInset boundingSize.width = max(boundingSize.width, typeLayout.size.width) boundingSize.width = max(boundingSize.width, votersLayout.size.width + 4.0 + (statusSize?.width ?? 0.0)) boundingSize.width = max(boundingSize.width, buttonSubmitInactiveTextLayout.size.width + 4.0 + (statusSize?.width ?? 0.0)) @@ -1456,7 +1459,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { timerTransition.updateTransformScale(node: timerNode, scale: 0.1) } - if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .quiz = poll.kind, let solution = poll.results.solution, !solution.isEmpty, (isClosed || hasSelected) { + if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .quiz = poll.kind, let solution = poll.results.solution, (isClosed || hasSelected) { let solutionButtonNode: SolutionButtonNode if let current = strongSelf.solutionButtonNode { solutionButtonNode = current diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 55fa62ae71..6a3739c596 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -19,6 +19,7 @@ import JoinLinkPreviewUI import LanguageLinkPreviewUI import SettingsUI import UrlHandling +import ShareController private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -224,25 +225,46 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } if let to = to { - let query = to.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789").inverted) - let _ = (context.account.postbox.searchContacts(query: query) - |> deliverOnMainQueue).start(next: { (peers, _) in - for case let peer as TelegramUser in peers { - if peer.phone == query { - continueWithPeer(peer.id) - break + if to.hasPrefix("@") { + let _ = (resolvePeerByName(account: context.account, name: String(to[to.index(to.startIndex, offsetBy: 1)...])) + |> deliverOnMainQueue).start(next: { peerId in + if let peerId = peerId { + let _ = (context.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { peer in + context.sharedContext.applicationBindings.dismissNativeController() + continueWithPeer(peer.id) + }) + } + }) + } else { + let query = to.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789").inverted) + let _ = (context.account.postbox.searchContacts(query: query) + |> deliverOnMainQueue).start(next: { (peers, _) in + for case let peer as TelegramUser in peers { + if peer.phone == query { + context.sharedContext.applicationBindings.dismissNativeController() + continueWithPeer(peer.id) + break + } + } + }) + } + } else { + if let url = url, !url.isEmpty { + let shareController = ShareController(context: context, subject: .url(url), presetText: text, externalShare: false, immediateExternalShare: false) + present(shareController, nil) + context.sharedContext.applicationBindings.dismissNativeController() + } else { + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) + controller.peerSelected = { [weak controller] peerId in + if let strongController = controller { + strongController.dismiss() + continueWithPeer(peerId) } } - }) - } else { - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled])) - controller.peerSelected = { [weak controller] peerId in - if let strongController = controller { - strongController.dismiss() - continueWithPeer(peerId) - } + context.sharedContext.applicationBindings.dismissNativeController() + navigationController?.pushViewController(controller) } - navigationController?.pushViewController(controller) } case let .wallpaper(parameter): let presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index 233b35c6ba..f0f67b96c0 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -401,10 +401,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen var availablePanes = availablePanes if availablePanes != nil, groupsInCommon != nil, let cachedData = peerView.cachedData as? CachedUserData { if cachedData.commonGroupCount != 0 { - if ignoreGroupInCommon != nil && cachedData.commonGroupCount == 1 { - } else { - availablePanes?.append(.groupsInCommon) - } + availablePanes?.append(.groupsInCommon) } } diff --git a/submodules/TelegramUI/Sources/PollBubbleTimerNode.swift b/submodules/TelegramUI/Sources/PollBubbleTimerNode.swift index 7e525edac3..fec6619852 100644 --- a/submodules/TelegramUI/Sources/PollBubbleTimerNode.swift +++ b/submodules/TelegramUI/Sources/PollBubbleTimerNode.swift @@ -11,9 +11,8 @@ private func textForTimeout(value: Int) -> String { } else { let minutes = value / 60 let seconds = value % 60 - let minutesPadding = minutes < 10 ? "0" : "" let secondsPadding = seconds < 10 ? "0" : "" - return "\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)" + return "\(minutes):\(secondsPadding)\(seconds)" } } diff --git a/submodules/TooltipUI/BUCK b/submodules/TooltipUI/BUCK index 5d1b38696f..ad85d29e1e 100644 --- a/submodules/TooltipUI/BUCK +++ b/submodules/TooltipUI/BUCK @@ -11,6 +11,9 @@ static_library( "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AppBundle:AppBundle", "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/SyncCore:SyncCore", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TextFormat:TextFormat", ], frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", diff --git a/submodules/TooltipUI/BUILD b/submodules/TooltipUI/BUILD index d49d5c90b3..3af5dc456c 100644 --- a/submodules/TooltipUI/BUILD +++ b/submodules/TooltipUI/BUILD @@ -12,6 +12,9 @@ swift_library( "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AppBundle:AppBundle", "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/SyncCore:SyncCore", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 1a4272918b..9d4f1dd76f 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -5,13 +5,17 @@ import Display import TelegramPresentationData import AnimatedStickerNode import AppBundle +import SyncCore +import TelegramCore +import TextFormat private final class TooltipScreenNode: ViewControllerTracingNode { - private let icon: TooltipScreen.Icon + private let icon: TooltipScreen.Icon? private let location: CGRect - private let shouldDismissOnTouch: (CGPoint) -> Bool + private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch private let requestDismiss: () -> Void + private let scrollingContainer: ASDisplayNode private let containerNode: ASDisplayNode private let backgroundNode: ASImageNode private let arrowNode: ASImageNode @@ -19,7 +23,9 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private let animatedStickerNode: AnimatedStickerNode private let textNode: ImmediateTextNode - init(text: String, icon: TooltipScreen.Icon, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> Bool, requestDismiss: @escaping () -> Void) { + private var isArrowInverted: Bool = false + + init(text: String, textEntities: [MessageTextEntity], icon: TooltipScreen.Icon?, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openUrl: @escaping (String) -> Void) { self.icon = icon self.location = location self.shouldDismissOnTouch = shouldDismissOnTouch @@ -29,6 +35,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode { let fillColor = UIColor(white: 0.0, alpha: 0.8) + self.scrollingContainer = ASDisplayNode() + self.backgroundNode = ASImageNode() self.backgroundNode.image = generateAdjustedStretchableFilledCircleImage(diameter: 15.0, color: fillColor) @@ -47,10 +55,13 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false self.textNode.maximumNumberOfLines = 0 - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white) + + self.textNode.attributedText = stringWithAppliedEntities(text, entities: textEntities, baseColor: .white, linkColor: .white, baseFont: Font.regular(14.0), linkFont: Font.regular(14.0), boldFont: Font.semibold(14.0), italicFont: Font.italic(14.0), boldItalicFont: Font.semiboldItalic(14.0), fixedFont: Font.monospace(14.0), blockQuoteFont: Font.regular(14.0), underlineLinks: true, external: false) self.animatedStickerNode = AnimatedStickerNode() switch icon { + case .none: + break case .chatListPress: if let path = getAppBundle().path(forResource: "ChatListFoldersTooltip", ofType: "json") { self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct) @@ -70,36 +81,80 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.textNode) self.containerNode.addSubnode(self.animatedStickerNode) - self.addSubnode(self.containerNode) + self.scrollingContainer.addSubnode(self.containerNode) + self.addSubnode(self.scrollingContainer) + + self.textNode.linkHighlightColor = UIColor.white.withAlphaComponent(0.5) + self.textNode.highlightAttributeAction = { attributes in + let highlightedAttributes = [ + TelegramTextAttributes.URL, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag, + TelegramTextAttributes.Timecode + ] + + for attribute in highlightedAttributes { + if let _ = attributes[NSAttributedString.Key(rawValue: attribute)] { + return NSAttributedString.Key(rawValue: attribute) + } + } + return nil + } + self.textNode.tapAttributeAction = { [weak self] attributes in + guard let strongSelf = self else { + return + } + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + openUrl(url) + } + } } func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.scrollingContainer.frame = CGRect(origin: CGPoint(), size: layout.size) + let sideInset: CGFloat = 13.0 let bottomInset: CGFloat = 10.0 let contentInset: CGFloat = 9.0 let contentVerticalInset: CGFloat = 11.0 let animationSize: CGSize let animationInset: CGFloat + let animationSpacing: CGFloat switch self.icon { + case .none: + animationSize = CGSize() + animationInset = 0.0 + animationSpacing = 0.0 case .chatListPress: animationSize = CGSize(width: 32.0, height: 32.0) animationInset = (70.0 - animationSize.width) / 2.0 + animationSpacing = 8.0 case .info: animationSize = CGSize(width: 32.0, height: 32.0) animationInset = 0.0 + animationSpacing = 8.0 } - let animationSpacing: CGFloat = 8.0 let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - contentInset * 2.0 - sideInset * 2.0 - animationSize.width - animationSpacing, height: .greatestFiniteMagnitude)) + let backgroundWidth = textSize.width + contentInset * 2.0 + sideInset * 2.0 + animationSize.width + animationSpacing let backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0 - var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: self.location.minY - bottomInset - backgroundHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: backgroundHeight)) + var backgroundFrame = CGRect(origin: CGPoint(x: self.location.midX - backgroundWidth / 2.0, y: self.location.minY - bottomInset - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight)) + if backgroundFrame.minX < sideInset { + backgroundFrame.origin.x = sideInset + } + if backgroundFrame.maxX > layout.size.width - sideInset { + backgroundFrame.origin.x = layout.size.width - sideInset - backgroundFrame.width + } var invertArrow = false if backgroundFrame.minY < layout.insets(options: .statusBar).top { backgroundFrame.origin.y = self.location.maxY + bottomInset invertArrow = true } + self.isArrowInverted = invertArrow transition.updateFrame(node: self.containerNode, frame: backgroundFrame) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) @@ -115,7 +170,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { arrowFrame = CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: backgroundFrame.height), size: arrowSize) } - transition.updateFrame(node: self.arrowContainer, frame: arrowFrame) + transition.updateFrame(node: self.arrowContainer, frame: arrowFrame.offsetBy(dx: -backgroundFrame.minX, dy: 0.0)) ContainedViewLayoutTransition.immediate.updateTransformScale(node: self.arrowContainer, scale: CGPoint(x: 1.0, y: invertArrow ? -1.0 : 1.0)) @@ -130,13 +185,23 @@ private final class TooltipScreenNode: ViewControllerTracingNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let event = event { + if let result = self.textNode.hitTest(self.view.convert(point, to: self.textNode.view), with: event) { + return result + } + var eventIsPresses = false if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { eventIsPresses = event.type == .presses } if event.type == .touches || eventIsPresses { - if self.shouldDismissOnTouch(point) { + switch self.shouldDismissOnTouch(point) { + case .ignore: + break + case let .dismiss(consume): self.requestDismiss() + if consume { + return self.view + } } return nil } @@ -146,7 +211,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode { func animateIn() { self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.01)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) - self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowContainer.frame.maxY - self.containerNode.bounds.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.6, additive: true) + let arrowY: CGFloat = self.isArrowInverted ? self.arrowContainer.frame.minY : self.arrowContainer.frame.maxY + self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.6, additive: true) self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) let animationDelay: Double @@ -155,6 +221,8 @@ private final class TooltipScreenNode: ViewControllerTracingNode { animationDelay = 0.6 case .info: animationDelay = 0.2 + case .none: + animationDelay = 0.0 } DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + animationDelay, execute: { [weak self] in @@ -167,7 +235,13 @@ private final class TooltipScreenNode: ViewControllerTracingNode { completion() }) self.containerNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowContainer.frame.maxY - self.containerNode.bounds.height / 2.0), duration: 0.2, removeOnCompletion: false, additive: true) + + let arrowY: CGFloat = self.isArrowInverted ? self.arrowContainer.frame.minY : self.arrowContainer.frame.maxY + self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.arrowContainer.frame.midX - self.containerNode.bounds.width / 2.0, y: arrowY - self.containerNode.bounds.height / 2.0), duration: 0.2, removeOnCompletion: false, additive: true) + } + + func addRelativeScrollingOffset(_ value: CGFloat) { + self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: value) } } @@ -177,10 +251,17 @@ public final class TooltipScreen: ViewController { case chatListPress } + public enum DismissOnTouch { + case ignore + case dismiss(consume: Bool) + } + private let text: String - private let icon: TooltipScreen.Icon + private let textEntities: [MessageTextEntity] + private let icon: TooltipScreen.Icon? private let location: CGRect - private let shouldDismissOnTouch: (CGPoint) -> Bool + private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch + private let openUrl: (String) -> Void private var controllerNode: TooltipScreenNode { return self.displayNode as! TooltipScreenNode @@ -189,11 +270,15 @@ public final class TooltipScreen: ViewController { private var validLayout: ContainerViewLayout? private var isDismissed: Bool = false - public init(text: String, icon: TooltipScreen.Icon, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> Bool) { + public var becameDismissed: ((TooltipScreen) -> Void)? + + public init(text: String, textEntities: [MessageTextEntity] = [], icon: TooltipScreen.Icon?, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, openUrl: @escaping (String) -> Void = { _ in }) { self.text = text + self.textEntities = textEntities self.icon = icon self.location = location self.shouldDismissOnTouch = shouldDismissOnTouch + self.openUrl = openUrl super.init(navigationBarPresentationData: nil) @@ -209,18 +294,18 @@ public final class TooltipScreen: ViewController { self.controllerNode.animateIn() - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 8.0, execute: { [weak self] in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 20.0, execute: { [weak self] in self?.dismiss() }) } override public func loadDisplayNode() { - self.displayNode = TooltipScreenNode(text: self.text, icon: self.icon, location: self.location, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in + self.displayNode = TooltipScreenNode(text: self.text, textEntities: self.textEntities, icon: self.icon, location: self.location, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in guard let strongSelf = self else { return } strongSelf.dismiss() - }) + }, openUrl: self.openUrl) self.displayNodeDidLoad() } @@ -237,6 +322,10 @@ public final class TooltipScreen: ViewController { self.controllerNode.updateLayout(layout: layout, transition: transition) } + public func addRelativeScrollingOffset(_ value: CGFloat) { + self.controllerNode.addRelativeScrollingOffset(value) + } + override public func dismiss(completion: (() -> Void)? = nil) { if self.isDismissed { return @@ -246,7 +335,9 @@ public final class TooltipScreen: ViewController { guard let strongSelf = self else { return } + let becameDismissed = strongSelf.becameDismissed strongSelf.presentingViewController?.dismiss(animated: false, completion: nil) + becameDismissed?(strongSelf) }) } }