mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 01:10:09 +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()
|
self.timer.swap(nil)?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private weak var nodeToCopyFrameFrom: AnimatedStickerNode?
|
||||||
override public func didLoad() {
|
override public func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
|
|
||||||
@ -879,8 +880,22 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
//self.renderer = MetalAnimationRenderer()
|
//self.renderer = MetalAnimationRenderer()
|
||||||
#endif
|
#endif
|
||||||
self.renderer?.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
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!)
|
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) {
|
public func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) {
|
||||||
if width < 2 || height < 2 {
|
if width < 2 || height < 2 {
|
||||||
@ -973,7 +988,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var isSetUpForPlayback = false
|
private var isSetUpForPlayback = false
|
||||||
|
|
||||||
public func play(firstFrame: Bool = false, fromIndex: Int? = nil) {
|
public func play(firstFrame: Bool = false, fromIndex: Int? = nil) {
|
||||||
switch self.playbackMode {
|
switch self.playbackMode {
|
||||||
case .once:
|
case .once:
|
||||||
|
|||||||
@ -300,6 +300,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
private var currentGeneralScrollDirection: GeneralScrollDirection?
|
private var currentGeneralScrollDirection: GeneralScrollDirection?
|
||||||
public final var generalScrollDirectionUpdated: (GeneralScrollDirection) -> Void = { _ in }
|
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 reorderItem: (Int, Int, Any?) -> Signal<Bool, NoError> = { _, _, _ in return .single(false) }
|
||||||
public final var reorderCompleted: (Any?) -> Void = { _ in }
|
public final var reorderCompleted: (Any?) -> Void = { _ in }
|
||||||
|
|
||||||
@ -322,8 +325,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
private var reorderNode: ListViewReorderingItemNode?
|
private var reorderNode: ListViewReorderingItemNode?
|
||||||
private var reorderFeedback: HapticFeedback?
|
private var reorderFeedback: HapticFeedback?
|
||||||
private var reorderFeedbackDisposable: MetaDisposable?
|
private var reorderFeedbackDisposable: MetaDisposable?
|
||||||
private var isReorderingItems: Bool = false
|
private var reorderInProgress: Bool = false
|
||||||
private var reorderingItemsCompleted: (() -> Void)?
|
private var reorderingItemsCompleted: (() -> Void)?
|
||||||
|
public var reorderedItemHasShadow = true
|
||||||
|
|
||||||
private let waitingForNodesDisposable = MetaDisposable()
|
private let waitingForNodesDisposable = MetaDisposable()
|
||||||
|
|
||||||
@ -394,15 +398,19 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
let itemNodeFrame = itemNode.frame
|
let itemNodeFrame = itemNode.frame
|
||||||
let itemNodeBounds = itemNode.bounds
|
let itemNodeBounds = itemNode.bounds
|
||||||
if itemNode.isReorderable(at: point.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY)) {
|
if itemNode.isReorderable(at: point.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY)) {
|
||||||
strongSelf.beginReordering(itemNode: itemNode)
|
let requiresLongPress = !strongSelf.reorderedItemHasShadow
|
||||||
return true
|
return (true, requiresLongPress, itemNode)
|
||||||
}
|
}
|
||||||
break
|
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
|
}, ended: { [weak self] in
|
||||||
self?.endReordering()
|
self?.endReordering()
|
||||||
}, moved: { [weak self] offset in
|
}, moved: { [weak self] offset in
|
||||||
@ -469,10 +477,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func beginReordering(itemNode: ListViewItemNode) {
|
private func beginReordering(itemNode: ListViewItemNode) {
|
||||||
|
self.isReordering = true
|
||||||
|
self.reorderBegan()
|
||||||
|
|
||||||
if let reorderNode = self.reorderNode {
|
if let reorderNode = self.reorderNode {
|
||||||
reorderNode.removeFromSupernode()
|
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
|
self.reorderNode = reorderNode
|
||||||
if let verticalScrollIndicator = self.verticalScrollIndicator {
|
if let verticalScrollIndicator = self.verticalScrollIndicator {
|
||||||
self.insertSubnode(reorderNode, belowSubnode: verticalScrollIndicator)
|
self.insertSubnode(reorderNode, belowSubnode: verticalScrollIndicator)
|
||||||
@ -509,8 +520,9 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
strongSelf.reorderCompleted(strongSelf.opaqueTransactionState)
|
strongSelf.reorderCompleted(strongSelf.opaqueTransactionState)
|
||||||
|
strongSelf.isReordering = false
|
||||||
}
|
}
|
||||||
if self.isReorderingItems {
|
if self.reorderInProgress {
|
||||||
self.reorderingItemsCompleted = f
|
self.reorderingItemsCompleted = f
|
||||||
} else {
|
} else {
|
||||||
f()
|
f()
|
||||||
@ -587,14 +599,14 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
if self.reorderFeedbackDisposable == nil {
|
if self.reorderFeedbackDisposable == nil {
|
||||||
self.reorderFeedbackDisposable = MetaDisposable()
|
self.reorderFeedbackDisposable = MetaDisposable()
|
||||||
}
|
}
|
||||||
self.isReorderingItems = true
|
self.reorderInProgress = true
|
||||||
self.reorderFeedbackDisposable?.set((self.reorderItem(reorderItemIndex, toIndex, self.opaqueTransactionState)
|
self.reorderFeedbackDisposable?.set((self.reorderItem(reorderItemIndex, toIndex, self.opaqueTransactionState)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.isReorderingItems = false
|
strongSelf.reorderInProgress = false
|
||||||
if let reorderingItemsCompleted = strongSelf.reorderingItemsCompleted {
|
if let reorderingItemsCompleted = strongSelf.reorderingItemsCompleted {
|
||||||
strongSelf.reorderingItemsCompleted = nil
|
strongSelf.reorderingItemsCompleted = nil
|
||||||
reorderingItemsCompleted()
|
reorderingItemsCompleted()
|
||||||
|
|||||||
@ -1,28 +1,101 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
|
||||||
final class ListViewReorderingGestureRecognizer: UIGestureRecognizer {
|
public final class ListViewReorderingGestureRecognizer: UIGestureRecognizer {
|
||||||
private let shouldBegin: (CGPoint) -> Bool
|
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 ended: () -> Void
|
||||||
private let moved: (CGFloat) -> Void
|
private let moved: (CGFloat) -> Void
|
||||||
|
|
||||||
private var initialLocation: CGPoint?
|
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.shouldBegin = shouldBegin
|
||||||
|
self.willBegin = willBegin
|
||||||
|
self.began = began
|
||||||
self.ended = ended
|
self.ended = ended
|
||||||
self.moved = moved
|
self.moved = moved
|
||||||
|
|
||||||
super.init(target: nil, action: nil)
|
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()
|
super.reset()
|
||||||
|
|
||||||
|
self.itemNode = nil
|
||||||
|
self.stopLongTapTimer()
|
||||||
|
self.stopLongPressTimer()
|
||||||
self.initialLocation = nil
|
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)
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
if self.numberOfTouches > 1 {
|
if self.numberOfTouches > 1 {
|
||||||
@ -32,40 +105,76 @@ final class ListViewReorderingGestureRecognizer: UIGestureRecognizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.state == .possible {
|
if self.state == .possible {
|
||||||
if let location = touches.first?.location(in: self.view), self.shouldBegin(location) {
|
if let location = touches.first?.location(in: self.view) {
|
||||||
self.initialLocation = location
|
let (allowed, requiresLongPress, itemNode) = self.shouldBegin(location)
|
||||||
self.state = .began
|
if allowed {
|
||||||
|
self.itemNode = itemNode
|
||||||
|
self.initialLocation = location
|
||||||
|
if requiresLongPress {
|
||||||
|
self.startLongTapTimer()
|
||||||
|
self.startLongPressTimer()
|
||||||
|
} else {
|
||||||
|
self.state = .began
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.state = .failed
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.state = .failed
|
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)
|
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 {
|
if self.state == .began || self.state == .changed {
|
||||||
self.ended()
|
self.ended()
|
||||||
self.state = .failed
|
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)
|
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 {
|
if self.state == .began || self.state == .changed {
|
||||||
self.ended()
|
self.ended()
|
||||||
self.state = .failed
|
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)
|
super.touchesMoved(touches, with: event)
|
||||||
|
|
||||||
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
||||||
self.state = .changed
|
self.state = .changed
|
||||||
let offset = location.y - initialLocation.y
|
let offset = location.y - initialLocation.y
|
||||||
self.moved(offset)
|
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 topShadow: UIImageView
|
||||||
let bottomShadow: UIImageView
|
let bottomShadow: UIImageView
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
init(frame: CGRect, hasShadow: Bool) {
|
||||||
self.topShadow = UIImageView()
|
self.topShadow = UIImageView()
|
||||||
self.bottomShadow = UIImageView()
|
self.bottomShadow = UIImageView()
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.topShadow.image = generateShadowImage(mirror: true)
|
if hasShadow {
|
||||||
self.bottomShadow.image = generateShadowImage(mirror: false)
|
self.topShadow.image = generateShadowImage(mirror: true)
|
||||||
|
self.bottomShadow.image = generateShadowImage(mirror: false)
|
||||||
|
}
|
||||||
|
|
||||||
self.addSubview(self.topShadow)
|
self.addSubview(self.topShadow)
|
||||||
self.addSubview(self.bottomShadow)
|
self.addSubview(self.bottomShadow)
|
||||||
@ -51,9 +53,9 @@ final class ListViewReorderingItemNode: ASDisplayNode {
|
|||||||
private let copyView: CopyView
|
private let copyView: CopyView
|
||||||
private let initialLocation: CGPoint
|
private let initialLocation: CGPoint
|
||||||
|
|
||||||
init(itemNode: ListViewItemNode, initialLocation: CGPoint) {
|
init(itemNode: ListViewItemNode, initialLocation: CGPoint, hasShadow: Bool) {
|
||||||
self.itemNode = itemNode
|
self.itemNode = itemNode
|
||||||
self.copyView = CopyView(frame: CGRect())
|
self.copyView = CopyView(frame: CGRect(), hasShadow: hasShadow)
|
||||||
let snapshotView = itemNode.snapshotForReordering()
|
let snapshotView = itemNode.snapshotForReordering()
|
||||||
self.initialLocation = initialLocation
|
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.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.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.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)
|
self.copyView.topShadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import TelegramCore
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
import Postbox
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
import AnimatedStickerNode
|
||||||
|
import TelegramAnimatedStickerNode
|
||||||
|
|
||||||
enum ChatMediaInputMetaSectionItemType: Equatable {
|
enum ChatMediaInputMetaSectionItemType: Equatable {
|
||||||
case savedStickers
|
case savedStickers
|
||||||
@ -13,10 +15,11 @@ enum ChatMediaInputMetaSectionItemType: Equatable {
|
|||||||
case stickersMode
|
case stickersMode
|
||||||
case savedGifs
|
case savedGifs
|
||||||
case trendingGifs
|
case trendingGifs
|
||||||
case gifEmoji(String)
|
case gifEmoji(String, TelegramMediaFile?)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ChatMediaInputMetaSectionItem: ListViewItem {
|
final class ChatMediaInputMetaSectionItem: ListViewItem {
|
||||||
|
let account: Account
|
||||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||||
let type: ChatMediaInputMetaSectionItemType
|
let type: ChatMediaInputMetaSectionItemType
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
@ -27,7 +30,8 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
|
|||||||
return true
|
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.inputNodeInteraction = inputNodeInteraction
|
||||||
self.type = type
|
self.type = type
|
||||||
self.selectedItem = selected
|
self.selectedItem = selected
|
||||||
@ -41,7 +45,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
|
|||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
node.inputNodeInteraction = self.inputNodeInteraction
|
node.inputNodeInteraction = self.inputNodeInteraction
|
||||||
node.setItem(item: self)
|
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.updateIsHighlighted()
|
||||||
node.updateAppearanceTransition(transition: .immediate)
|
node.updateAppearanceTransition(transition: .immediate)
|
||||||
|
|
||||||
@ -61,7 +65,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
|
|||||||
Queue.mainQueue().async {
|
Queue.mainQueue().async {
|
||||||
completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: node().insets), { _ in
|
completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: node().insets), { _ in
|
||||||
(node() as? ChatMediaInputMetaSectionItemNode)?.setItem(item: self)
|
(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 highlightNode: ASImageNode
|
||||||
private let titleNode: ImmediateTextNode
|
private let titleNode: ImmediateTextNode
|
||||||
|
|
||||||
|
private var animatedStickerNode: AnimatedStickerNode?
|
||||||
|
|
||||||
private var currentExpanded = false
|
private var currentExpanded = false
|
||||||
|
|
||||||
var item: ChatMediaInputMetaSectionItem?
|
var item: ChatMediaInputMetaSectionItem?
|
||||||
@ -94,6 +100,23 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
var theme: PresentationTheme?
|
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() {
|
init() {
|
||||||
self.containerNode = ASDisplayNode()
|
self.containerNode = ASDisplayNode()
|
||||||
self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
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)
|
self.scalingNode.addSubnode(self.textNodeContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.stickerFetchedDisposable.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
super.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)
|
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)
|
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:
|
case .trendingGifs:
|
||||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingGifsIcon(theme)
|
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingGifsIcon(theme)
|
||||||
title = "Trending"
|
title = "Trending"
|
||||||
case let .gifEmoji(emoji):
|
case let .gifEmoji(emoji, file):
|
||||||
var emoji = emoji
|
var emoji = emoji
|
||||||
switch emoji {
|
switch emoji {
|
||||||
case "😡":
|
case "😡":
|
||||||
@ -200,16 +227,38 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
|||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if emoji == "🥳" {
|
// if emoji == "🥳" {
|
||||||
if #available(iOSApplicationExtension 12.1, iOS 12.1, *) {
|
// if #available(iOSApplicationExtension 12.1, iOS 12.1, *) {
|
||||||
} else {
|
// } else {
|
||||||
emoji = "🎉"
|
// emoji = "🎉"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
self.imageNode.image = nil
|
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))
|
if let file = file {
|
||||||
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)
|
|
||||||
|
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)
|
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.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)))
|
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 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)
|
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
|
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))
|
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 {
|
if case .trending = inputNodeInteraction.highlightedGifMode {
|
||||||
isHighlighted = true
|
isHighlighted = true
|
||||||
}
|
}
|
||||||
case let .gifEmoji(emoji):
|
case let .gifEmoji(emoji, _):
|
||||||
if case .emojiSearch(emoji) = inputNodeInteraction.highlightedGifMode {
|
if case .emojiSearch(emoji) = inputNodeInteraction.highlightedGifMode {
|
||||||
isHighlighted = true
|
isHighlighted = true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,11 +30,20 @@ enum CanInstallPeerSpecificPack {
|
|||||||
case available(peer: Peer, dismissed: Bool)
|
case available(peer: Peer, dismissed: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ChatMediaInputPanelOpaqueState {
|
||||||
|
let entries: [ChatMediaInputPanelEntry]
|
||||||
|
|
||||||
|
init(entries: [ChatMediaInputPanelEntry]) {
|
||||||
|
self.entries = entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ChatMediaInputPanelTransition {
|
struct ChatMediaInputPanelTransition {
|
||||||
let deletions: [ListViewDeleteItem]
|
let deletions: [ListViewDeleteItem]
|
||||||
let insertions: [ListViewInsertItem]
|
let insertions: [ListViewInsertItem]
|
||||||
let updates: [ListViewUpdateItem]
|
let updates: [ListViewUpdateItem]
|
||||||
let scrollToItem: ListViewScrollToItem?
|
let scrollToItem: ListViewScrollToItem?
|
||||||
|
let updateOpaqueState: ChatMediaInputPanelOpaqueState?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatMediaInputGridTransition {
|
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 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) }
|
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 {
|
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)
|
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] = []
|
var entries: [ChatMediaInputPanelEntry] = []
|
||||||
if hasGifs {
|
if hasGifs {
|
||||||
entries.append(.recentGifs(theme, expanded))
|
entries.append(.recentGifs(theme, expanded))
|
||||||
@ -189,13 +198,36 @@ func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: Ordere
|
|||||||
entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded))
|
entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded))
|
||||||
}
|
}
|
||||||
var index = 0
|
var index = 0
|
||||||
for (_, info, item) in view.collectionInfos {
|
|
||||||
if let info = info as? StickerPackCollectionInfo, item != nil {
|
var sortedPacks: [(ItemCollectionId, StickerPackCollectionInfo, StickerPackItem?)] = []
|
||||||
entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem, theme: theme, expanded: expanded))
|
for (id, info, item) in view.collectionInfos {
|
||||||
index += 1
|
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 {
|
if peerSpecificPack == nil, case let .available(peer, true) = canInstallPeerSpecificPack {
|
||||||
entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded))
|
entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded))
|
||||||
}
|
}
|
||||||
@ -206,14 +238,14 @@ func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: Ordere
|
|||||||
return entries
|
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] = []
|
var entries: [ChatMediaInputPanelEntry] = []
|
||||||
entries.append(.stickersMode(theme, expanded))
|
entries.append(.stickersMode(theme, expanded))
|
||||||
entries.append(.savedGifs(theme, expanded))
|
entries.append(.savedGifs(theme, expanded))
|
||||||
entries.append(.trendingGifs(theme, expanded))
|
entries.append(.trendingGifs(theme, expanded))
|
||||||
|
|
||||||
for reaction in reactions {
|
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
|
return entries
|
||||||
@ -248,7 +280,7 @@ func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: Ordered
|
|||||||
}
|
}
|
||||||
|
|
||||||
var trendingIsDismissed = false
|
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
|
trendingIsDismissed = true
|
||||||
}
|
}
|
||||||
if !trendingIsDismissed {
|
if !trendingIsDismissed {
|
||||||
@ -470,6 +502,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var panelFocusTimer: SwiftSignalKit.Timer?
|
private var panelFocusTimer: SwiftSignalKit.Timer?
|
||||||
|
private var lastReorderItemIndex: Int?
|
||||||
|
|
||||||
var requestDisableStickerAnimations: ((Bool) -> Void)?
|
var requestDisableStickerAnimations: ((Bool) -> Void)?
|
||||||
|
|
||||||
@ -514,6 +547,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
|||||||
|
|
||||||
self.listView = ListView()
|
self.listView = ListView()
|
||||||
self.listView.useSingleDimensionTouchPoint = true
|
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.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0)
|
||||||
self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = false
|
self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = false
|
||||||
self.listView.accessibilityPageScrolledString = { row, count in
|
self.listView.accessibilityPageScrolledString = { row, count in
|
||||||
@ -551,6 +585,147 @@ final class ChatMediaInputNode: ChatInputNode {
|
|||||||
|
|
||||||
super.init()
|
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
|
self.inputNodeInteraction = ChatMediaInputNodeInteraction(navigateToCollectionId: { [weak self] collectionId in
|
||||||
if let strongSelf = self, let currentView = strongSelf.currentView, (collectionId != strongSelf.inputNodeInteraction.highlightedItemCollectionId || true) {
|
if let strongSelf = self, let currentView = strongSelf.currentView, (collectionId != strongSelf.inputNodeInteraction.highlightedItemCollectionId || true) {
|
||||||
var index: Int32 = 0
|
var index: Int32 = 0
|
||||||
@ -885,10 +1060,30 @@ final class ChatMediaInputNode: ChatInputNode {
|
|||||||
}
|
}
|
||||||
|> distinctUntilChanged
|
|> 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 previousView = Atomic<ItemCollectionsView?>(value: nil)
|
||||||
let transitionQueue = Queue()
|
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))
|
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 -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in
|
|> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions, panelExpanded, dismissedTrendingStickerPacks, temporaryPackOrder, animatedEmojiStickers -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in
|
||||||
let (view, viewUpdate) = viewAndUpdate
|
let (view, viewUpdate) = viewAndUpdate
|
||||||
let previous = previousView.swap(view)
|
let previous = previousView.swap(view)
|
||||||
var update = viewUpdate
|
var update = viewUpdate
|
||||||
@ -912,8 +1107,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
|||||||
installedPacks.insert(info.0)
|
installedPacks.insert(info.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, theme: theme, 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, 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)
|
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 {
|
if view.higher == nil {
|
||||||
@ -1017,7 +1212,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
|||||||
|
|
||||||
self.listView.beganInteractiveDragging = { [weak self] position in
|
self.listView.beganInteractiveDragging = { [weak self] position in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.panelFocusTimer?.invalidate()
|
strongSelf.stopCollapseTimer()
|
||||||
|
|
||||||
var position = position
|
var position = position
|
||||||
var index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y))
|
var index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y))
|
||||||
if index == nil {
|
if index == nil {
|
||||||
@ -1050,13 +1246,13 @@ final class ChatMediaInputNode: ChatInputNode {
|
|||||||
strongSelf.panelFocusScrollToIndex = nil
|
strongSelf.panelFocusScrollToIndex = nil
|
||||||
strongSelf.panelFocusInitialPosition = 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
|
self.gifListView.beganInteractiveDragging = { [weak self] position in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.panelFocusTimer?.invalidate()
|
strongSelf.stopCollapseTimer()
|
||||||
var position = position
|
var position = position
|
||||||
var index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y))
|
var index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y))
|
||||||
if index == nil {
|
if index == nil {
|
||||||
@ -1088,7 +1284,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
|||||||
strongSelf.panelFocusScrollToIndex = nil
|
strongSelf.panelFocusScrollToIndex = nil
|
||||||
strongSelf.panelFocusInitialPosition = 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))
|
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()
|
self.panelFocusTimer?.invalidate()
|
||||||
|
|
||||||
let timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in
|
let timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in
|
||||||
@ -1124,6 +1320,11 @@ final class ChatMediaInputNode: ChatInputNode {
|
|||||||
timer.start()
|
timer.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func stopCollapseTimer() {
|
||||||
|
self.panelFocusTimer?.invalidate()
|
||||||
|
self.panelFocusTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
private func openGifContextMenu(file: MultiplexedVideoNodeFile, sourceNode: ASDisplayNode, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
|
private func openGifContextMenu(file: MultiplexedVideoNodeFile, sourceNode: ASDisplayNode, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) {
|
||||||
let canSaveGif: Bool
|
let canSaveGif: Bool
|
||||||
if file.file.media.fileId.namespace == Namespaces.Media.CloudFile {
|
if file.file.media.fileId.namespace == Namespaces.Media.CloudFile {
|
||||||
@ -2024,7 +2225,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var scrollToItem: ListViewScrollToItem?
|
var scrollToItem: ListViewScrollToItem?
|
||||||
if let targetIndex = self.panelFocusScrollToIndex {
|
if let targetIndex = self.panelFocusScrollToIndex, !self.listView.isReordering {
|
||||||
var position: ListViewScrollPosition
|
var position: ListViewScrollPosition
|
||||||
if self.panelIsFocused {
|
if self.panelIsFocused {
|
||||||
if let initialPosition = self.panelFocusInitialPosition {
|
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)
|
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 {
|
if let strongSelf = self {
|
||||||
strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime)
|
strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime)
|
||||||
if !strongSelf.didSetReady {
|
if !strongSelf.didSetReady {
|
||||||
|
|||||||
@ -42,7 +42,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
|||||||
case stickersMode(PresentationTheme, Bool)
|
case stickersMode(PresentationTheme, Bool)
|
||||||
case savedGifs(PresentationTheme, Bool)
|
case savedGifs(PresentationTheme, Bool)
|
||||||
case trendingGifs(PresentationTheme, Bool)
|
case trendingGifs(PresentationTheme, Bool)
|
||||||
case gifEmotion(Int, PresentationTheme, String, Bool)
|
case gifEmotion(Int, PresentationTheme, String, TelegramMediaFile?, Bool)
|
||||||
|
|
||||||
var stableId: ChatMediaInputPanelEntryStableId {
|
var stableId: ChatMediaInputPanelEntryStableId {
|
||||||
switch self {
|
switch self {
|
||||||
@ -66,7 +66,7 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
|||||||
return .savedGifs
|
return .savedGifs
|
||||||
case .trendingGifs:
|
case .trendingGifs:
|
||||||
return .trendingGifs
|
return .trendingGifs
|
||||||
case let .gifEmotion(_, _, emoji, _):
|
case let .gifEmotion(_, _, emoji, _, _):
|
||||||
return .gifEmotion(emoji)
|
return .gifEmotion(emoji)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,8 +133,15 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .gifEmotion(lhsIndex, lhsTheme, lhsEmoji, lhsExpanded):
|
case let .gifEmotion(lhsIndex, lhsTheme, lhsEmoji, lhsFile, lhsExpanded):
|
||||||
if case let .gifEmotion(rhsIndex, rhsTheme, rhsEmoji, rhsExpanded) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsEmoji == rhsEmoji, lhsExpanded == rhsExpanded {
|
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
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -230,11 +237,11 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
|||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
case let .gifEmotion(lhsIndex, _, _, _):
|
case let .gifEmotion(lhsIndex, _, _, _, _):
|
||||||
switch rhs {
|
switch rhs {
|
||||||
case .stickersMode, .savedGifs, .trendingGifs:
|
case .stickersMode, .savedGifs, .trendingGifs:
|
||||||
return false
|
return false
|
||||||
case let .gifEmotion(rhsIndex, _, _, _):
|
case let .gifEmotion(rhsIndex, _, _, _, _):
|
||||||
return lhsIndex < rhsIndex
|
return lhsIndex < rhsIndex
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
@ -256,12 +263,12 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
|||||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||||
})
|
})
|
||||||
case let .savedStickers(theme, expanded):
|
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)
|
let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0)
|
||||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||||
})
|
})
|
||||||
case let .recentPacks(theme, expanded):
|
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)
|
let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0)
|
||||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||||
})
|
})
|
||||||
@ -284,19 +291,19 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
|||||||
inputNodeInteraction.navigateToCollectionId(info.id)
|
inputNodeInteraction.navigateToCollectionId(info.id)
|
||||||
})
|
})
|
||||||
case let .stickersMode(theme, expanded):
|
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()
|
inputNodeInteraction.navigateBackToStickers()
|
||||||
})
|
})
|
||||||
case let .savedGifs(theme, expanded):
|
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)
|
inputNodeInteraction.setGifMode(.recent)
|
||||||
})
|
})
|
||||||
case let .trendingGifs(theme, expanded):
|
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)
|
inputNodeInteraction.setGifMode(.trending)
|
||||||
})
|
})
|
||||||
case let .gifEmotion(_, theme, emoji, expanded):
|
case let .gifEmotion(_, theme, emoji, file, expanded):
|
||||||
return ChatMediaInputMetaSectionItem(inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji), theme: theme, expanded: expanded, selected: {
|
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji, file), theme: theme, expanded: expanded, selected: {
|
||||||
inputNodeInteraction.setGifMode(.emojiSearch(emoji))
|
inputNodeInteraction.setGifMode(.emojiSearch(emoji))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,6 +87,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||||
var currentCollectionId: ItemCollectionId?
|
var currentCollectionId: ItemCollectionId?
|
||||||
|
private var account: Account?
|
||||||
private var currentThumbnailItem: StickerPackThumbnailItem?
|
private var currentThumbnailItem: StickerPackThumbnailItem?
|
||||||
private var currentExpanded = false
|
private var currentExpanded = false
|
||||||
private var theme: PresentationTheme?
|
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) {
|
func updateStickerPackItem(account: Account, info: StickerPackCollectionInfo, item: StickerPackItem?, collectionId: ItemCollectionId, theme: PresentationTheme, expanded: Bool) {
|
||||||
self.currentCollectionId = collectionId
|
self.currentCollectionId = collectionId
|
||||||
|
self.account = account
|
||||||
var themeUpdated = false
|
var themeUpdated = false
|
||||||
if self.theme !== theme {
|
if self.theme !== theme {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
@ -262,7 +263,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
|||||||
expandTransition.updateTransformScale(node: self.scalingNode, scale: expandScale)
|
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)))
|
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 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)
|
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) {
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
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 {
|
} else {
|
||||||
audioRecordingInfoContainerNode = ASDisplayNode()
|
audioRecordingInfoContainerNode = ASDisplayNode()
|
||||||
self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode
|
self.audioRecordingInfoContainerNode = audioRecordingInfoContainerNode
|
||||||
self.insertSubnode(audioRecordingInfoContainerNode, at: 0)
|
self.clippingNode.insertSubnode(audioRecordingInfoContainerNode, at: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var animateTimeSlideIn = false
|
var animateTimeSlideIn = false
|
||||||
@ -1148,7 +1148,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
self?.interfaceInteraction?.finishMediaRecording(.dismiss)
|
self?.interfaceInteraction?.finishMediaRecording(.dismiss)
|
||||||
})
|
})
|
||||||
self.audioRecordingCancelIndicator = audioRecordingCancelIndicator
|
self.audioRecordingCancelIndicator = audioRecordingCancelIndicator
|
||||||
self.insertSubnode(audioRecordingCancelIndicator, at: 0)
|
self.clippingNode.insertSubnode(audioRecordingCancelIndicator, at: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let isLocked = mediaRecordingState?.isLocked ?? (interfaceState.recordedMediaPreview != nil)
|
let isLocked = mediaRecordingState?.isLocked ?? (interfaceState.recordedMediaPreview != nil)
|
||||||
@ -1257,7 +1257,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
audioRecordingDotNode = AnimationNode(animation: "BinRed")
|
audioRecordingDotNode = AnimationNode(animation: "BinRed")
|
||||||
self.audioRecordingDotNode = audioRecordingDotNode
|
self.audioRecordingDotNode = audioRecordingDotNode
|
||||||
self.audioRecordingDotNodeDismissed = false
|
self.audioRecordingDotNodeDismissed = false
|
||||||
self.insertSubnode(audioRecordingDotNode, belowSubnode: self.menuButton)
|
self.clippingNode.insertSubnode(audioRecordingDotNode, belowSubnode: self.menuButton)
|
||||||
self.animatingBinNode?.removeFromSupernode()
|
self.animatingBinNode?.removeFromSupernode()
|
||||||
self.animatingBinNode = nil
|
self.animatingBinNode = nil
|
||||||
}
|
}
|
||||||
@ -1412,7 +1412,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
self?.interfaceInteraction?.finishMediaRecording(.send)
|
self?.interfaceInteraction?.finishMediaRecording(.send)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
self.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons)
|
self.clippingNode.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons)
|
||||||
}
|
}
|
||||||
self.actionButtons.isAccessibilityElement = false
|
self.actionButtons.isAccessibilityElement = false
|
||||||
let size: CGFloat = 120.0
|
let size: CGFloat = 120.0
|
||||||
@ -1475,7 +1475,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
contextPlaceholderNode.displaysAsynchronously = false
|
contextPlaceholderNode.displaysAsynchronously = false
|
||||||
contextPlaceholderNode.isUserInteractionEnabled = false
|
contextPlaceholderNode.isUserInteractionEnabled = false
|
||||||
self.contextPlaceholderNode = contextPlaceholderNode
|
self.contextPlaceholderNode = contextPlaceholderNode
|
||||||
self.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode)
|
self.clippingNode.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = placeholderApply()
|
let _ = placeholderApply()
|
||||||
@ -1495,7 +1495,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
|||||||
} else {
|
} else {
|
||||||
slowmodePlaceholderNode = ChatTextInputSlowmodePlaceholderNode(theme: interfaceState.theme)
|
slowmodePlaceholderNode = ChatTextInputSlowmodePlaceholderNode(theme: interfaceState.theme)
|
||||||
self.slowmodePlaceholderNode = slowmodePlaceholderNode
|
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))
|
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)
|
slowmodePlaceholderNode.updateState(slowmodeState)
|
||||||
|
|||||||
@ -71,7 +71,7 @@ private final class PrefetchManagerInnerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let popularEmoji = ["\u{2764}", "👍", "😳", "😒", "🥳"]
|
let popularEmoji = ["\u{2764}", "👍", "👎", "😳", "😒", "🥳", "😡", "😮", "😂", "😘", "😍", "🙄", "😎"]
|
||||||
for emoji in popularEmoji {
|
for emoji in popularEmoji {
|
||||||
if let sticker = animatedEmojiStickers[emoji] {
|
if let sticker = animatedEmojiStickers[emoji] {
|
||||||
if let _ = account.postbox.mediaBox.completedResourcePath(sticker.file.resource) {
|
if let _ = account.postbox.mediaBox.completedResourcePath(sticker.file.resource) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user