mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Sticker pack preview updates
This commit is contained in:
parent
728d68dc6d
commit
78e12ad45e
22 changed files with 459 additions and 129 deletions
|
|
@ -864,10 +864,10 @@ ios_application(
|
|||
":AppStringResources",
|
||||
],
|
||||
extensions = [
|
||||
":ShareExtension",
|
||||
":NotificationServiceExtension",
|
||||
#":ShareExtension",
|
||||
#":NotificationServiceExtension",
|
||||
],
|
||||
watch_application = ":TelegramWatchApp",
|
||||
#watch_application = ":TelegramWatchApp",
|
||||
deps = [
|
||||
":Main",
|
||||
":Lib",
|
||||
|
|
|
|||
|
|
@ -1197,7 +1197,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
} else {
|
||||
text = strongSelf.presentationData.strings.ChatList_TabIconFoldersTooltipEmptyFolders
|
||||
}
|
||||
parentController.present(TooltipScreen(text: text, location: CGPoint(x: absoluteFrame.midX - 14.0, y: absoluteFrame.minY - 8.0), shouldDismissOnTouch: { point in
|
||||
|
||||
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX - 14.0, 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,9 +222,11 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
|||
public var scrollingInitiated: (() -> Void)?
|
||||
public var scrollingCompleted: (() -> Void)?
|
||||
public var interactiveScrollingEnded: (() -> Void)?
|
||||
public var interactiveScrollingWillBeEnded: ((CGPoint, CGPoint) -> Void)?
|
||||
public var interactiveScrollingWillBeEnded: ((CGPoint, CGPoint, CGPoint) -> CGPoint)?
|
||||
public var visibleContentOffsetChanged: (GridNodeVisibleContentOffset) -> Void = { _ in }
|
||||
|
||||
private var autoscrollingAnimator: DisplayLinkAnimator?
|
||||
|
||||
public final var floatingSections = false
|
||||
|
||||
public final var initialOffset: CGFloat = 0.0
|
||||
|
|
@ -256,6 +258,10 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.autoscrollingAnimator?.invalidate()
|
||||
}
|
||||
|
||||
public func transaction(_ transaction: GridNodeTransaction, completion: (GridNodeDisplayedItemRange) -> Void) {
|
||||
if let updateOpaqueState = transaction.updateOpaqueState {
|
||||
self.opaqueState = updateOpaqueState
|
||||
|
|
@ -365,14 +371,45 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
|||
self.applyPresentationLayoutTransition(self.generatePresentationLayoutTransition(stationaryItems: transaction.stationaryItems, layoutTransactionOffset: layoutTransactionOffset, scrollToItem: generatedScrollToItem), removedNodes: removedNodes, updateLayoutTransition: updateLayoutTransition, customScrollToItem: transaction.scrollToItem != nil, itemTransition: transaction.itemTransition, synchronousLoads: transaction.synchronousLoads, updatingLayout: transaction.updateLayout != nil, completion: completion)
|
||||
}
|
||||
|
||||
public func autoscroll(toOffset: CGPoint, duration: Double) {
|
||||
if let autoscrollingAnimator = self.autoscrollingAnimator {
|
||||
self.autoscrollingAnimator = nil
|
||||
autoscrollingAnimator.invalidate()
|
||||
}
|
||||
|
||||
let fromOffset: CGPoint = self.scrollView.contentOffset
|
||||
|
||||
self.autoscrollingAnimator = DisplayLinkAnimator(duration: duration, from: 0.0, to: 1.0, update: { [weak self] t in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let mappedT = bezierPoint(0.23, 1.0, 0.32, 1.0, t)
|
||||
let offset = CGPoint(x: 0.0, y: (1.0 - mappedT) * fromOffset.y + mappedT * toOffset.y)
|
||||
strongSelf.scrollView.setContentOffset(offset, animated: false)
|
||||
}, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.autoscrollingAnimator?.invalidate()
|
||||
strongSelf.autoscrollingAnimator = nil
|
||||
})
|
||||
}
|
||||
|
||||
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
if let autoscrollingAnimator = self.autoscrollingAnimator {
|
||||
self.autoscrollingAnimator = nil
|
||||
autoscrollingAnimator.invalidate()
|
||||
}
|
||||
|
||||
self.updateItemNodeVisibilititesAndScrolling()
|
||||
self.updateVisibleContentOffset()
|
||||
self.scrollingInitiated?()
|
||||
}
|
||||
|
||||
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
self.interactiveScrollingWillBeEnded?(velocity, targetContentOffset.pointee)
|
||||
if let interactiveScrollingWillBeEnded = self.interactiveScrollingWillBeEnded {
|
||||
targetContentOffset.pointee = interactiveScrollingWillBeEnded(scrollView.contentOffset, velocity, targetContentOffset.pointee)
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
|
|
|
|||
|
|
@ -395,6 +395,8 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||
let _ = (context.account.postbox.transaction { transaction -> Void in
|
||||
transaction.clearItemCacheCollection(collectionId: Namespaces.CachedItemCollection.cachedPollResults)
|
||||
unmarkChatListFeaturedFiltersAsSeen(transaction: transaction)
|
||||
|
||||
transaction.clearItemCacheCollection(collectionId: Namespaces.CachedItemCollection.cachedStickerPacks)
|
||||
}).start()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ private struct StickerPackPreviewGridEntry: Comparable, Identifiable {
|
|||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(account: Account, interaction: StickerPackPreviewInteraction) -> StickerPackPreviewGridItem {
|
||||
return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction)
|
||||
func item(account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) -> StickerPackPreviewGridItem {
|
||||
return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction, theme: theme, isEmpty: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,12 +35,12 @@ private struct StickerPackPreviewGridTransaction {
|
|||
let insertions: [GridNodeInsertItem]
|
||||
let updates: [GridNodeUpdateItem]
|
||||
|
||||
init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction) {
|
||||
init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list)
|
||||
|
||||
self.deletions = deleteIndices
|
||||
self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) }
|
||||
self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) }
|
||||
self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction, theme: theme), previousIndex: $0.2) }
|
||||
self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction, theme: theme)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -385,7 +385,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol
|
|||
self.contentTitleNode.attributedText = stringWithAppliedEntities(info.title, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font)
|
||||
animateIn = true
|
||||
}
|
||||
transaction = StickerPackPreviewGridTransaction(previousList: self.currentItems, list: updatedItems, account: self.context.account, interaction: self.interaction)
|
||||
transaction = StickerPackPreviewGridTransaction(previousList: self.currentItems, list: updatedItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme)
|
||||
self.currentItems = updatedItems
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import StickerResources
|
|||
import AccountContext
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import TelegramPresentationData
|
||||
|
||||
final class StickerPackPreviewInteraction {
|
||||
var previewedItem: StickerPreviewPeekItem?
|
||||
|
|
@ -22,20 +23,24 @@ final class StickerPackPreviewInteraction {
|
|||
|
||||
final class StickerPackPreviewGridItem: GridItem {
|
||||
let account: Account
|
||||
let stickerItem: StickerPackItem
|
||||
let stickerItem: StickerPackItem?
|
||||
let interaction: StickerPackPreviewInteraction
|
||||
let theme: PresentationTheme
|
||||
let isEmpty: Bool
|
||||
|
||||
let section: GridSection? = nil
|
||||
|
||||
init(account: Account, stickerItem: StickerPackItem, interaction: StickerPackPreviewInteraction) {
|
||||
init(account: Account, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isEmpty: Bool) {
|
||||
self.account = account
|
||||
self.stickerItem = stickerItem
|
||||
self.interaction = interaction
|
||||
self.theme = theme
|
||||
self.isEmpty = isEmpty
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = StickerPackPreviewGridItemNode()
|
||||
node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction)
|
||||
node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isEmpty: self.isEmpty)
|
||||
return node
|
||||
}
|
||||
|
||||
|
|
@ -44,16 +49,18 @@ final class StickerPackPreviewGridItem: GridItem {
|
|||
assertionFailure()
|
||||
return
|
||||
}
|
||||
node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction)
|
||||
node.setup(account: self.account, stickerItem: self.stickerItem, interaction: self.interaction, theme: self.theme, isEmpty: self.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
private let textFont = Font.regular(20.0)
|
||||
|
||||
final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
private var currentState: (Account, StickerPackItem, CGSize)?
|
||||
private var currentState: (Account, StickerPackItem?)?
|
||||
private var isEmpty: Bool?
|
||||
private let imageNode: TransformImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
private let placeholderNode: ASDisplayNode
|
||||
|
||||
override var isVisibleInGrid: Bool {
|
||||
didSet {
|
||||
|
|
@ -76,10 +83,26 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
|||
override init() {
|
||||
self.imageNode = TransformImageNode()
|
||||
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
|
||||
self.placeholderNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.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)
|
||||
}
|
||||
}
|
||||
firstTime = false
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
@ -92,41 +115,55 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
|||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
|
||||
}
|
||||
|
||||
func setup(account: Account, stickerItem: StickerPackItem, interaction: StickerPackPreviewInteraction) {
|
||||
func setup(account: Account, stickerItem: StickerPackItem?, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, isEmpty: Bool) {
|
||||
self.interaction = interaction
|
||||
|
||||
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem {
|
||||
if let dimensions = stickerItem.file.dimensions {
|
||||
if stickerItem.file.isAnimatedSticker {
|
||||
let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))))
|
||||
|
||||
if self.animationNode == nil {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
self.animationNode = animationNode
|
||||
self.addSubnode(animationNode)
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
self.placeholderNode.backgroundColor = theme.list.mediaPlaceholderColor
|
||||
|
||||
if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 != stickerItem || self.isEmpty != isEmpty {
|
||||
if let stickerItem = stickerItem {
|
||||
if let _ = stickerItem.file.dimensions {
|
||||
if stickerItem.file.isAnimatedSticker {
|
||||
let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))))
|
||||
|
||||
if self.animationNode == nil {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
self.animationNode = animationNode
|
||||
self.addSubnode(animationNode)
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
self?.placeholderNode.alpha = 0.0
|
||||
}
|
||||
}
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
||||
} else {
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.visibility = false
|
||||
self.animationNode = nil
|
||||
animationNode.removeFromSupernode()
|
||||
}
|
||||
self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start())
|
||||
}
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
||||
} else {
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.visibility = false
|
||||
self.animationNode = nil
|
||||
animationNode.removeFromSupernode()
|
||||
}
|
||||
self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start())
|
||||
}
|
||||
|
||||
self.currentState = (account, stickerItem, dimensions.cgSize)
|
||||
self.setNeedsLayout()
|
||||
} 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)
|
||||
}
|
||||
} else {
|
||||
self.placeholderNode.alpha = 1.0
|
||||
}
|
||||
}
|
||||
self.currentState = (account, stickerItem)
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
self.isEmpty = isEmpty
|
||||
|
||||
//self.updateSelectionState(animated: false)
|
||||
//self.updateHiddenMedia()
|
||||
|
|
@ -139,13 +176,17 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
|||
let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0)
|
||||
let boundingSize = CGSize(width: boundsSide, height: boundsSide)
|
||||
|
||||
if let (_, _, mediaDimensions) = self.currentState {
|
||||
let imageSize = mediaDimensions.aspectFitted(boundingSize)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
animationNode.updateLayout(size: imageSize)
|
||||
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 (_, item) = self.currentState {
|
||||
if let item = item, let dimensions = item.file.dimensions?.cgSize {
|
||||
let imageSize = dimensions.aspectFitted(boundingSize)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
animationNode.updateLayout(size: imageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,14 +196,11 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
|||
}
|
||||
|
||||
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
|
||||
if let interaction = self.interaction, let (_, item, _) = self.currentState, case .ended = recognizer.state {
|
||||
//interaction.sendSticker(item)
|
||||
}
|
||||
}
|
||||
|
||||
func updatePreviewing(animated: Bool) {
|
||||
var isPreviewing = false
|
||||
if let (_, item, _) = self.currentState, let interaction = self.interaction {
|
||||
if let (_, maybeItem) = self.currentState, let interaction = self.interaction, let item = maybeItem {
|
||||
isPreviewing = interaction.previewedItem == .pack(item)
|
||||
}
|
||||
if self.currentIsPreviewing != isPreviewing {
|
||||
|
|
|
|||
|
|
@ -13,18 +13,16 @@ import MergeLists
|
|||
|
||||
private struct StickerPackPreviewGridEntry: Comparable, Identifiable {
|
||||
let index: Int
|
||||
let stickerItem: StickerPackItem
|
||||
|
||||
var stableId: MediaId {
|
||||
return self.stickerItem.file.fileId
|
||||
}
|
||||
let stableId: Int
|
||||
let stickerItem: StickerPackItem?
|
||||
let isEmpty: Bool
|
||||
|
||||
static func <(lhs: StickerPackPreviewGridEntry, rhs: StickerPackPreviewGridEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(account: Account, interaction: StickerPackPreviewInteraction) -> StickerPackPreviewGridItem {
|
||||
return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction)
|
||||
func item(account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme) -> StickerPackPreviewGridItem {
|
||||
return StickerPackPreviewGridItem(account: account, stickerItem: self.stickerItem, interaction: interaction, theme: theme, isEmpty: self.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -32,13 +30,16 @@ private struct StickerPackPreviewGridTransaction {
|
|||
let deletions: [Int]
|
||||
let insertions: [GridNodeInsertItem]
|
||||
let updates: [GridNodeUpdateItem]
|
||||
let scrollToItem: GridNodeScrollToItem?
|
||||
|
||||
init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction) {
|
||||
init(previousList: [StickerPackPreviewGridEntry], list: [StickerPackPreviewGridEntry], account: Account, interaction: StickerPackPreviewInteraction, theme: PresentationTheme, scrollToItem: GridNodeScrollToItem?) {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list)
|
||||
|
||||
self.deletions = deleteIndices
|
||||
self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) }
|
||||
self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction)) }
|
||||
self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction, theme: theme), previousIndex: $0.2) }
|
||||
self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interaction: interaction, theme: theme)) }
|
||||
|
||||
self.scrollToItem = scrollToItem
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,11 +68,13 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
private let actionAreaSeparatorNode: ASDisplayNode
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let titlePlaceholderNode: ASDisplayNode
|
||||
private let titleContainer: ASDisplayNode
|
||||
private let titleSeparatorNode: ASDisplayNode
|
||||
|
||||
private(set) var validLayout: (ContainerViewLayout, CGRect, CGFloat, UIEdgeInsets)?
|
||||
|
||||
private var nextStableId: Int = 1
|
||||
private var currentEntries: [StickerPackPreviewGridEntry] = []
|
||||
private var enqueuedTransactions: [StickerPackPreviewGridTransaction] = []
|
||||
|
||||
|
|
@ -85,6 +88,7 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
}
|
||||
|
||||
var expandProgress: CGFloat = 0.0
|
||||
var expandScrollProgress: CGFloat = 0.0
|
||||
var modalProgress: CGFloat = 0.0
|
||||
let expandProgressUpdated: (StickerPackContainer, ContainedViewLayoutTransition) -> Void
|
||||
|
||||
|
|
@ -120,6 +124,9 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
|
||||
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
|
||||
|
|
@ -135,6 +142,7 @@ 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)
|
||||
|
||||
|
|
@ -157,32 +165,75 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
self.gridNode.interactiveScrollingWillBeEnded = { [weak self] velocity, targetOffset in
|
||||
self.gridNode.interactiveScrollingWillBeEnded = { [weak self] contentOffset, velocity, targetOffset -> CGPoint in
|
||||
guard let strongSelf = self, !strongSelf.isDismissed else {
|
||||
return
|
||||
return targetOffset
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
let contentOffset = targetOffset
|
||||
let insets = strongSelf.gridNode.scrollView.contentInset
|
||||
var modalProgress: CGFloat = 0.0
|
||||
|
||||
if contentOffset.y < 0.0 && contentOffset.y >= -insets.top {
|
||||
strongSelf.gridNode.scrollView.stopScrollingAnimation()
|
||||
if contentOffset.y > -insets.top / 2.0 || velocity.y <= -100.0 {
|
||||
strongSelf.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
|
||||
modalProgress = 1.0
|
||||
} else {
|
||||
strongSelf.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: -insets.top), animated: true)
|
||||
}
|
||||
} else if contentOffset.y >= 0.0 {
|
||||
|
||||
let insets = strongSelf.gridNode.scrollView.contentInset
|
||||
var modalProgress: CGFloat = 0.0
|
||||
|
||||
var updatedOffset = targetOffset
|
||||
var resetOffset = false
|
||||
|
||||
if targetOffset.y < 0.0 && targetOffset.y >= -insets.top {
|
||||
if contentOffset.y > 0.0 {
|
||||
updatedOffset = CGPoint(x: 0.0, y: 0.0)
|
||||
modalProgress = 1.0
|
||||
} else {
|
||||
if targetOffset.y > -insets.top / 2.0 || velocity.y <= -100.0 {
|
||||
modalProgress = 1.0
|
||||
resetOffset = true
|
||||
} else {
|
||||
modalProgress = 0.0
|
||||
if contentOffset.y > -insets.top {
|
||||
resetOffset = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if targetOffset.y >= 0.0 {
|
||||
modalProgress = 1.0
|
||||
}
|
||||
|
||||
if abs(strongSelf.modalProgress - modalProgress) > CGFloat.ulpOfOne {
|
||||
if contentOffset.y > 0.0 && targetOffset.y > 0.0 {
|
||||
} else {
|
||||
resetOffset = true
|
||||
}
|
||||
strongSelf.modalProgress = modalProgress
|
||||
strongSelf.expandProgressUpdated(strongSelf, .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
if resetOffset {
|
||||
let offset: CGPoint
|
||||
let isVelocityAligned: Bool
|
||||
if modalProgress.isZero {
|
||||
offset = CGPoint(x: 0.0, y: -insets.top)
|
||||
isVelocityAligned = velocity.y < 0.0
|
||||
} else {
|
||||
offset = CGPoint(x: 0.0, y: 0.0)
|
||||
isVelocityAligned = velocity.y > 0.0
|
||||
}
|
||||
|
||||
if abs(strongSelf.modalProgress - modalProgress) > CGFloat.ulpOfOne {
|
||||
strongSelf.modalProgress = modalProgress
|
||||
strongSelf.expandProgressUpdated(strongSelf, .animated(duration: 0.4, curve: .spring))
|
||||
DispatchQueue.main.async {
|
||||
let duration: Double
|
||||
if isVelocityAligned {
|
||||
let minVelocity: CGFloat = 400.0
|
||||
let maxVelocity: CGFloat = 1000.0
|
||||
let clippedVelocity = max(minVelocity, min(maxVelocity, abs(velocity.y * 500.0)))
|
||||
|
||||
let distance = abs(offset.y - contentOffset.y)
|
||||
duration = Double(distance / clippedVelocity)
|
||||
} else {
|
||||
duration = 0.5
|
||||
}
|
||||
|
||||
strongSelf.gridNode.autoscroll(toOffset: offset, duration: duration)
|
||||
}
|
||||
updatedOffset = contentOffset
|
||||
}
|
||||
|
||||
return updatedOffset
|
||||
}
|
||||
|
||||
self.itemsDisposable = (loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: stickerPack, forceActualized: false)
|
||||
|
|
@ -276,6 +327,7 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
|
||||
@objc func buttonPressed() {
|
||||
guard let (info, items, installed) = currentStickerPack else {
|
||||
self.requestDismiss()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -310,12 +362,57 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
|
||||
var updateLayout = false
|
||||
|
||||
var scrollToItem: GridNodeScrollToItem?
|
||||
|
||||
switch contents {
|
||||
case .fetching:
|
||||
entries = []
|
||||
self.buttonNode.setTitle(self.presentationData.strings.Channel_NotificationLoading.uppercased(), with: Font.semibold(17.0), with: self.presentationData.theme.list.itemDisabledTextColor, for: .normal)
|
||||
self.buttonNode.setBackgroundImage(nil, for: [])
|
||||
|
||||
for _ in 0 ..< 16 {
|
||||
var stableId: Int?
|
||||
inner: for entry in self.currentEntries {
|
||||
if entry.stickerItem == nil, entry.index == entries.count {
|
||||
stableId = entry.stableId
|
||||
break inner
|
||||
}
|
||||
}
|
||||
|
||||
let resolvedStableId: Int
|
||||
if let stableId = stableId {
|
||||
resolvedStableId = stableId
|
||||
} else {
|
||||
resolvedStableId = self.nextStableId
|
||||
self.nextStableId += 1
|
||||
}
|
||||
|
||||
self.nextStableId += 1
|
||||
entries.append(StickerPackPreviewGridEntry(index: entries.count, stableId: resolvedStableId, stickerItem: nil, isEmpty: false))
|
||||
}
|
||||
self.titlePlaceholderNode.alpha = 1.0
|
||||
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)
|
||||
self.buttonNode.setBackgroundImage(nil, for: [])
|
||||
|
||||
for _ in 0 ..< 16 {
|
||||
let resolvedStableId = self.nextStableId
|
||||
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)
|
||||
}
|
||||
case let .result(info, items, installed):
|
||||
if !items.isEmpty && self.currentStickerPack == nil {
|
||||
if let (_, _, _, gridInsets) = self.validLayout, abs(self.expandScrollProgress - 1.0) < .ulpOfOne {
|
||||
scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .immediate, directionHint: .up, adjustForSection: false)
|
||||
}
|
||||
}
|
||||
|
||||
self.currentStickerPack = (info, items, installed)
|
||||
|
||||
if installed {
|
||||
|
|
@ -341,6 +438,16 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: info.title, font: Font.semibold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
||||
|
|
@ -350,7 +457,21 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
guard let item = item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
entries.append(StickerPackPreviewGridEntry(index: entries.count, stickerItem: item))
|
||||
var stableId: Int?
|
||||
inner: for entry in self.currentEntries {
|
||||
if let stickerItem = entry.stickerItem, stickerItem.file.fileId == item.file.fileId {
|
||||
stableId = entry.stableId
|
||||
break inner
|
||||
}
|
||||
}
|
||||
let resolvedStableId: Int
|
||||
if let stableId = stableId {
|
||||
resolvedStableId = stableId
|
||||
} else {
|
||||
resolvedStableId = self.nextStableId
|
||||
self.nextStableId += 1
|
||||
}
|
||||
entries.append(StickerPackPreviewGridEntry(index: entries.count, stableId: resolvedStableId, stickerItem: item, isEmpty: false))
|
||||
}
|
||||
}
|
||||
let previousEntries = self.currentEntries
|
||||
|
|
@ -363,7 +484,11 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
self.updateLayout(layout: layout, transition: .immediate)
|
||||
}
|
||||
|
||||
let transaction = StickerPackPreviewGridTransaction(previousList: previousEntries, list: entries, account: self.context.account, interaction: self.interaction)
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -374,6 +499,19 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
return min(self.backgroundNode.frame.minY, gridFrame.minY + gridInsets.top - titleAreaInset)
|
||||
}
|
||||
|
||||
func syncExpandProgress(expandScrollProgress: CGFloat, expandProgress: CGFloat, modalProgress: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let (_, _, _, gridInsets) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let contentOffset = (1.0 - expandScrollProgress) * (-gridInsets.top)
|
||||
self.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
|
||||
|
||||
self.expandScrollProgress = expandScrollProgress
|
||||
self.expandProgress = expandProgress
|
||||
self.modalProgress = modalProgress
|
||||
}
|
||||
|
||||
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
var insets = layout.insets(options: [.statusBar])
|
||||
insets.top += 10.0
|
||||
|
|
@ -448,10 +586,16 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
let offsetFromInitialPosition = presentationLayout.contentOffset.y + gridInsets.top
|
||||
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)))
|
||||
|
||||
var expandProgressTransition = transition
|
||||
let expandProgressTransition = transition
|
||||
var expandUpdated = false
|
||||
|
||||
if abs(self.expandScrollProgress - expandScrollProgress) > CGFloat.ulpOfOne {
|
||||
self.expandScrollProgress = expandScrollProgress
|
||||
expandUpdated = true
|
||||
}
|
||||
|
||||
if abs(self.expandProgress - expandProgress) > CGFloat.ulpOfOne {
|
||||
self.expandProgress = expandProgress
|
||||
expandUpdated = true
|
||||
|
|
@ -482,7 +626,7 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||
}
|
||||
let transaction = self.enqueuedTransactions.removeFirst()
|
||||
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: transaction.deletions, insertItems: transaction.insertions, updateItems: transaction.updates, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: transaction.deletions, insertItems: transaction.insertions, updateItems: transaction.updates, scrollToItem: transaction.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
|
|
@ -640,6 +784,11 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
|
|||
let modalProgress = container.modalProgress
|
||||
strongSelf.modalProgressUpdated(modalProgress, transition)
|
||||
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
|
||||
for (otherIndex, otherContainer) in strongSelf.containers {
|
||||
if otherContainer !== container {
|
||||
otherContainer.syncExpandProgress(expandScrollProgress: container.expandScrollProgress, expandProgress: container.expandProgress, modalProgress: container.modalProgress, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, presentInGlobalOverlay: presentInGlobalOverlay,
|
||||
sendSticker: sendSticker)
|
||||
|
|
@ -647,11 +796,18 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
|
|||
self.containers[i] = container
|
||||
}
|
||||
|
||||
containerTransition.updateFrame(node: container, frame: CGRect(origin: CGPoint(x: CGFloat(indexOffset) * layout.size.width + self.relativeToSelectedStickerPackTransition + scaledOffset, y: containerVerticalOffset), size: layout.size), beginWithCurrentState: true)
|
||||
let containerFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * layout.size.width + self.relativeToSelectedStickerPackTransition + scaledOffset, y: containerVerticalOffset), size: layout.size)
|
||||
containerTransition.updateFrame(node: container, frame: containerFrame, beginWithCurrentState: true)
|
||||
containerTransition.updateSublayerTransformScaleAndOffset(node: container, scale: containerScale, offset: CGPoint(), beginWithCurrentState: true)
|
||||
if container.validLayout?.0 != layout {
|
||||
container.updateLayout(layout: layout, transition: containerTransition)
|
||||
}
|
||||
|
||||
if let selectedContainer = self.containers[self.selectedStickerPackIndex] {
|
||||
if selectedContainer !== container {
|
||||
container.syncExpandProgress(expandScrollProgress: selectedContainer.expandScrollProgress, expandProgress: selectedContainer.expandProgress, modalProgress: selectedContainer.modalProgress, transition: .immediate)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let container = self.containers[i] {
|
||||
container.removeFromSupernode()
|
||||
|
|
@ -703,9 +859,17 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
|
|||
let deltaIndex = velocity.x > 0 ? -1 : 1
|
||||
self.selectedStickerPackIndex = max(0, min(self.stickerPacks.count - 1, Int(self.selectedStickerPackIndex + deltaIndex)))
|
||||
}
|
||||
let deltaOffset = self.relativeToSelectedStickerPackTransition
|
||||
self.relativeToSelectedStickerPackTransition = 0.0
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.35, curve: .spring))
|
||||
let existingIndices = Array(self.containers.keys)
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
|
||||
self.containerLayoutUpdated(layout, transition: transition)
|
||||
for (key, container) in self.containers {
|
||||
if !existingIndices.contains(key) {
|
||||
transition.animatePositionAdditive(node: container, offset: CGPoint(x: -deltaOffset, y: 0.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
|
@ -717,12 +881,12 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
|
|||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
let minInset: CGFloat = (self.containers.map { (_, container) -> CGFloat in container.topContentInset }).max() ?? 0.0
|
||||
self.containerContainingNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
self.containerContainingNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.dimNode.alpha = 0.0
|
||||
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
|
||||
let minInset: CGFloat = (self.containers.map { (_, container) -> CGFloat in container.topContentInset }).max() ?? 0.0
|
||||
self.containerContainingNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
|
|
@ -848,6 +1012,8 @@ public enum StickerPackScreenPerformedAction {
|
|||
}
|
||||
|
||||
public func StickerPackScreen(context: AccountContext, mode: StickerPackPreviewControllerMode = .default, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [ItemCollectionItem], StickerPackScreenPerformedAction) -> Void)? = nil) -> ViewController {
|
||||
return StickerPackScreenImpl(context: context, stickerPacks: stickerPacks, selectedStickerPackIndex: stickerPacks.firstIndex(of: mainStickerPack) ?? 0, parentNavigationController: parentNavigationController, sendSticker: sendSticker)
|
||||
|
||||
let controller = StickerPackPreviewController(context: context, stickerPack: mainStickerPack, mode: mode, parentNavigationController: parentNavigationController, actionPerformed: actionPerformed)
|
||||
controller.sendSticker = sendSticker
|
||||
return controller
|
||||
|
|
|
|||
|
|
@ -392,6 +392,10 @@ public func chatMessageSticker(postbox: Postbox, file: TelegramMediaFile, small:
|
|||
let fullSizeData = value._1
|
||||
let fullSizeComplete = value._2
|
||||
return { arguments in
|
||||
if thumbnailData == nil && fullSizeData == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil)
|
||||
|
||||
let drawingRect = arguments.drawingRect
|
||||
|
|
@ -483,6 +487,10 @@ public func chatMessageAnimatedSticker(postbox: Postbox, file: TelegramMediaFile
|
|||
let fullSizeData = value._1
|
||||
let fullSizeComplete = value._2
|
||||
return { arguments in
|
||||
if thumbnailData == nil && fullSizeData == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true)
|
||||
|
||||
let drawingRect = arguments.drawingRect
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ framework(
|
|||
"//submodules/Svg:Svg",
|
||||
"//submodules/StatisticsUI:StatisticsUI",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
"//submodules/TooltipUI:TooltipUI",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ swift_library(
|
|||
"//submodules/AccountUtils:AccountUtils",
|
||||
"//submodules/Svg:Svg",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||
"//submodules/TooltipUI:TooltipUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import PhoneNumberFormat
|
|||
import SettingsUI
|
||||
import UrlWhitelist
|
||||
import TelegramIntents
|
||||
import TooltipUI
|
||||
|
||||
public enum ChatControllerPeekActions {
|
||||
case standard
|
||||
|
|
@ -1692,7 +1693,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
itemNode.animateQuizInvalidOptionSelected()
|
||||
|
||||
if let solution = resultPoll.results.solution {
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: solution), elevatedLayout: true, action: { _ in return false }), in: .window(.root))
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1938,11 +1947,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
return
|
||||
}
|
||||
strongSelf.presentPollCreation(isQuiz: isQuiz)
|
||||
}, displayPollSolution: { [weak self] text in
|
||||
}, displayPollSolution: { [weak self] text, sourceNode in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(text: text), elevatedLayout: true, action: { _ in return false }), in: .window(.root))
|
||||
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)
|
||||
}, requestMessageUpdate: { [weak self] id in
|
||||
if let strongSelf = self {
|
||||
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ public final class ChatControllerInteraction {
|
|||
let dismissReplyMarkupMessage: (Message) -> Void
|
||||
let openMessagePollResults: (MessageId, Data) -> Void
|
||||
let openPollCreation: (Bool?) -> Void
|
||||
let displayPollSolution: (String) -> Void
|
||||
let displayPollSolution: (String, 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) -> 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 (String, ASDisplayNode) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||
self.openMessage = openMessage
|
||||
self.openPeer = openPeer
|
||||
self.openPeerMention = openPeerMention
|
||||
|
|
@ -222,7 +222,7 @@ public final class ChatControllerInteraction {
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||
}
|
||||
private var replyInfoNode: ChatMessageReplyInfoNode?
|
||||
|
||||
private var contentNodes: [ChatMessageBubbleContentNode] = []
|
||||
private(set) var contentNodes: [ChatMessageBubbleContentNode] = []
|
||||
private var mosaicStatusNode: ChatMessageDateAndStatusNode?
|
||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||
|
||||
|
|
|
|||
|
|
@ -851,6 +851,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
private let statusNode: ChatMessageDateAndStatusNode
|
||||
private var optionNodes: [ChatMessagePollOptionNode] = []
|
||||
|
||||
var solutionTipSourceNode: ASDisplayNode? {
|
||||
return self.solutionButtonNode
|
||||
}
|
||||
|
||||
private var poll: TelegramMediaPoll?
|
||||
|
||||
required init() {
|
||||
|
|
@ -1452,16 +1456,16 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|||
timerTransition.updateTransformScale(node: timerNode, scale: 0.1)
|
||||
}
|
||||
|
||||
if (strongSelf.timerNode == nil || !displayDeadline), let poll = poll, case .anonymous = poll.publicity, 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, !solution.isEmpty, (isClosed || hasSelected) {
|
||||
let solutionButtonNode: SolutionButtonNode
|
||||
if let current = strongSelf.solutionButtonNode {
|
||||
solutionButtonNode = current
|
||||
} else {
|
||||
solutionButtonNode = SolutionButtonNode(pressed: {
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
guard let strongSelf = self, let solutionButtonNode = strongSelf.solutionButtonNode, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
item.controllerInteraction.displayPollSolution(solution)
|
||||
item.controllerInteraction.displayPollSolution(solution, solutionButtonNode)
|
||||
})
|
||||
strongSelf.solutionButtonNode = solutionButtonNode
|
||||
strongSelf.addSubnode(solutionButtonNode)
|
||||
|
|
|
|||
|
|
@ -423,7 +423,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|
|||
openPeer(peerId, .chat(textInputState: nil, subject: .message(messageId)))
|
||||
case let .stickerPack(name):
|
||||
dismissInput()
|
||||
if false {
|
||||
if true {
|
||||
var mainStickerPack: StickerPackReference?
|
||||
var stickerPacks: [StickerPackReference] = []
|
||||
if let message = contentContext as? Message {
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))
|
||||
|
|
|
|||
|
|
@ -1527,7 +1527,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
|
|
|||
|
|
@ -427,7 +427,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
|
|
|||
|
|
@ -219,11 +219,11 @@ private func pollResultsControllerEntries(presentationData: PresentationData, po
|
|||
|
||||
entries.append(.text(poll.text))
|
||||
|
||||
if let solution = poll.results.solution, !solution.isEmpty {
|
||||
/*if let solution = poll.results.solution, !solution.isEmpty {
|
||||
//TODO:localize
|
||||
entries.append(.solutionHeader("EXPLANATION"))
|
||||
entries.append(.solutionText(solution))
|
||||
}
|
||||
}*/
|
||||
|
||||
var optionVoterCount: [Int: Int32] = [:]
|
||||
let totalVoterCount = poll.results.totalVoters ?? 0
|
||||
|
|
|
|||
|
|
@ -1135,7 +1135,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, requestMessageUpdate: { _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,
|
||||
|
|
|
|||
|
|
@ -7,17 +7,20 @@ import AnimatedStickerNode
|
|||
import AppBundle
|
||||
|
||||
private final class TooltipScreenNode: ViewControllerTracingNode {
|
||||
private let location: CGPoint
|
||||
private let icon: TooltipScreen.Icon
|
||||
private let location: CGRect
|
||||
private let shouldDismissOnTouch: (CGPoint) -> Bool
|
||||
private let requestDismiss: () -> Void
|
||||
|
||||
private let containerNode: ASDisplayNode
|
||||
private let backgroundNode: ASImageNode
|
||||
private let arrowNode: ASImageNode
|
||||
private let arrowContainer: ASDisplayNode
|
||||
private let animatedStickerNode: AnimatedStickerNode
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
init(text: String, location: CGPoint, shouldDismissOnTouch: @escaping (CGPoint) -> Bool, requestDismiss: @escaping () -> Void) {
|
||||
init(text: String, icon: TooltipScreen.Icon, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> Bool, requestDismiss: @escaping () -> Void) {
|
||||
self.icon = icon
|
||||
self.location = location
|
||||
self.shouldDismissOnTouch = shouldDismissOnTouch
|
||||
self.requestDismiss = requestDismiss
|
||||
|
|
@ -39,20 +42,31 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
|||
context.fillPath()
|
||||
})
|
||||
|
||||
self.arrowContainer = ASDisplayNode()
|
||||
|
||||
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.animatedStickerNode = AnimatedStickerNode()
|
||||
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)
|
||||
self.animatedStickerNode.automaticallyLoadFirstFrame = true
|
||||
switch icon {
|
||||
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)
|
||||
self.animatedStickerNode.automaticallyLoadFirstFrame = true
|
||||
}
|
||||
case .info:
|
||||
if let path = getAppBundle().path(forResource: "anim_infotip", ofType: "json") {
|
||||
self.animatedStickerNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: Int(70 * UIScreenScale), height: Int(70 * UIScreenScale), playbackMode: .once, mode: .direct)
|
||||
self.animatedStickerNode.automaticallyLoadFirstFrame = true
|
||||
}
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundNode.addSubnode(self.arrowNode)
|
||||
self.arrowContainer.addSubnode(self.arrowNode)
|
||||
self.backgroundNode.addSubnode(self.arrowContainer)
|
||||
self.containerNode.addSubnode(self.backgroundNode)
|
||||
self.containerNode.addSubnode(self.textNode)
|
||||
self.containerNode.addSubnode(self.animatedStickerNode)
|
||||
|
|
@ -64,19 +78,48 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
|||
let bottomInset: CGFloat = 10.0
|
||||
let contentInset: CGFloat = 9.0
|
||||
let contentVerticalInset: CGFloat = 11.0
|
||||
let animationSize = CGSize(width: 32.0, height: 32.0)
|
||||
let animationInset: CGFloat = (70 - animationSize.width) / 2.0
|
||||
let animationSize: CGSize
|
||||
let animationInset: CGFloat
|
||||
|
||||
switch self.icon {
|
||||
case .chatListPress:
|
||||
animationSize = CGSize(width: 32.0, height: 32.0)
|
||||
animationInset = (70.0 - animationSize.width) / 2.0
|
||||
case .info:
|
||||
animationSize = CGSize(width: 32.0, height: 32.0)
|
||||
animationInset = 0.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 backgroundHeight = max(animationSize.height, textSize.height) + contentVerticalInset * 2.0
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: self.location.y - bottomInset - backgroundHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: backgroundHeight))
|
||||
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 invertArrow = false
|
||||
if backgroundFrame.minY < layout.insets(options: .statusBar).top {
|
||||
backgroundFrame.origin.y = self.location.maxY + bottomInset
|
||||
invertArrow = true
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.containerNode, frame: backgroundFrame)
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
if let image = self.arrowNode.image {
|
||||
let arrowSize = image.size
|
||||
let arrowCenterX = self.location.x
|
||||
transition.updateFrame(node: self.arrowNode, frame: CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: backgroundFrame.height), size: arrowSize))
|
||||
let arrowCenterX = self.location.midX
|
||||
|
||||
let arrowFrame: CGRect
|
||||
|
||||
if invertArrow {
|
||||
arrowFrame = CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: -arrowSize.height), size: arrowSize)
|
||||
} else {
|
||||
arrowFrame = CGRect(origin: CGPoint(x: floor(arrowCenterX - arrowSize.width / 2.0), y: backgroundFrame.height), size: arrowSize)
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.arrowContainer, frame: arrowFrame)
|
||||
|
||||
ContainedViewLayoutTransition.immediate.updateTransformScale(node: self.arrowContainer, scale: CGPoint(x: 1.0, y: invertArrow ? -1.0 : 1.0))
|
||||
|
||||
self.arrowNode.frame = CGRect(origin: CGPoint(), size: arrowFrame.size)
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: floor((backgroundHeight - textSize.height) / 2.0)), size: textSize))
|
||||
|
|
@ -103,10 +146,18 @@ 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.arrowNode.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowNode.frame.maxY - self.containerNode.bounds.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.6, additive: true)
|
||||
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)
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in
|
||||
let animationDelay: Double
|
||||
switch self.icon {
|
||||
case .chatListPress:
|
||||
animationDelay = 0.6
|
||||
case .info:
|
||||
animationDelay = 0.2
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + animationDelay, execute: { [weak self] in
|
||||
self?.animatedStickerNode.visibility = true
|
||||
})
|
||||
}
|
||||
|
|
@ -116,13 +167,19 @@ 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.arrowNode.frame.midX - self.containerNode.bounds.width / 2.0, y: self.arrowNode.frame.maxY - self.containerNode.bounds.height / 2.0), duration: 0.2, removeOnCompletion: false, additive: true)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
public final class TooltipScreen: ViewController {
|
||||
public enum Icon {
|
||||
case info
|
||||
case chatListPress
|
||||
}
|
||||
|
||||
private let text: String
|
||||
private let location: CGPoint
|
||||
private let icon: TooltipScreen.Icon
|
||||
private let location: CGRect
|
||||
private let shouldDismissOnTouch: (CGPoint) -> Bool
|
||||
|
||||
private var controllerNode: TooltipScreenNode {
|
||||
|
|
@ -132,8 +189,9 @@ public final class TooltipScreen: ViewController {
|
|||
private var validLayout: ContainerViewLayout?
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
public init(text: String, location: CGPoint, shouldDismissOnTouch: @escaping (CGPoint) -> Bool) {
|
||||
public init(text: String, icon: TooltipScreen.Icon, location: CGRect, shouldDismissOnTouch: @escaping (CGPoint) -> Bool) {
|
||||
self.text = text
|
||||
self.icon = icon
|
||||
self.location = location
|
||||
self.shouldDismissOnTouch = shouldDismissOnTouch
|
||||
|
||||
|
|
@ -151,13 +209,13 @@ public final class TooltipScreen: ViewController {
|
|||
|
||||
self.controllerNode.animateIn()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0, execute: { [weak self] in
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 8.0, execute: { [weak self] in
|
||||
self?.dismiss()
|
||||
})
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = TooltipScreenNode(text: self.text, location: self.location, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in
|
||||
self.displayNode = TooltipScreenNode(text: self.text, icon: self.icon, location: self.location, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue