mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Add sticker packs reordering
This commit is contained in:
parent
1e0e0de7c8
commit
856638a82f
@ -869,6 +869,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
self.timer.swap(nil)?.invalidate()
|
||||
}
|
||||
|
||||
private weak var nodeToCopyFrameFrom: AnimatedStickerNode?
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
@ -879,8 +880,22 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
//self.renderer = MetalAnimationRenderer()
|
||||
#endif
|
||||
self.renderer?.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
||||
if let contents = self.nodeToCopyFrameFrom?.renderer?.contents {
|
||||
self.renderer?.contents = contents
|
||||
}
|
||||
self.nodeToCopyFrameFrom = nil
|
||||
self.addSubnode(self.renderer!)
|
||||
}
|
||||
|
||||
public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) {
|
||||
if let renderer = self.renderer {
|
||||
if let contents = otherNode?.renderer?.contents {
|
||||
renderer.contents = contents
|
||||
}
|
||||
} else {
|
||||
self.nodeToCopyFrameFrom = otherNode
|
||||
}
|
||||
}
|
||||
|
||||
public func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) {
|
||||
if width < 2 || height < 2 {
|
||||
@ -973,7 +988,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
private var isSetUpForPlayback = false
|
||||
|
||||
|
||||
public func play(firstFrame: Bool = false, fromIndex: Int? = nil) {
|
||||
switch self.playbackMode {
|
||||
case .once:
|
||||
|
@ -300,6 +300,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
private var currentGeneralScrollDirection: GeneralScrollDirection?
|
||||
public final var generalScrollDirectionUpdated: (GeneralScrollDirection) -> Void = { _ in }
|
||||
|
||||
public private(set) var isReordering = false
|
||||
public final var willBeginReorder: (CGPoint) -> Void = { _ in }
|
||||
public final var reorderBegan: () -> Void = { }
|
||||
public final var reorderItem: (Int, Int, Any?) -> Signal<Bool, NoError> = { _, _, _ in return .single(false) }
|
||||
public final var reorderCompleted: (Any?) -> Void = { _ in }
|
||||
|
||||
@ -322,8 +325,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
private var reorderNode: ListViewReorderingItemNode?
|
||||
private var reorderFeedback: HapticFeedback?
|
||||
private var reorderFeedbackDisposable: MetaDisposable?
|
||||
private var isReorderingItems: Bool = false
|
||||
private var reorderInProgress: Bool = false
|
||||
private var reorderingItemsCompleted: (() -> Void)?
|
||||
public var reorderedItemHasShadow = true
|
||||
|
||||
private let waitingForNodesDisposable = MetaDisposable()
|
||||
|
||||
@ -394,15 +398,19 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
let itemNodeFrame = itemNode.frame
|
||||
let itemNodeBounds = itemNode.bounds
|
||||
if itemNode.isReorderable(at: point.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY)) {
|
||||
strongSelf.beginReordering(itemNode: itemNode)
|
||||
return true
|
||||
let requiresLongPress = !strongSelf.reorderedItemHasShadow
|
||||
return (true, requiresLongPress, itemNode)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return (false, false, nil)
|
||||
}, willBegin: { [weak self] point in
|
||||
self?.willBeginReorder(point)
|
||||
}, began: { [weak self] itemNode in
|
||||
self?.beginReordering(itemNode: itemNode)
|
||||
}, ended: { [weak self] in
|
||||
self?.endReordering()
|
||||
}, moved: { [weak self] offset in
|
||||
@ -469,10 +477,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
}
|
||||
|
||||
private func beginReordering(itemNode: ListViewItemNode) {
|
||||
self.isReordering = true
|
||||
self.reorderBegan()
|
||||
|
||||
if let reorderNode = self.reorderNode {
|
||||
reorderNode.removeFromSupernode()
|
||||
}
|
||||
let reorderNode = ListViewReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin)
|
||||
let reorderNode = ListViewReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin, hasShadow: self.reorderedItemHasShadow)
|
||||
self.reorderNode = reorderNode
|
||||
if let verticalScrollIndicator = self.verticalScrollIndicator {
|
||||
self.insertSubnode(reorderNode, belowSubnode: verticalScrollIndicator)
|
||||
@ -509,8 +520,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
}
|
||||
}
|
||||
strongSelf.reorderCompleted(strongSelf.opaqueTransactionState)
|
||||
strongSelf.isReordering = false
|
||||
}
|
||||
if self.isReorderingItems {
|
||||
if self.reorderInProgress {
|
||||
self.reorderingItemsCompleted = f
|
||||
} else {
|
||||
f()
|
||||
@ -587,14 +599,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
if self.reorderFeedbackDisposable == nil {
|
||||
self.reorderFeedbackDisposable = MetaDisposable()
|
||||
}
|
||||
self.isReorderingItems = true
|
||||
self.reorderInProgress = true
|
||||
self.reorderFeedbackDisposable?.set((self.reorderItem(reorderItemIndex, toIndex, self.opaqueTransactionState)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.isReorderingItems = false
|
||||
strongSelf.reorderInProgress = false
|
||||
if let reorderingItemsCompleted = strongSelf.reorderingItemsCompleted {
|
||||
strongSelf.reorderingItemsCompleted = nil
|
||||
reorderingItemsCompleted()
|
||||
|
@ -1,28 +1,101 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
|
||||
final class ListViewReorderingGestureRecognizer: UIGestureRecognizer {
|
||||
private let shouldBegin: (CGPoint) -> Bool
|
||||
public final class ListViewReorderingGestureRecognizer: UIGestureRecognizer {
|
||||
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: ListViewItemNode?)
|
||||
private let willBegin: (CGPoint) -> Void
|
||||
private let began: (ListViewItemNode) -> Void
|
||||
private let ended: () -> Void
|
||||
private let moved: (CGFloat) -> Void
|
||||
|
||||
private var initialLocation: CGPoint?
|
||||
private var longTapTimer: SwiftSignalKit.Timer?
|
||||
private var longPressTimer: SwiftSignalKit.Timer?
|
||||
|
||||
init(shouldBegin: @escaping (CGPoint) -> Bool, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) {
|
||||
private var itemNode: ListViewItemNode?
|
||||
|
||||
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: ListViewItemNode?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (ListViewItemNode) -> Void, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) {
|
||||
self.shouldBegin = shouldBegin
|
||||
self.willBegin = willBegin
|
||||
self.began = began
|
||||
self.ended = ended
|
||||
self.moved = moved
|
||||
|
||||
super.init(target: nil, action: nil)
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
deinit {
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longPressTimer?.invalidate()
|
||||
}
|
||||
|
||||
private func startLongTapTimer() {
|
||||
self.longTapTimer?.invalidate()
|
||||
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
|
||||
self?.longTapTimerFired()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longTapTimer = longTapTimer
|
||||
longTapTimer.start()
|
||||
}
|
||||
|
||||
private func stopLongTapTimer() {
|
||||
self.itemNode = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
}
|
||||
|
||||
private func startLongPressTimer() {
|
||||
self.longPressTimer?.invalidate()
|
||||
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.8, repeat: false, completion: { [weak self] in
|
||||
self?.longPressTimerFired()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longPressTimer = longPressTimer
|
||||
longPressTimer.start()
|
||||
}
|
||||
|
||||
private func stopLongPressTimer() {
|
||||
self.itemNode = nil
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
}
|
||||
|
||||
override public func reset() {
|
||||
super.reset()
|
||||
|
||||
self.itemNode = nil
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
private func longTapTimerFired() {
|
||||
guard let location = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
|
||||
self.willBegin(location)
|
||||
}
|
||||
|
||||
private func longPressTimerFired() {
|
||||
guard let _ = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.state = .began
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
if let itemNode = self.itemNode {
|
||||
self.began(itemNode)
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if self.numberOfTouches > 1 {
|
||||
@ -32,40 +105,76 @@ final class ListViewReorderingGestureRecognizer: UIGestureRecognizer {
|
||||
}
|
||||
|
||||
if self.state == .possible {
|
||||
if let location = touches.first?.location(in: self.view), self.shouldBegin(location) {
|
||||
self.initialLocation = location
|
||||
self.state = .began
|
||||
if let location = touches.first?.location(in: self.view) {
|
||||
let (allowed, requiresLongPress, itemNode) = self.shouldBegin(location)
|
||||
if allowed {
|
||||
self.itemNode = itemNode
|
||||
self.initialLocation = location
|
||||
if requiresLongPress {
|
||||
self.startLongTapTimer()
|
||||
self.startLongPressTimer()
|
||||
} else {
|
||||
self.state = .began
|
||||
}
|
||||
} else {
|
||||
self.state = .failed
|
||||
}
|
||||
} else {
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.stopLongPressTimer()
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.stopLongPressTimer()
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
||||
self.state = .changed
|
||||
let offset = location.y - initialLocation.y
|
||||
self.moved(offset)
|
||||
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
|
||||
let touchLocation = touch.location(in: self.view)
|
||||
let dX = touchLocation.x - initialTapLocation.x
|
||||
let dY = touchLocation.y - initialTapLocation.y
|
||||
|
||||
if dX * dX + dY * dY > 3.0 * 3.0 {
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,14 +25,16 @@ private final class CopyView: UIView {
|
||||
let topShadow: UIImageView
|
||||
let bottomShadow: UIImageView
|
||||
|
||||
override init(frame: CGRect) {
|
||||
init(frame: CGRect, hasShadow: Bool) {
|
||||
self.topShadow = UIImageView()
|
||||
self.bottomShadow = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.topShadow.image = generateShadowImage(mirror: true)
|
||||
self.bottomShadow.image = generateShadowImage(mirror: false)
|
||||
if hasShadow {
|
||||
self.topShadow.image = generateShadowImage(mirror: true)
|
||||
self.bottomShadow.image = generateShadowImage(mirror: false)
|
||||
}
|
||||
|
||||
self.addSubview(self.topShadow)
|
||||
self.addSubview(self.bottomShadow)
|
||||
@ -51,9 +53,9 @@ final class ListViewReorderingItemNode: ASDisplayNode {
|
||||
private let copyView: CopyView
|
||||
private let initialLocation: CGPoint
|
||||
|
||||
init(itemNode: ListViewItemNode, initialLocation: CGPoint) {
|
||||
init(itemNode: ListViewItemNode, initialLocation: CGPoint, hasShadow: Bool) {
|
||||
self.itemNode = itemNode
|
||||
self.copyView = CopyView(frame: CGRect())
|
||||
self.copyView = CopyView(frame: CGRect(), hasShadow: hasShadow)
|
||||
let snapshotView = itemNode.snapshotForReordering()
|
||||
self.initialLocation = initialLocation
|
||||
|
||||
@ -68,8 +70,6 @@ final class ListViewReorderingItemNode: ASDisplayNode {
|
||||
self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: itemNode.bounds.size)
|
||||
|
||||
self.copyView.topShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: -30.0), size: CGSize(width: copyView.bounds.size.width, height: 45.0))
|
||||
|
||||
self.copyView.bottomShadow.image = generateShadowImage(mirror: false)
|
||||
self.copyView.bottomShadow.frame = CGRect(origin: CGPoint(x: 0.0, y: self.copyView.bounds.size.height - 15.0), size: CGSize(width: self.copyView.bounds.size.width, height: 45.0))
|
||||
|
||||
self.copyView.topShadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
@ -6,6 +6,8 @@ import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
|
||||
enum ChatMediaInputMetaSectionItemType: Equatable {
|
||||
case savedStickers
|
||||
@ -13,10 +15,11 @@ enum ChatMediaInputMetaSectionItemType: Equatable {
|
||||
case stickersMode
|
||||
case savedGifs
|
||||
case trendingGifs
|
||||
case gifEmoji(String)
|
||||
case gifEmoji(String, TelegramMediaFile?)
|
||||
}
|
||||
|
||||
final class ChatMediaInputMetaSectionItem: ListViewItem {
|
||||
let account: Account
|
||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
let type: ChatMediaInputMetaSectionItemType
|
||||
let theme: PresentationTheme
|
||||
@ -27,7 +30,8 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
|
||||
return true
|
||||
}
|
||||
|
||||
init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) {
|
||||
init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.type = type
|
||||
self.selectedItem = selected
|
||||
@ -41,7 +45,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
|
||||
Queue.mainQueue().async {
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
node.setItem(item: self)
|
||||
node.updateTheme(theme: self.theme, expanded: self.expanded)
|
||||
node.updateTheme(account: self.account, theme: self.theme, expanded: self.expanded)
|
||||
node.updateIsHighlighted()
|
||||
node.updateAppearanceTransition(transition: .immediate)
|
||||
|
||||
@ -61,7 +65,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
|
||||
Queue.mainQueue().async {
|
||||
completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: node().insets), { _ in
|
||||
(node() as? ChatMediaInputMetaSectionItemNode)?.setItem(item: self)
|
||||
(node() as? ChatMediaInputMetaSectionItemNode)?.updateTheme(theme: self.theme, expanded: self.expanded)
|
||||
(node() as? ChatMediaInputMetaSectionItemNode)?.updateTheme(account: self.account, theme: self.theme, expanded: self.expanded)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -86,6 +90,8 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
private let highlightNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
private var animatedStickerNode: AnimatedStickerNode?
|
||||
|
||||
private var currentExpanded = false
|
||||
|
||||
var item: ChatMediaInputMetaSectionItem?
|
||||
@ -94,6 +100,23 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
|
||||
var theme: PresentationTheme?
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
self.visibilityStatus = self.visibility != .none
|
||||
}
|
||||
}
|
||||
|
||||
private var visibilityStatus: Bool = false {
|
||||
didSet {
|
||||
if self.visibilityStatus != oldValue {
|
||||
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
|
||||
self.animatedStickerNode?.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
|
||||
init() {
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
@ -130,6 +153,10 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
self.scalingNode.addSubnode(self.textNodeContainer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
}
|
||||
@ -146,7 +173,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme, expanded: Bool) {
|
||||
func updateTheme(account: Account, theme: PresentationTheme, expanded: Bool) {
|
||||
let imageSize = CGSize(width: 26.0 * 1.6, height: 26.0 * 1.6)
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((expandedBoundingSize.width - imageSize.width) / 2.0), y: floor((expandedBoundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize)
|
||||
|
||||
@ -174,7 +201,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
case .trendingGifs:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingGifsIcon(theme)
|
||||
title = "Trending"
|
||||
case let .gifEmoji(emoji):
|
||||
case let .gifEmoji(emoji, file):
|
||||
var emoji = emoji
|
||||
switch emoji {
|
||||
case "😡":
|
||||
@ -200,16 +227,38 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
default:
|
||||
break
|
||||
}
|
||||
if emoji == "🥳" {
|
||||
if #available(iOSApplicationExtension 12.1, iOS 12.1, *) {
|
||||
} else {
|
||||
emoji = "🎉"
|
||||
}
|
||||
}
|
||||
// if emoji == "🥳" {
|
||||
// if #available(iOSApplicationExtension 12.1, iOS 12.1, *) {
|
||||
// } else {
|
||||
// emoji = "🎉"
|
||||
// }
|
||||
// }
|
||||
self.imageNode.image = nil
|
||||
self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(43.0), textColor: .black)
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: floor((self.textNodeContainer.bounds.width - textSize.width) / 2.0), y: floor((self.textNodeContainer.bounds.height - textSize.height) / 2.0)), size: textSize)
|
||||
|
||||
if let file = file {
|
||||
|
||||
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
|
||||
let animatedStickerNode: AnimatedStickerNode
|
||||
if let current = self.animatedStickerNode {
|
||||
animatedStickerNode = current
|
||||
} else {
|
||||
animatedStickerNode = AnimatedStickerNode()
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
// if let placeholderNode = self.placeholderNode {
|
||||
// self.scalingNode.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode)
|
||||
// } else {
|
||||
self.scalingNode.addSubnode(animatedStickerNode)
|
||||
// }
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: file.resource), width: 128, height: 128, mode: .cached)
|
||||
}
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
|
||||
self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start())
|
||||
} else {
|
||||
self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(43.0), textColor: .black)
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: floor((self.textNodeContainer.bounds.width - textSize.width) / 2.0), y: floor((self.textNodeContainer.bounds.height - textSize.height) / 2.0)), size: textSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor)
|
||||
@ -224,7 +273,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale)
|
||||
expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0)))
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height))
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width, height: expandedBoundingSize.height))
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 6.0), size: titleSize)
|
||||
let displayTitleFrame = expanded ? titleFrame : CGRect(origin: CGPoint(x: titleFrame.minX, y: self.imageNode.position.y - titleFrame.size.height), size: titleFrame.size)
|
||||
@ -236,6 +285,11 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
|
||||
self.currentExpanded = expanded
|
||||
|
||||
if let animatedStickerNode = self.animatedStickerNode {
|
||||
animatedStickerNode.frame = self.imageNode.frame
|
||||
animatedStickerNode.updateLayout(size: self.imageNode.frame.size)
|
||||
}
|
||||
|
||||
expandTransition.updateFrame(node: self.highlightNode, frame: expanded ? titleFrame.insetBy(dx: -7.0, dy: -2.0) : CGRect(origin: CGPoint(x: self.imageNode.position.x - highlightSize.width / 2.0, y: self.imageNode.position.y - highlightSize.height / 2.0), size: highlightSize))
|
||||
}
|
||||
|
||||
@ -256,7 +310,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
if case .trending = inputNodeInteraction.highlightedGifMode {
|
||||
isHighlighted = true
|
||||
}
|
||||
case let .gifEmoji(emoji):
|
||||
case let .gifEmoji(emoji, _):
|
||||
if case .emojiSearch(emoji) = inputNodeInteraction.highlightedGifMode {
|
||||
isHighlighted = true
|
||||
}
|
||||
|
@ -30,11 +30,20 @@ enum CanInstallPeerSpecificPack {
|
||||
case available(peer: Peer, dismissed: Bool)
|
||||
}
|
||||
|
||||
final class ChatMediaInputPanelOpaqueState {
|
||||
let entries: [ChatMediaInputPanelEntry]
|
||||
|
||||
init(entries: [ChatMediaInputPanelEntry]) {
|
||||
self.entries = entries
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatMediaInputPanelTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
let updateOpaqueState: ChatMediaInputPanelOpaqueState?
|
||||
}
|
||||
|
||||
struct ChatMediaInputGridTransition {
|
||||
@ -55,7 +64,7 @@ func preparedChatMediaInputPanelEntryTransition(context: AccountContext, from fr
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, inputNodeInteraction: inputNodeInteraction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, inputNodeInteraction: inputNodeInteraction), directionHint: nil) }
|
||||
|
||||
return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates, scrollToItem: scrollToItem)
|
||||
return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates, scrollToItem: scrollToItem, updateOpaqueState: ChatMediaInputPanelOpaqueState(entries: toEntries))
|
||||
}
|
||||
|
||||
func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemCollectionsView, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingInteraction: TrendingPaneInteraction) -> ChatMediaInputGridTransition {
|
||||
@ -153,7 +162,7 @@ func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemColle
|
||||
return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, updateOpaqueState: opaqueState, animated: animated)
|
||||
}
|
||||
|
||||
func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, theme: PresentationTheme, hasGifs: Bool = true, hasSettings: Bool = true, expanded: Bool = false) -> [ChatMediaInputPanelEntry] {
|
||||
func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, temporaryPackOrder: [ItemCollectionId]? = nil, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, theme: PresentationTheme, hasGifs: Bool = true, hasSettings: Bool = true, expanded: Bool = false) -> [ChatMediaInputPanelEntry] {
|
||||
var entries: [ChatMediaInputPanelEntry] = []
|
||||
if hasGifs {
|
||||
entries.append(.recentGifs(theme, expanded))
|
||||
@ -189,13 +198,36 @@ func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: Ordere
|
||||
entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded))
|
||||
}
|
||||
var index = 0
|
||||
for (_, info, item) in view.collectionInfos {
|
||||
if let info = info as? StickerPackCollectionInfo, item != nil {
|
||||
entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem, theme: theme, expanded: expanded))
|
||||
index += 1
|
||||
|
||||
var sortedPacks: [(ItemCollectionId, StickerPackCollectionInfo, StickerPackItem?)] = []
|
||||
for (id, info, item) in view.collectionInfos {
|
||||
if let info = info as? StickerPackCollectionInfo, let item = item as? StickerPackItem {
|
||||
sortedPacks.append((id, info, item))
|
||||
}
|
||||
}
|
||||
|
||||
if let temporaryPackOrder = temporaryPackOrder {
|
||||
var packDict: [ItemCollectionId: Int] = [:]
|
||||
for i in 0 ..< sortedPacks.count {
|
||||
packDict[sortedPacks[i].0] = i
|
||||
}
|
||||
var tempSortedPacks: [(ItemCollectionId, StickerPackCollectionInfo, StickerPackItem?)] = []
|
||||
var processedPacks = Set<ItemCollectionId>()
|
||||
for id in temporaryPackOrder {
|
||||
if let index = packDict[id] {
|
||||
tempSortedPacks.append(sortedPacks[index])
|
||||
processedPacks.insert(id)
|
||||
}
|
||||
}
|
||||
let restPacks = sortedPacks.filter { !processedPacks.contains($0.0) }
|
||||
sortedPacks = restPacks + tempSortedPacks
|
||||
}
|
||||
|
||||
for (_, info, topItem) in sortedPacks {
|
||||
entries.append(.stickerPack(index: index, info: info, topItem: topItem, theme: theme, expanded: expanded))
|
||||
index += 1
|
||||
}
|
||||
|
||||
if peerSpecificPack == nil, case let .available(peer, true) = canInstallPeerSpecificPack {
|
||||
entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded))
|
||||
}
|
||||
@ -206,14 +238,14 @@ func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: Ordere
|
||||
return entries
|
||||
}
|
||||
|
||||
func chatMediaInputPanelGifModeEntries(theme: PresentationTheme, reactions: [String], expanded: Bool) -> [ChatMediaInputPanelEntry] {
|
||||
func chatMediaInputPanelGifModeEntries(theme: PresentationTheme, reactions: [String], animatedEmojiStickers: [String: [StickerPackItem]], expanded: Bool) -> [ChatMediaInputPanelEntry] {
|
||||
var entries: [ChatMediaInputPanelEntry] = []
|
||||
entries.append(.stickersMode(theme, expanded))
|
||||
entries.append(.savedGifs(theme, expanded))
|
||||
entries.append(.trendingGifs(theme, expanded))
|
||||
|
||||
for reaction in reactions {
|
||||
entries.append(.gifEmotion(entries.count, theme, reaction, expanded))
|
||||
entries.append(.gifEmotion(entries.count, theme, reaction, animatedEmojiStickers[reaction]?.first?.file, expanded))
|
||||
}
|
||||
|
||||
return entries
|
||||
@ -248,7 +280,7 @@ func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: Ordered
|
||||
}
|
||||
|
||||
var trendingIsDismissed = false
|
||||
if let dismissedTrendingStickerPacks = dismissedTrendingStickerPacks, trendingPacks.map({ $0.info.id.id }) == dismissedTrendingStickerPacks {
|
||||
if let dismissedTrendingStickerPacks = dismissedTrendingStickerPacks, Set(trendingPacks.map({ $0.info.id.id })) == Set(dismissedTrendingStickerPacks) {
|
||||
trendingIsDismissed = true
|
||||
}
|
||||
if !trendingIsDismissed {
|
||||
@ -470,6 +502,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
}
|
||||
private var panelFocusTimer: SwiftSignalKit.Timer?
|
||||
private var lastReorderItemIndex: Int?
|
||||
|
||||
var requestDisableStickerAnimations: ((Bool) -> Void)?
|
||||
|
||||
@ -514,6 +547,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
|
||||
self.listView = ListView()
|
||||
self.listView.useSingleDimensionTouchPoint = true
|
||||
self.listView.reorderedItemHasShadow = false
|
||||
self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0)
|
||||
self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = false
|
||||
self.listView.accessibilityPageScrolledString = { row, count in
|
||||
@ -551,6 +585,147 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
|
||||
super.init()
|
||||
|
||||
let temporaryPackOrder = Promise<[ItemCollectionId]?>(nil)
|
||||
|
||||
self.listView.willBeginReorder = { [weak self] point in
|
||||
self?.listView.beganInteractiveDragging(point)
|
||||
}
|
||||
|
||||
self.listView.reorderBegan = { [weak self] in
|
||||
self?.stopCollapseTimer()
|
||||
}
|
||||
|
||||
self.listView.reorderItem = { [weak self] fromIndex, toIndex, opaqueState in
|
||||
guard let entries = (opaqueState as? ChatMediaInputPanelOpaqueState)?.entries else {
|
||||
return .single(false)
|
||||
}
|
||||
self?.lastReorderItemIndex = toIndex
|
||||
|
||||
let fromEntry = entries[fromIndex]
|
||||
guard case let .stickerPack(_, fromPackInfo, _, _, _) = fromEntry else {
|
||||
return .single(false)
|
||||
}
|
||||
var referenceId: ItemCollectionId?
|
||||
var beforeAll = false
|
||||
var afterAll = false
|
||||
if toIndex < entries.count {
|
||||
switch entries[toIndex] {
|
||||
case let .stickerPack(_, toPackInfo, _, _, _):
|
||||
referenceId = toPackInfo.id
|
||||
default:
|
||||
if entries[toIndex] < fromEntry {
|
||||
beforeAll = true
|
||||
} else {
|
||||
afterAll = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
afterAll = true
|
||||
}
|
||||
|
||||
var currentIds: [ItemCollectionId] = []
|
||||
for entry in entries {
|
||||
switch entry {
|
||||
case let .stickerPack(_, info, _, _, _):
|
||||
currentIds.append(info.id)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var previousIndex: Int?
|
||||
for i in 0 ..< currentIds.count {
|
||||
if currentIds[i] == fromPackInfo.id {
|
||||
previousIndex = i
|
||||
currentIds.remove(at: i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var didReorder = false
|
||||
|
||||
if let referenceId = referenceId {
|
||||
var inserted = false
|
||||
for i in 0 ..< currentIds.count {
|
||||
if currentIds[i] == referenceId {
|
||||
if fromIndex < toIndex {
|
||||
didReorder = previousIndex != i + 1
|
||||
currentIds.insert(fromPackInfo.id, at: i + 1)
|
||||
} else {
|
||||
didReorder = previousIndex != i
|
||||
currentIds.insert(fromPackInfo.id, at: i)
|
||||
}
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inserted {
|
||||
didReorder = previousIndex != currentIds.count
|
||||
currentIds.append(fromPackInfo.id)
|
||||
}
|
||||
} else if beforeAll {
|
||||
didReorder = previousIndex != 0
|
||||
currentIds.insert(fromPackInfo.id, at: 0)
|
||||
} else if afterAll {
|
||||
didReorder = previousIndex != currentIds.count
|
||||
currentIds.append(fromPackInfo.id)
|
||||
}
|
||||
|
||||
temporaryPackOrder.set(.single(currentIds))
|
||||
|
||||
return .single(didReorder)
|
||||
}
|
||||
self.listView.reorderCompleted = { [weak self] opaqueState in
|
||||
guard let entries = (opaqueState as? ChatMediaInputPanelOpaqueState)?.entries else {
|
||||
return
|
||||
}
|
||||
|
||||
var currentIds: [ItemCollectionId] = []
|
||||
for entry in entries {
|
||||
switch entry {
|
||||
case let .stickerPack(_, info, _, _, _):
|
||||
currentIds.append(info.id)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
let _ = (context.account.postbox.transaction { transaction -> Void in
|
||||
let namespace = Namespaces.ItemCollection.CloudStickerPacks
|
||||
let infos = transaction.getItemCollectionsInfos(namespace: namespace)
|
||||
|
||||
var packDict: [ItemCollectionId: Int] = [:]
|
||||
for i in 0 ..< infos.count {
|
||||
packDict[infos[i].0] = i
|
||||
}
|
||||
var tempSortedPacks: [(ItemCollectionId, ItemCollectionInfo)] = []
|
||||
var processedPacks = Set<ItemCollectionId>()
|
||||
for id in currentIds {
|
||||
if let index = packDict[id] {
|
||||
tempSortedPacks.append(infos[index])
|
||||
processedPacks.insert(id)
|
||||
}
|
||||
}
|
||||
let restPacks = infos.filter { !processedPacks.contains($0.0) }
|
||||
let sortedPacks = restPacks + tempSortedPacks
|
||||
addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: .sync, noDelay: false)
|
||||
transaction.replaceItemCollectionInfos(namespace: namespace, itemCollectionInfos: sortedPacks)
|
||||
}
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
temporaryPackOrder.set(.single(nil))
|
||||
|
||||
if let strongSelf = self {
|
||||
if let lastReorderItemIndex = strongSelf.lastReorderItemIndex {
|
||||
strongSelf.lastReorderItemIndex = nil
|
||||
if strongSelf.panelIsFocused {
|
||||
strongSelf.panelFocusScrollToIndex = lastReorderItemIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self?.startCollapseTimer(timeout: 1.0)
|
||||
})
|
||||
}
|
||||
|
||||
self.inputNodeInteraction = ChatMediaInputNodeInteraction(navigateToCollectionId: { [weak self] collectionId in
|
||||
if let strongSelf = self, let currentView = strongSelf.currentView, (collectionId != strongSelf.inputNodeInteraction.highlightedItemCollectionId || true) {
|
||||
var index: Int32 = 0
|
||||
@ -885,10 +1060,30 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|
||||
|> map { animatedEmoji -> [String: [StickerPackItem]] in
|
||||
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
|
||||
switch animatedEmoji {
|
||||
case let .result(_, items, _):
|
||||
for case let item as StickerPackItem in items {
|
||||
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
|
||||
animatedEmojiStickers[emoji.basicEmoji.0] = [item]
|
||||
let strippedEmoji = emoji.basicEmoji.0.strippedEmoji
|
||||
if animatedEmojiStickers[strippedEmoji] == nil {
|
||||
animatedEmojiStickers[strippedEmoji] = [item]
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return animatedEmojiStickers
|
||||
}
|
||||
|
||||
let previousView = Atomic<ItemCollectionsView?>(value: nil)
|
||||
let transitionQueue = Queue()
|
||||
let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get(), reactions, self.panelIsFocusedPromise.get(), ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager))
|
||||
|> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions, panelExpanded, dismissedTrendingStickerPacks -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in
|
||||
let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get(), reactions, self.panelIsFocusedPromise.get(), ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager), temporaryPackOrder.get(), animatedEmojiStickers)
|
||||
|> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions, panelExpanded, dismissedTrendingStickerPacks, temporaryPackOrder, animatedEmojiStickers -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in
|
||||
let (view, viewUpdate) = viewAndUpdate
|
||||
let previous = previousView.swap(view)
|
||||
var update = viewUpdate
|
||||
@ -912,8 +1107,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
installedPacks.insert(info.0)
|
||||
}
|
||||
|
||||
let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, theme: theme, expanded: panelExpanded)
|
||||
let gifPaneEntries = chatMediaInputPanelGifModeEntries(theme: theme, reactions: reactions, expanded: panelExpanded)
|
||||
let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, temporaryPackOrder: temporaryPackOrder, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, theme: theme, expanded: panelExpanded)
|
||||
let gifPaneEntries = chatMediaInputPanelGifModeEntries(theme: theme, reactions: reactions, animatedEmojiStickers: animatedEmojiStickers, expanded: panelExpanded)
|
||||
var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, trendingPacks: trendingPacks, dismissedTrendingStickerPacks: dismissedTrendingStickerPacks, strings: strings, theme: theme)
|
||||
|
||||
if view.higher == nil {
|
||||
@ -1017,7 +1212,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
|
||||
self.listView.beganInteractiveDragging = { [weak self] position in
|
||||
if let strongSelf = self {
|
||||
strongSelf.panelFocusTimer?.invalidate()
|
||||
strongSelf.stopCollapseTimer()
|
||||
|
||||
var position = position
|
||||
var index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y))
|
||||
if index == nil {
|
||||
@ -1050,13 +1246,13 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
strongSelf.panelFocusScrollToIndex = nil
|
||||
strongSelf.panelFocusInitialPosition = nil
|
||||
}
|
||||
strongSelf.setupCollapseTimer(timeout: decelerated ? 0.5 : 1.5)
|
||||
strongSelf.startCollapseTimer(timeout: decelerated ? 0.5 : 1.5)
|
||||
}
|
||||
}
|
||||
|
||||
self.gifListView.beganInteractiveDragging = { [weak self] position in
|
||||
if let strongSelf = self {
|
||||
strongSelf.panelFocusTimer?.invalidate()
|
||||
strongSelf.stopCollapseTimer()
|
||||
var position = position
|
||||
var index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y))
|
||||
if index == nil {
|
||||
@ -1088,7 +1284,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
strongSelf.panelFocusScrollToIndex = nil
|
||||
strongSelf.panelFocusInitialPosition = nil
|
||||
}
|
||||
strongSelf.setupCollapseTimer(timeout: decelerated ? 0.5 : 1.5)
|
||||
strongSelf.startCollapseTimer(timeout: decelerated ? 0.5 : 1.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1108,7 +1304,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
self.updatePaneClippingContainer(size: self.paneClippingContainer.bounds.size, offset: self.currentCollectionListPanelOffset(), transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
|
||||
private func setupCollapseTimer(timeout: Double) {
|
||||
private func startCollapseTimer(timeout: Double) {
|
||||
self.panelFocusTimer?.invalidate()
|
||||
|
||||
let timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in
|
||||
@ -1124,6 +1320,11 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
timer.start()
|
||||
}
|
||||
|
||||
private func stopCollapseTimer() {
|
||||
self.panelFocusTimer?.invalidate()
|
||||
self.panelFocusTimer = nil
|
||||
}
|
||||
|
||||
private func openGifContextMenu(file: MultiplexedVideoNodeFile, sourceNode: ASDisplayNode, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
|
||||
let canSaveGif: Bool
|
||||
if file.file.media.fileId.namespace == Namespaces.Media.CloudFile {
|
||||
@ -2024,7 +2225,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
|
||||
var scrollToItem: ListViewScrollToItem?
|
||||
if let targetIndex = self.panelFocusScrollToIndex {
|
||||
if let targetIndex = self.panelFocusScrollToIndex, !self.listView.isReordering {
|
||||
var position: ListViewScrollPosition
|
||||
if self.panelIsFocused {
|
||||
if let initialPosition = self.panelFocusInitialPosition {
|
||||
@ -2044,7 +2245,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
scrollToItem = ListViewScrollToItem(index: targetIndex, position: position, animated: true, curve: .Spring(duration: 0.4), directionHint: .Down, displayLink: true)
|
||||
}
|
||||
|
||||
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateOpaqueState: transition.updateOpaqueState, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime)
|
||||
if !strongSelf.didSetReady {
|
||||
|
@ -42,7 +42,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
case stickersMode(PresentationTheme, Bool)
|
||||
case savedGifs(PresentationTheme, Bool)
|
||||
case trendingGifs(PresentationTheme, Bool)
|
||||
case gifEmotion(Int, PresentationTheme, String, Bool)
|
||||
case gifEmotion(Int, PresentationTheme, String, TelegramMediaFile?, Bool)
|
||||
|
||||
var stableId: ChatMediaInputPanelEntryStableId {
|
||||
switch self {
|
||||
@ -66,7 +66,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
return .savedGifs
|
||||
case .trendingGifs:
|
||||
return .trendingGifs
|
||||
case let .gifEmotion(_, _, emoji, _):
|
||||
case let .gifEmotion(_, _, emoji, _, _):
|
||||
return .gifEmotion(emoji)
|
||||
}
|
||||
}
|
||||
@ -133,8 +133,15 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .gifEmotion(lhsIndex, lhsTheme, lhsEmoji, lhsExpanded):
|
||||
if case let .gifEmotion(rhsIndex, rhsTheme, rhsEmoji, rhsExpanded) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsEmoji == rhsEmoji, lhsExpanded == rhsExpanded {
|
||||
case let .gifEmotion(lhsIndex, lhsTheme, lhsEmoji, lhsFile, lhsExpanded):
|
||||
if case let .gifEmotion(rhsIndex, rhsTheme, rhsEmoji, rhsFile, rhsExpanded) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsEmoji == rhsEmoji, lhsExpanded == rhsExpanded {
|
||||
if let lhsFile = lhsFile, let rhsFile = rhsFile {
|
||||
if !lhsFile.isEqual(to: rhsFile) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsFile != nil) != (rhsFile != nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -230,11 +237,11 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .gifEmotion(lhsIndex, _, _, _):
|
||||
case let .gifEmotion(lhsIndex, _, _, _, _):
|
||||
switch rhs {
|
||||
case .stickersMode, .savedGifs, .trendingGifs:
|
||||
return false
|
||||
case let .gifEmotion(rhsIndex, _, _, _):
|
||||
case let .gifEmotion(rhsIndex, _, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
default:
|
||||
return true
|
||||
@ -256,12 +263,12 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||
})
|
||||
case let .savedStickers(theme, expanded):
|
||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedStickers, theme: theme, expanded: expanded, selected: {
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .savedStickers, theme: theme, expanded: expanded, selected: {
|
||||
let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0)
|
||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||
})
|
||||
case let .recentPacks(theme, expanded):
|
||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .recentStickers, theme: theme, expanded: expanded, selected: {
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .recentStickers, theme: theme, expanded: expanded, selected: {
|
||||
let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0)
|
||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||
})
|
||||
@ -284,19 +291,19 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
inputNodeInteraction.navigateToCollectionId(info.id)
|
||||
})
|
||||
case let .stickersMode(theme, expanded):
|
||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .stickersMode, theme: theme, expanded: expanded, selected: {
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .stickersMode, theme: theme, expanded: expanded, selected: {
|
||||
inputNodeInteraction.navigateBackToStickers()
|
||||
})
|
||||
case let .savedGifs(theme, expanded):
|
||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .savedGifs, theme: theme, expanded: expanded, selected: {
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .savedGifs, theme: theme, expanded: expanded, selected: {
|
||||
inputNodeInteraction.setGifMode(.recent)
|
||||
})
|
||||
case let .trendingGifs(theme, expanded):
|
||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .trendingGifs, theme: theme, expanded: expanded, selected: {
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .trendingGifs, theme: theme, expanded: expanded, selected: {
|
||||
inputNodeInteraction.setGifMode(.trending)
|
||||
})
|
||||
case let .gifEmotion(_, theme, emoji, expanded):
|
||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji), theme: theme, expanded: expanded, selected: {
|
||||
case let .gifEmotion(_, theme, emoji, file, expanded):
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji, file), theme: theme, expanded: expanded, selected: {
|
||||
inputNodeInteraction.setGifMode(.emojiSearch(emoji))
|
||||
})
|
||||
}
|
||||
|
@ -87,6 +87,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
var currentCollectionId: ItemCollectionId?
|
||||
private var account: Account?
|
||||
private var currentThumbnailItem: StickerPackThumbnailItem?
|
||||
private var currentExpanded = false
|
||||
private var theme: PresentationTheme?
|
||||
@ -172,7 +173,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
|
||||
func updateStickerPackItem(account: Account, info: StickerPackCollectionInfo, item: StickerPackItem?, collectionId: ItemCollectionId, theme: PresentationTheme, expanded: Bool) {
|
||||
self.currentCollectionId = collectionId
|
||||
|
||||
self.account = account
|
||||
var themeUpdated = false
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
@ -262,7 +263,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale)
|
||||
expandTransition.updatePosition(node: self.scalingNode, position: CGPoint(x: boundsSize.width / 2.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0)))
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 8.0, height: expandedBoundingSize.height))
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: expandedBoundingSize.width - 4.0, height: expandedBoundingSize.height))
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((expandedBoundingSize.width - titleSize.width) / 2.0), y: expandedBoundingSize.height - titleSize.height + 6.0), size: titleSize)
|
||||
let displayTitleFrame = expanded ? titleFrame : CGRect(origin: CGPoint(x: titleFrame.minX, y: self.imageNode.position.y - titleFrame.size.height), size: titleFrame.size)
|
||||
@ -314,4 +315,74 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func isReorderable(at point: CGPoint) -> Bool {
|
||||
if self.bounds.contains(point) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func snapshotForReordering() -> UIView? {
|
||||
if let account = account, let thumbnailItem = self.currentThumbnailItem {
|
||||
var imageSize = boundingImageSize
|
||||
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
|
||||
let containerNode = ASDisplayNode()
|
||||
let scalingNode = ASDisplayNode()
|
||||
containerNode.addSubnode(scalingNode)
|
||||
containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
|
||||
var snapshotImageNode: TransformImageNode?
|
||||
var snapshotAnimationNode: AnimatedStickerNode?
|
||||
switch thumbnailItem {
|
||||
case let .still(representation):
|
||||
imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize)
|
||||
|
||||
let imageNode = TransformImageNode()
|
||||
let imageApply = imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingImageSize, intrinsicInsets: UIEdgeInsets()))
|
||||
imageApply()
|
||||
imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource, nilIfEmpty: true))
|
||||
scalingNode.addSubnode(imageNode)
|
||||
|
||||
snapshotImageNode = imageNode
|
||||
case let .animated(resource, _):
|
||||
let animatedStickerNode = AnimatedStickerNode()
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 128, height: 128, mode: .direct(cachePathPrefix: nil))
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
scalingNode.addSubnode(animatedStickerNode)
|
||||
|
||||
animatedStickerNode.cloneCurrentFrame(from: self.animatedStickerNode)
|
||||
animatedStickerNode.play(fromIndex: self.animatedStickerNode?.currentFrameIndex)
|
||||
|
||||
snapshotAnimationNode = animatedStickerNode
|
||||
}
|
||||
|
||||
containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize)
|
||||
scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize)
|
||||
|
||||
if let titleView = self.titleNode.view.snapshotContentTree() {
|
||||
titleView.frame = self.titleNode.frame
|
||||
scalingNode.view.addSubview(titleView)
|
||||
}
|
||||
|
||||
let imageFrame = CGRect(origin: CGPoint(x: (expandedBoundingSize.height - imageSize.width) / 2.0, y: (expandedBoundingSize.width - imageSize.height) / 2.0), size: imageSize)
|
||||
if let imageNode = snapshotImageNode {
|
||||
imageNode.bounds = CGRect(origin: CGPoint(), size: imageSize)
|
||||
imageNode.position = imageFrame.center
|
||||
}
|
||||
if let animatedStickerNode = snapshotAnimationNode {
|
||||
animatedStickerNode.frame = imageFrame
|
||||
animatedStickerNode.updateLayout(size: imageFrame.size)
|
||||
}
|
||||
|
||||
let expanded = self.currentExpanded
|
||||
let scale = expanded ? 1.0 : boundingImageScale
|
||||
let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height)
|
||||
scalingNode.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
scalingNode.position = CGPoint(x: boundsSize.width / 2.0 + 3.0, y: boundsSize.height / 2.0 + (expanded ? -53.0 : -7.0) - 3.0)
|
||||
|
||||
return containerNode.view
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -1120,7 +1120,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
} else {
|
||||
audioRecordingInfoContainerNode = ASDisplayNode()
|
||||
self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode
|
||||
self.insertSubnode(audioRecordingInfoContainerNode, at: 0)
|
||||
self.clippingNode.insertSubnode(audioRecordingInfoContainerNode, at: 0)
|
||||
}
|
||||
|
||||
var animateTimeSlideIn = false
|
||||
@ -1148,7 +1148,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
self?.interfaceInteraction?.finishMediaRecording(.dismiss)
|
||||
})
|
||||
self.audioRecordingCancelIndicator = audioRecordingCancelIndicator
|
||||
self.insertSubnode(audioRecordingCancelIndicator, at: 0)
|
||||
self.clippingNode.insertSubnode(audioRecordingCancelIndicator, at: 0)
|
||||
}
|
||||
|
||||
let isLocked = mediaRecordingState?.isLocked ?? (interfaceState.recordedMediaPreview != nil)
|
||||
@ -1257,7 +1257,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
audioRecordingDotNode = AnimationNode(animation: "BinRed")
|
||||
self.audioRecordingDotNode = audioRecordingDotNode
|
||||
self.audioRecordingDotNodeDismissed = false
|
||||
self.insertSubnode(audioRecordingDotNode, belowSubnode: self.menuButton)
|
||||
self.clippingNode.insertSubnode(audioRecordingDotNode, belowSubnode: self.menuButton)
|
||||
self.animatingBinNode?.removeFromSupernode()
|
||||
self.animatingBinNode = nil
|
||||
}
|
||||
@ -1412,7 +1412,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
self?.interfaceInteraction?.finishMediaRecording(.send)
|
||||
return true
|
||||
}
|
||||
self.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons)
|
||||
self.clippingNode.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons)
|
||||
}
|
||||
self.actionButtons.isAccessibilityElement = false
|
||||
let size: CGFloat = 120.0
|
||||
@ -1475,7 +1475,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
contextPlaceholderNode.displaysAsynchronously = false
|
||||
contextPlaceholderNode.isUserInteractionEnabled = false
|
||||
self.contextPlaceholderNode = contextPlaceholderNode
|
||||
self.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode)
|
||||
self.clippingNode.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode)
|
||||
}
|
||||
|
||||
let _ = placeholderApply()
|
||||
@ -1495,7 +1495,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
} else {
|
||||
slowmodePlaceholderNode = ChatTextInputSlowmodePlaceholderNode(theme: interfaceState.theme)
|
||||
self.slowmodePlaceholderNode = slowmodePlaceholderNode
|
||||
self.insertSubnode(slowmodePlaceholderNode, aboveSubnode: self.textPlaceholderNode)
|
||||
self.clippingNode.insertSubnode(slowmodePlaceholderNode, aboveSubnode: self.textPlaceholderNode)
|
||||
}
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: CGSize(width: width - leftInset - rightInset - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: 30.0))
|
||||
slowmodePlaceholderNode.updateState(slowmodeState)
|
||||
|
@ -71,7 +71,7 @@ private final class PrefetchManagerInnerImpl {
|
||||
}
|
||||
}
|
||||
|
||||
let popularEmoji = ["\u{2764}", "👍", "😳", "😒", "🥳"]
|
||||
let popularEmoji = ["\u{2764}", "👍", "👎", "😳", "😒", "🥳", "😡", "😮", "😂", "😘", "😍", "🙄", "😎"]
|
||||
for emoji in popularEmoji {
|
||||
if let sticker = animatedEmojiStickers[emoji] {
|
||||
if let _ = account.postbox.mediaBox.completedResourcePath(sticker.file.resource) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user