mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Assorted fixes
This commit is contained in:
parent
a7282bf2fc
commit
7f02fb759a
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -17,6 +17,7 @@ swift_library(
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/AlertUI:AlertUI",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)"
|
||||
}
|
||||
|
@ -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?()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
16
submodules/ShimmerEffect/BUCK
Normal file
16
submodules/ShimmerEffect/BUCK
Normal file
@ -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",
|
||||
],
|
||||
)
|
16
submodules/ShimmerEffect/BUILD
Normal file
16
submodules/ShimmerEffect/BUILD
Normal file
@ -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",
|
||||
],
|
||||
)
|
200
submodules/ShimmerEffect/Sources/ShimmerEffect.swift
Normal file
200
submodules/ShimmerEffect/Sources/ShimmerEffect.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -26,6 +26,7 @@ swift_library(
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/ArchivedStickerPacksNotice:ArchivedStickerPacksNotice",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<Bool>()
|
||||
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) {
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<MessageId>()
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user