Sticker pack preview updates

This commit is contained in:
Ali 2020-04-07 17:55:39 +04:00
parent 728d68dc6d
commit 78e12ad45e
22 changed files with 459 additions and 129 deletions

View file

@ -864,10 +864,10 @@ ios_application(
":AppStringResources",
],
extensions = [
":ShareExtension",
":NotificationServiceExtension",
#":ShareExtension",
#":NotificationServiceExtension",
],
watch_application = ":TelegramWatchApp",
#watch_application = ":TelegramWatchApp",
deps = [
":Main",
":Lib",

View file

@ -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
}

View file

@ -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) {

View file

@ -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()
}
})

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -204,6 +204,7 @@ framework(
"//submodules/Svg:Svg",
"//submodules/StatisticsUI:StatisticsUI",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/TooltipUI:TooltipUI",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View file

@ -202,6 +202,7 @@ swift_library(
"//submodules/AccountUtils:AccountUtils",
"//submodules/Svg:Svg",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/TooltipUI:TooltipUI",
],
visibility = [
"//visibility:public",

View file

@ -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)

View file

@ -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,

View file

@ -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?

View file

@ -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)

View file

@ -423,7 +423,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, displayPollSolution: { _ in
}, displayPollSolution: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View file

@ -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 {

View file

@ -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))

View file

@ -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,

View file

@ -427,7 +427,7 @@ public class PeerMediaCollectionController: TelegramBaseController {
}, dismissReplyMarkupMessage: { _ in
}, openMessagePollResults: { _, _ in
}, openPollCreation: { _ in
}, displayPollSolution: { _ in
}, displayPollSolution: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View file

@ -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

View file

@ -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,

View file

@ -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
}