mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Cleanup
This commit is contained in:
parent
32bf3ad1ca
commit
fa93c6d791
@ -147,7 +147,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
private(set) var textInputPanelNode: ChatTextInputPanelNode?
|
||||
|
||||
private var inputMediaNode: ChatMediaInputNode?
|
||||
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
|
||||
private var inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
|
||||
private var didInitializeInputMediaNodeDataPromise: Bool = false
|
||||
@ -823,7 +822,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
func inFocusUpdated(isInFocus: Bool) {
|
||||
self.isInFocus = isInFocus
|
||||
|
||||
self.inputMediaNode?.simulateUpdateLayout(isVisible: isInFocus)
|
||||
if let inputNode = self.inputNode as? ChatEntityKeyboardInputNode {
|
||||
inputNode.simulateUpdateLayout(isVisible: isInFocus)
|
||||
}
|
||||
@ -1180,7 +1178,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
previewing = false
|
||||
}
|
||||
|
||||
let inputNodeForState = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, inputMediaNode: self.inputMediaNode, controllerInteraction: self.controllerInteraction, inputPanelNode: self.inputPanelNode, makeMediaInputNode: {
|
||||
let inputNodeForState = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, controllerInteraction: self.controllerInteraction, inputPanelNode: self.inputPanelNode, makeMediaInputNode: {
|
||||
return self.makeMediaInputNode()
|
||||
})
|
||||
|
||||
@ -1335,19 +1333,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
var immediatelyLayoutInputNodeAndAnimateAppearance = false
|
||||
var inputNodeHeightAndOverflow: (CGFloat, CGFloat)?
|
||||
if let inputNode = inputNodeForState {
|
||||
if self.inputMediaNode != nil {
|
||||
if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode {
|
||||
if inputPanelNode.isFocused {
|
||||
self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
if let inputMediaNode = inputNode as? ChatMediaInputNode, self.inputMediaNode == nil {
|
||||
self.inputMediaNode = inputMediaNode
|
||||
inputMediaNode.requestDisableStickerAnimations = { [weak self] disabled in
|
||||
self?.controller?.disableStickerAnimations = disabled
|
||||
}
|
||||
}
|
||||
if self.inputNode != inputNode {
|
||||
inputNode.topBackgroundExtensionUpdated = { [weak self] transition in
|
||||
self?.updateInputPanelBackgroundExtension(transition: transition)
|
||||
@ -1377,15 +1362,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
inputNode.layer.removeAnimation(forKey: "opacity")
|
||||
immediatelyLayoutInputNodeAndAnimateAppearance = true
|
||||
|
||||
if self.inputMediaNode != nil {
|
||||
if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil {
|
||||
self.inputPanelClippingNode.insertSubnode(inputNode, belowSubnode: inputPanelNode)
|
||||
} else {
|
||||
self.inputPanelClippingNode.insertSubnode(inputNode, belowSubnode: self.inputPanelBackgroundNode)
|
||||
}
|
||||
} else {
|
||||
self.inputPanelClippingNode.insertSubnode(inputNode, belowSubnode: self.inputPanelBackgroundNode)
|
||||
}
|
||||
self.inputPanelClippingNode.insertSubnode(inputNode, belowSubnode: self.inputPanelBackgroundNode)
|
||||
|
||||
if let externalTopPanelContainer = inputNode.externalTopPanelContainer {
|
||||
if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil {
|
||||
@ -1452,10 +1429,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
self.historyNode.isSelectionGestureEnabled = isSelectionEnabled
|
||||
|
||||
if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode {
|
||||
let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, layoutMetrics: layout.metrics, deviceMetrics: layout.deviceMetrics, isVisible: false, isExpanded: self.inputPanelContainerNode.stableIsExpanded)
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 200.0)))
|
||||
|
||||
transition.updateFrame(node: self.inputContextPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)))
|
||||
@ -2673,33 +2646,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.inputMediaNodeDataPromise.set(ChatEntityKeyboardInputNode.inputData(context: self.context, interfaceInteraction: interfaceInteraction, controllerInteraction: self.controllerInteraction, chatPeerId: self.chatLocation.peerId, areCustomEmojiEnabled: areCustomEmojiEnabled))
|
||||
}
|
||||
|
||||
if self.inputMediaNode == nil && !"".isEmpty {
|
||||
let peerId: PeerId? = self.chatPresentationInterfaceState.chatLocation.peerId
|
||||
let inputNode = ChatMediaInputNode(context: self.context, peerId: peerId, chatLocation: self.chatPresentationInterfaceState.chatLocation, controllerInteraction: self.controllerInteraction, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, theme: theme, strings: strings, fontSize: fontSize, gifPaneIsActiveUpdated: { [weak self] value in
|
||||
if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction {
|
||||
interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in
|
||||
if case let .media(_, expanded, focused) = state.inputMode {
|
||||
if value {
|
||||
return (.media(mode: .gif, expanded: expanded, focused: focused), nil)
|
||||
} else {
|
||||
return (.media(mode: .other, expanded: expanded, focused: focused), nil)
|
||||
}
|
||||
} else {
|
||||
return (state.inputMode, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
inputNode.interfaceInteraction = interfaceInteraction
|
||||
inputNode.requestDisableStickerAnimations = { [weak self] disabled in
|
||||
self?.controller?.disableStickerAnimations = disabled
|
||||
}
|
||||
self.inputMediaNode = inputNode
|
||||
if let (validLayout, _) = self.validLayout {
|
||||
let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, inputHeight: validLayout.inputHeight ?? 0.0, maximumHeight: validLayout.standardInputHeight, inputPanelHeight: 44.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, layoutMetrics: validLayout.metrics, deviceMetrics: validLayout.deviceMetrics, isVisible: false, isExpanded: self.inputPanelContainerNode.stableIsExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
self.textInputPanelNode?.loadTextInputNodeIfNeeded()
|
||||
}
|
||||
|
||||
@ -2933,12 +2879,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let inputMediaNode = self.inputMediaNode, self.inputNode === inputMediaNode {
|
||||
let convertedPoint = self.view.convert(point, to: inputMediaNode.view)
|
||||
if inputMediaNode.point(inside: convertedPoint, with: event) {
|
||||
return inputMediaNode.hitTest(convertedPoint, with: event)
|
||||
}
|
||||
}
|
||||
switch self.chatPresentationInterfaceState.mode {
|
||||
case .standard(previewing: true):
|
||||
if let result = self.historyNode.view.hitTest(self.view.convert(point, to: self.historyNode.view), with: event), let node = result.asyncdisplaykit_node, node is ChatMessageSelectionNode || node is GridMessageSelectionNode {
|
||||
@ -3130,31 +3070,18 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
func openStickers(beginWithEmoji: Bool) {
|
||||
self.openStickersBeginWithEmoji = beginWithEmoji
|
||||
|
||||
if let inputMediaNode = self.inputMediaNode {
|
||||
if self.openStickersDisposable == nil {
|
||||
self.openStickersDisposable = (inputMediaNode.ready
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] in
|
||||
self?.openStickersDisposable = nil
|
||||
self?.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
||||
return (.media(mode: .other, expanded: nil, focused: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
|
||||
})
|
||||
if self.openStickersDisposable == nil {
|
||||
self.openStickersDisposable = (self.inputMediaNodeDataPromise.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
||||
return (.media(mode: .other, expanded: nil, focused: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if self.openStickersDisposable == nil {
|
||||
self.openStickersDisposable = (self.inputMediaNodeDataPromise.get()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in
|
||||
return (.media(mode: .other, expanded: nil, focused: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import ChatControllerInteraction
|
||||
import ChatInputNode
|
||||
import ChatEntityKeyboardInputNode
|
||||
|
||||
func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, inputMediaNode: ChatMediaInputNode?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?, makeMediaInputNode: () -> ChatInputNode?) -> ChatInputNode? {
|
||||
func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?, makeMediaInputNode: () -> ChatInputNode?) -> ChatInputNode? {
|
||||
if let inputPanelNode = inputPanelNode, !(inputPanelNode is ChatTextInputPanelNode) {
|
||||
return nil
|
||||
}
|
||||
@ -17,8 +17,6 @@ func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState:
|
||||
case .media:
|
||||
if let currentNode = currentNode as? ChatEntityKeyboardInputNode {
|
||||
return currentNode
|
||||
} else if let inputMediaNode = inputMediaNode {
|
||||
return inputMediaNode
|
||||
} else if let inputMediaNode = makeMediaInputNode() {
|
||||
inputMediaNode.interfaceInteraction = interfaceInteraction
|
||||
return inputMediaNode
|
||||
|
@ -1,399 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ContextUI
|
||||
import AccountContext
|
||||
import ChatPresentationInterfaceState
|
||||
import ChatControllerInteraction
|
||||
import MultiplexedVideoNode
|
||||
import FeaturedStickersScreen
|
||||
import ChatEntityKeyboardInputNode
|
||||
|
||||
private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) {
|
||||
let searchBarHeight: CGFloat = 56.0
|
||||
|
||||
let contentOffset = multiplexedNode.scrollNode.view.contentOffset.y
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
|
||||
|
||||
if contentOffset < 60.0 {
|
||||
if contentOffset < searchBarHeight * 0.6 {
|
||||
transition.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: CGRect(origin: CGPoint(), size: multiplexedNode.bounds.size))
|
||||
} else {
|
||||
transition.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 60.0), size: multiplexedNode.bounds.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
|
||||
private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void
|
||||
private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void
|
||||
private let openGifContextMenu: (MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void
|
||||
|
||||
private let searchPlaceholderNode: PaneSearchBarPlaceholderNode
|
||||
var visibleSearchPlaceholderNode: PaneSearchBarPlaceholderNode? {
|
||||
guard let scrollNode = multiplexedNode?.scrollNode else {
|
||||
return nil
|
||||
}
|
||||
if scrollNode.bounds.contains(self.searchPlaceholderNode.frame) {
|
||||
return self.searchPlaceholderNode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private var multiplexedNode: MultiplexedVideoNode?
|
||||
private let emptyNode: ImmediateTextNode
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
let trendingPromise = Promise<ChatMediaInputGifPaneTrendingState?>(nil)
|
||||
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat, Bool, Bool, DeviceMetrics)?
|
||||
private var didScrollPreviousOffset: CGFloat?
|
||||
|
||||
private var didScrollPreviousState: ChatMediaInputPaneScrollState?
|
||||
|
||||
private(set) var mode: ChatMediaInputGifMode = .recent
|
||||
private var isLoadingMore: Bool = false
|
||||
private var nextOffset: String?
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void, openGifContextMenu: @escaping (MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.paneDidScroll = paneDidScroll
|
||||
self.fixPaneScroll = fixPaneScroll
|
||||
self.openGifContextMenu = openGifContextMenu
|
||||
|
||||
self.searchPlaceholderNode = PaneSearchBarPlaceholderNode()
|
||||
|
||||
self.emptyNode = ImmediateTextNode()
|
||||
self.emptyNode.isUserInteractionEnabled = false
|
||||
self.emptyNode.attributedText = NSAttributedString(string: strings.Gif_NoGifsPlaceholder, font: Font.regular(15.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
|
||||
self.emptyNode.textAlignment = .center
|
||||
self.emptyNode.maximumNumberOfLines = 3
|
||||
self.emptyNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.emptyNode)
|
||||
|
||||
self.searchPlaceholderNode.activate = { [weak self] in
|
||||
self?.inputNodeInteraction?.toggleSearch(true, .gif, "")
|
||||
}
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
self.emptyNode.attributedText = NSAttributedString(string: strings.Gif_NoGifsPlaceholder, font: Font.regular(15.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
|
||||
|
||||
self.searchPlaceholderNode.setup(theme: theme, strings: strings, type: .gifs)
|
||||
|
||||
if let layout = self.validLayout {
|
||||
self.updateLayout(size: layout.0, topInset: layout.1, bottomInset: layout.2, isExpanded: layout.3, isVisible: layout.4, deviceMetrics: layout.5, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
|
||||
var changedIsExpanded = false
|
||||
if let (_, _, _, previousIsExpanded, _, _) = self.validLayout {
|
||||
if previousIsExpanded != isExpanded {
|
||||
changedIsExpanded = true
|
||||
}
|
||||
}
|
||||
self.validLayout = (size, topInset, bottomInset, isExpanded, isVisible, deviceMetrics)
|
||||
|
||||
let emptySize = self.emptyNode.updateLayout(size)
|
||||
transition.updateFrame(node: self.emptyNode, frame: CGRect(origin: CGPoint(x: floor(size.width - emptySize.width) / 2.0, y: topInset + floor(size.height - topInset - emptySize.height) / 2.0), size: emptySize))
|
||||
|
||||
self.updateMultiplexedNodeLayout(changedIsExpanded: changedIsExpanded, transition: transition)
|
||||
}
|
||||
|
||||
func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? {
|
||||
if let multiplexedNode = self.multiplexedNode {
|
||||
return multiplexedNode.fileAt(point: point.offsetBy(dx: -multiplexedNode.frame.minX, dy: -multiplexedNode.frame.minY))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func setMode(mode: ChatMediaInputGifMode) {
|
||||
if self.mode == mode {
|
||||
if let multiplexedNode = self.multiplexedNode {
|
||||
multiplexedNode.scrollNode.view.setContentOffset(CGPoint(), animated: true)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.mode = mode
|
||||
self.resetMode(synchronous: true, searchOffset: nil)
|
||||
}
|
||||
|
||||
override var isEmpty: Bool {
|
||||
if let files = self.multiplexedNode?.files {
|
||||
return files.trending.isEmpty && files.saved.isEmpty
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override func willEnterHierarchy() {
|
||||
super.willEnterHierarchy()
|
||||
|
||||
self.initializeIfNeeded()
|
||||
}
|
||||
|
||||
private func updateMultiplexedNodeLayout(changedIsExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
||||
guard let (size, topInset, bottomInset, _, _, deviceMetrics) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
if let multiplexedNode = self.multiplexedNode {
|
||||
let _ = multiplexedNode.scrollNode.layer.bounds
|
||||
|
||||
let displaySearch: Bool
|
||||
|
||||
switch self.mode {
|
||||
case .recent, .trending:
|
||||
displaySearch = true
|
||||
default:
|
||||
displaySearch = false
|
||||
}
|
||||
|
||||
multiplexedNode.topInset = topInset + (displaySearch ? 60.0 : 0.0)
|
||||
multiplexedNode.bottomInset = bottomInset
|
||||
|
||||
if case .tablet = deviceMetrics.type, size.width > 480.0 {
|
||||
multiplexedNode.idealHeight = 120.0
|
||||
} else {
|
||||
multiplexedNode.idealHeight = 93.0
|
||||
}
|
||||
|
||||
let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||
|
||||
/*var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size)
|
||||
if changedIsExpanded {
|
||||
let isEmpty = multiplexedNode.files.trending.isEmpty && multiplexedNode.files.saved.isEmpty
|
||||
//targetBounds.origin.y = isExpanded || isEmpty ? 0.0 : 60.0
|
||||
}*/
|
||||
|
||||
//transition.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: targetBounds)
|
||||
transition.updateFrame(node: multiplexedNode, frame: nodeFrame)
|
||||
|
||||
multiplexedNode.updateLayout(theme: self.theme, strings: self.strings, size: nodeFrame.size, transition: transition)
|
||||
self.searchPlaceholderNode.frame = CGRect(x: 0.0, y: 41.0, width: size.width, height: 56.0)
|
||||
}
|
||||
}
|
||||
|
||||
func initializeIfNeeded() {
|
||||
if self.multiplexedNode == nil {
|
||||
self.trendingPromise.set(paneGifSearchForQuery(context: self.context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil)
|
||||
|> map { items -> ChatMediaInputGifPaneTrendingState? in
|
||||
if let items = items {
|
||||
return ChatMediaInputGifPaneTrendingState(files: items.files, nextOffset: items.nextOffset)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
let multiplexedNode = MultiplexedVideoNode(account: self.context.account, theme: self.theme, strings: self.strings)
|
||||
self.multiplexedNode = multiplexedNode
|
||||
if let layout = self.validLayout {
|
||||
multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout.0)
|
||||
}
|
||||
|
||||
multiplexedNode.reactionSelected = { [weak self] reaction in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.inputNodeInteraction?.toggleSearch(true, .gif, reaction)
|
||||
}
|
||||
|
||||
self.addSubnode(multiplexedNode)
|
||||
multiplexedNode.scrollNode.addSubnode(self.searchPlaceholderNode)
|
||||
|
||||
multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in
|
||||
if let (collection, result) = file.contextResult {
|
||||
let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode.view, sourceRect, false)
|
||||
} else {
|
||||
let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, false)
|
||||
}
|
||||
}
|
||||
|
||||
multiplexedNode.fileContextMenu = { [weak self] fileReference, sourceNode, sourceRect, gesture, isSaved in
|
||||
self?.openGifContextMenu(fileReference, sourceNode, sourceRect, gesture, isSaved)
|
||||
}
|
||||
|
||||
multiplexedNode.didScroll = { [weak self] offset, height in
|
||||
guard let strongSelf = self, let multiplexedNode = strongSelf.multiplexedNode else {
|
||||
return
|
||||
}
|
||||
let absoluteOffset = -offset + 60.0
|
||||
var delta: CGFloat = 0.0
|
||||
if let didScrollPreviousOffset = strongSelf.didScrollPreviousOffset {
|
||||
delta = absoluteOffset - didScrollPreviousOffset
|
||||
}
|
||||
strongSelf.didScrollPreviousOffset = absoluteOffset
|
||||
let state = ChatMediaInputPaneScrollState(absoluteOffset: absoluteOffset, relativeChange: delta)
|
||||
strongSelf.didScrollPreviousState = state
|
||||
strongSelf.paneDidScroll(strongSelf, state, .immediate)
|
||||
|
||||
if offset >= height - multiplexedNode.bounds.height - 200.0 {
|
||||
strongSelf.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
multiplexedNode.didEndScrolling = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let didScrollPreviousState = strongSelf.didScrollPreviousState {
|
||||
strongSelf.fixPaneScroll(strongSelf, didScrollPreviousState)
|
||||
}
|
||||
|
||||
if let _ = strongSelf.multiplexedNode {
|
||||
//fixListScrolling(multiplexedNode)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateMultiplexedNodeLayout(changedIsExpanded: false, transition: .immediate)
|
||||
|
||||
self.resetMode(synchronous: false, searchOffset: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func resetMode(synchronous: Bool, searchOffset: String?) {
|
||||
self.isLoadingMore = true
|
||||
|
||||
let filesSignal: Signal<(MultiplexedVideoNodeFiles, String?), NoError>
|
||||
switch self.mode {
|
||||
case .recent:
|
||||
filesSignal = combineLatest(
|
||||
self.trendingPromise.get(),
|
||||
self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|
||||
)
|
||||
|> map { trending, cloudRecentGifs -> (MultiplexedVideoNodeFiles, String?) in
|
||||
var saved: [MultiplexedVideoNodeFile] = []
|
||||
|
||||
saved = cloudRecentGifs.map { item in
|
||||
let file = item.contents.get(RecentMediaItem.self)!.media
|
||||
return MultiplexedVideoNodeFile(file: .savedGif(media: file), contextResult: nil)
|
||||
}
|
||||
|
||||
return (MultiplexedVideoNodeFiles(saved: saved, trending: trending?.files ?? [], isSearch: false, canLoadMore: false, isStale: false), nil)
|
||||
}
|
||||
case .trending:
|
||||
if let searchOffset = searchOffset {
|
||||
filesSignal = paneGifSearchForQuery(context: self.context, query: "", offset: searchOffset, incompleteResults: true, delayRequest: false, updateActivity: nil)
|
||||
|> map { result -> (MultiplexedVideoNodeFiles, String?) in
|
||||
let canLoadMore: Bool
|
||||
if let result = result {
|
||||
canLoadMore = !result.isComplete
|
||||
} else {
|
||||
canLoadMore = true
|
||||
}
|
||||
return (MultiplexedVideoNodeFiles(saved: [], trending: result?.files ?? [], isSearch: true, canLoadMore: canLoadMore, isStale: false), result?.nextOffset)
|
||||
}
|
||||
} else {
|
||||
filesSignal = self.trendingPromise.get()
|
||||
|> map { trending -> (MultiplexedVideoNodeFiles, String?) in
|
||||
return (MultiplexedVideoNodeFiles(saved: [], trending: trending?.files ?? [], isSearch: true, canLoadMore: false, isStale: false), trending?.nextOffset)
|
||||
}
|
||||
}
|
||||
case let .emojiSearch(emoji):
|
||||
filesSignal = paneGifSearchForQuery(context: self.context, query: emoji, offset: searchOffset, incompleteResults: true, staleCachedResults: searchOffset == nil, delayRequest: false, updateActivity: nil)
|
||||
|> map { result -> (MultiplexedVideoNodeFiles, String?) in
|
||||
let canLoadMore: Bool
|
||||
if let result = result {
|
||||
canLoadMore = !result.isComplete
|
||||
} else {
|
||||
canLoadMore = true
|
||||
}
|
||||
return (MultiplexedVideoNodeFiles(saved: [], trending: result?.files ?? [], isSearch: true, canLoadMore: canLoadMore, isStale: result?.isStale ?? false), result?.nextOffset)
|
||||
}
|
||||
}
|
||||
|
||||
var firstTime = true
|
||||
|
||||
self.disposable.set((filesSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] addedFiles, nextOffset in
|
||||
if let strongSelf = self {
|
||||
var resetScrollingToOffset: CGFloat?
|
||||
if firstTime {
|
||||
firstTime = false
|
||||
if searchOffset == nil {
|
||||
resetScrollingToOffset = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.isLoadingMore = false
|
||||
|
||||
let displaySearch: Bool
|
||||
|
||||
switch strongSelf.mode {
|
||||
case .recent, .trending:
|
||||
displaySearch = true
|
||||
default:
|
||||
displaySearch = false
|
||||
}
|
||||
|
||||
strongSelf.searchPlaceholderNode.isHidden = !displaySearch
|
||||
|
||||
if let (_, topInset, _, _, _, _) = strongSelf.validLayout {
|
||||
strongSelf.multiplexedNode?.topInset = topInset + (displaySearch ? 60.0 : 0.0)
|
||||
}
|
||||
|
||||
var files = addedFiles
|
||||
if let _ = searchOffset {
|
||||
var resultFiles: [MultiplexedVideoNodeFile] = []
|
||||
if let currentFiles = strongSelf.multiplexedNode?.files.trending {
|
||||
resultFiles = currentFiles
|
||||
}
|
||||
var existingFileIds = Set(resultFiles.map { $0.file.media.fileId })
|
||||
for file in addedFiles.trending {
|
||||
if existingFileIds.contains(file.file.media.fileId) {
|
||||
continue
|
||||
}
|
||||
existingFileIds.insert(file.file.media.fileId)
|
||||
resultFiles.append(file)
|
||||
}
|
||||
files = MultiplexedVideoNodeFiles(saved: [], trending: resultFiles, isSearch: true, canLoadMore: addedFiles.canLoadMore, isStale: addedFiles.isStale)
|
||||
}
|
||||
|
||||
strongSelf.nextOffset = nextOffset
|
||||
strongSelf.multiplexedNode?.setFiles(files: files, synchronous: synchronous, resetScrollingToOffset: resetScrollingToOffset)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
private func loadMore() {
|
||||
if self.isLoadingMore {
|
||||
return
|
||||
}
|
||||
guard let nextOffset = self.nextOffset else {
|
||||
return
|
||||
}
|
||||
switch self.mode {
|
||||
case .trending, .emojiSearch:
|
||||
self.resetMode(synchronous: false, searchOffset: nextOffset)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
@ -1,234 +0,0 @@
|
||||
import Postbox
|
||||
import UIKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import MergeLists
|
||||
import ChatPresentationInterfaceState
|
||||
import ChatControllerInteraction
|
||||
import FeaturedStickersScreen
|
||||
|
||||
enum ChatMediaInputGridEntryStableId: Equatable, Hashable {
|
||||
case search
|
||||
case trendingList
|
||||
case peerSpecificSetup
|
||||
case sticker(ItemCollectionId, ItemCollectionItemIndex.Id)
|
||||
case trending(ItemCollectionId)
|
||||
}
|
||||
|
||||
enum ChatMediaInputGridEntryIndex: Equatable, Comparable {
|
||||
case search
|
||||
case trendingList
|
||||
case peerSpecificSetup(dismissed: Bool)
|
||||
case collectionIndex(ItemCollectionViewEntryIndex)
|
||||
case trending(ItemCollectionId, Int)
|
||||
|
||||
var stableId: ChatMediaInputGridEntryStableId {
|
||||
switch self {
|
||||
case .search:
|
||||
return .search
|
||||
case .trendingList:
|
||||
return .trendingList
|
||||
case .peerSpecificSetup:
|
||||
return .peerSpecificSetup
|
||||
case let .collectionIndex(index):
|
||||
return .sticker(index.collectionId, index.itemIndex.id)
|
||||
case let .trending(id, _):
|
||||
return .trending(id)
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: ChatMediaInputGridEntryIndex, rhs: ChatMediaInputGridEntryIndex) -> Bool {
|
||||
switch lhs {
|
||||
case .search:
|
||||
if case .search = rhs {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
case .trendingList:
|
||||
switch rhs {
|
||||
case .search, .trendingList:
|
||||
return false
|
||||
case .peerSpecificSetup, .collectionIndex, .trending:
|
||||
return true
|
||||
}
|
||||
case let .peerSpecificSetup(lhsDismissed):
|
||||
switch rhs {
|
||||
case .search, .trendingList, .peerSpecificSetup:
|
||||
return false
|
||||
case let .collectionIndex(index):
|
||||
if lhsDismissed {
|
||||
return false
|
||||
} else {
|
||||
if index.collectionId.id == 0 {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case .trending:
|
||||
return true
|
||||
}
|
||||
case let .collectionIndex(lhsIndex):
|
||||
switch rhs {
|
||||
case .search, .trendingList:
|
||||
return false
|
||||
case let .peerSpecificSetup(dismissed):
|
||||
if dismissed {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .collectionIndex(rhsIndex):
|
||||
return lhsIndex < rhsIndex
|
||||
case .trending:
|
||||
return true
|
||||
}
|
||||
case let .trending(_, lhsIndex):
|
||||
switch rhs {
|
||||
case .search, .trendingList, .peerSpecificSetup, .collectionIndex:
|
||||
return false
|
||||
case let .trending(_, rhsIndex):
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable {
|
||||
case search(theme: PresentationTheme, strings: PresentationStrings)
|
||||
case trendingList(theme: PresentationTheme, strings: PresentationStrings, packs: [FeaturedStickerPackItem], isPremium: Bool)
|
||||
case peerSpecificSetup(theme: PresentationTheme, strings: PresentationStrings, dismissed: Bool)
|
||||
case sticker(index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, stickerPackInfo: StickerPackCollectionInfo?, canManagePeerSpecificPack: Bool?, maybeManageable: Bool, theme: PresentationTheme, isLocked: Bool)
|
||||
case trending(TrendingPanePackEntry)
|
||||
|
||||
var index: ChatMediaInputGridEntryIndex {
|
||||
switch self {
|
||||
case .search:
|
||||
return .search
|
||||
case .trendingList:
|
||||
return .trendingList
|
||||
case let .peerSpecificSetup(_, _, dismissed):
|
||||
return .peerSpecificSetup(dismissed: dismissed)
|
||||
case let .sticker(index, _, _, _, _, _, _):
|
||||
return .collectionIndex(index)
|
||||
case let .trending(entry):
|
||||
return .trending(entry.info.id, entry.index)
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: ChatMediaInputGridEntryStableId {
|
||||
return self.index.stableId
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .search(lhsTheme, lhsStrings):
|
||||
if case let .search(rhsTheme, rhsStrings) = rhs {
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
if lhsStrings !== rhsStrings {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .trendingList(lhsTheme, lhsStrings, lhsPacks, lhsIsPremium):
|
||||
if case let .trendingList(rhsTheme, rhsStrings, rhsPacks, rhsIsPremium) = rhs {
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
if lhsStrings !== rhsStrings {
|
||||
return false
|
||||
}
|
||||
if lhsPacks.count != rhsPacks.count {
|
||||
return false
|
||||
}
|
||||
for i in 0 ..< lhsPacks.count {
|
||||
if lhsPacks[i].unread != rhsPacks[i].unread {
|
||||
return false
|
||||
}
|
||||
if lhsPacks[i].info != rhsPacks[i].info {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if lhsIsPremium != rhsIsPremium {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .peerSpecificSetup(lhsTheme, lhsStrings, lhsDismissed):
|
||||
if case let .peerSpecificSetup(rhsTheme, rhsStrings, rhsDismissed) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDismissed == rhsDismissed {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .sticker(lhsIndex, lhsStickerItem, lhsStickerPackInfo, lhsCanManagePeerSpecificPack, lhsMaybeManageable, lhsTheme, lhsIsLocked):
|
||||
if case let .sticker(rhsIndex, rhsStickerItem, rhsStickerPackInfo, rhsCanManagePeerSpecificPack, rhsMaybeManageable, rhsTheme, rhsIsLocked) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if lhsStickerItem != rhsStickerItem {
|
||||
return false
|
||||
}
|
||||
if lhsStickerPackInfo != rhsStickerPackInfo {
|
||||
return false
|
||||
}
|
||||
if lhsCanManagePeerSpecificPack != rhsCanManagePeerSpecificPack {
|
||||
return false
|
||||
}
|
||||
if lhsMaybeManageable != rhsMaybeManageable {
|
||||
return false
|
||||
}
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
if lhsIsLocked != rhsIsLocked {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .trending(entry):
|
||||
if case .trending(entry) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(account: Account, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingInteraction: TrendingPaneInteraction) -> GridItem {
|
||||
switch self {
|
||||
case let .search(theme, strings):
|
||||
return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: {
|
||||
inputNodeInteraction.toggleSearch(true, .sticker, "")
|
||||
})
|
||||
case let .trendingList(theme, strings, packs, isPremium):
|
||||
return StickerPaneTrendingListGridItem(account: account, theme: theme, strings: strings, trendingPacks: packs, isPremium: isPremium, inputNodeInteraction: inputNodeInteraction, dismiss: {
|
||||
inputNodeInteraction.dismissTrendingPacks(packs.map { $0.info.id })
|
||||
})
|
||||
case let .peerSpecificSetup(theme, strings, dismissed):
|
||||
return StickerPanePeerSpecificSetupGridItem(theme: theme, strings: strings, setup: {
|
||||
inputNodeInteraction.openPeerSpecificSettings()
|
||||
}, dismiss: dismissed ? nil : {
|
||||
inputNodeInteraction.dismissPeerSpecificSettings()
|
||||
})
|
||||
case let .sticker(index, stickerItem, stickerPackInfo, canManagePeerSpecificPack, maybeManageable, theme, isLocked):
|
||||
return ChatMediaInputStickerGridItem(account: account, collectionId: index.collectionId, stickerPackInfo: stickerPackInfo, index: index, stickerItem: stickerItem, canManagePeerSpecificPack: canManagePeerSpecificPack, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, hasAccessory: maybeManageable, theme: theme, isLocked: isLocked, selected: { })
|
||||
case let .trending(entry):
|
||||
return entry.item(account: account, interaction: trendingInteraction, grid: false)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,342 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
enum ChatMediaInputMetaSectionItemType: Equatable {
|
||||
case savedStickers
|
||||
case recentStickers
|
||||
case stickersMode
|
||||
case savedGifs
|
||||
case trendingGifs
|
||||
case premium
|
||||
case gifEmoji(String, TelegramMediaFile?)
|
||||
}
|
||||
|
||||
final class ChatMediaInputMetaSectionItem: ListViewItem {
|
||||
let account: Account
|
||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
let type: ChatMediaInputMetaSectionItemType
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let expanded: Bool
|
||||
let selectedItem: () -> Void
|
||||
|
||||
var selectable: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, strings: PresentationStrings, expanded: Bool, selected: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.type = type
|
||||
self.selectedItem = selected
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.expanded = expanded
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatMediaInputMetaSectionItemNode()
|
||||
Queue.mainQueue().async {
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
node.setItem(item: self)
|
||||
node.updateTheme(account: self.account, theme: self.theme, strings: self.strings, expanded: self.expanded)
|
||||
node.updateIsHighlighted()
|
||||
node.updateAppearanceTransition(transition: .immediate)
|
||||
|
||||
node.contentSize = self.expanded ? expandedBoundingSize : boundingSize
|
||||
node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)
|
||||
|
||||
completion(node, {
|
||||
return (nil, { _ in
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: node().insets), { _ in
|
||||
(node() as? ChatMediaInputMetaSectionItemNode)?.setItem(item: self)
|
||||
(node() as? ChatMediaInputMetaSectionItemNode)?.updateTheme(account: self.account, theme: self.theme, strings: self.strings, expanded: self.expanded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
self.selectedItem()
|
||||
}
|
||||
}
|
||||
|
||||
private let boundingSize = CGSize(width: 72.0, height: 41.0)
|
||||
private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0)
|
||||
private let boundingImageScale: CGFloat = 0.625
|
||||
private let highlightSize = CGSize(width: 56.0, height: 56.0)
|
||||
private let verticalOffset: CGFloat = 3.0 + UIScreenPixel
|
||||
|
||||
final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let scalingNode: ASDisplayNode
|
||||
private let imageNode: ASImageNode
|
||||
private let textNodeContainer: ASDisplayNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let highlightNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
private var animatedStickerNode: AnimatedStickerNode?
|
||||
|
||||
private var currentExpanded = false
|
||||
|
||||
var item: ChatMediaInputMetaSectionItem?
|
||||
var currentCollectionId: ItemCollectionId?
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
|
||||
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)
|
||||
|
||||
self.scalingNode = ASDisplayNode()
|
||||
|
||||
self.highlightNode = ASImageNode()
|
||||
self.highlightNode.isLayerBacked = true
|
||||
self.highlightNode.isHidden = true
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isLayerBacked = true
|
||||
self.imageNode.contentMode = .center
|
||||
|
||||
self.textNodeContainer = ASDisplayNode()
|
||||
self.textNodeContainer.isUserInteractionEnabled = false
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.textNodeContainer.addSubnode(self.textNode)
|
||||
self.textNodeContainer.isUserInteractionEnabled = false
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
self.containerNode.addSubnode(self.scalingNode)
|
||||
|
||||
self.scalingNode.addSubnode(self.highlightNode)
|
||||
self.scalingNode.addSubnode(self.titleNode)
|
||||
self.scalingNode.addSubnode(self.imageNode)
|
||||
self.scalingNode.addSubnode(self.textNodeContainer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
}
|
||||
|
||||
func setItem(item: ChatMediaInputMetaSectionItem) {
|
||||
self.item = item
|
||||
switch item.type {
|
||||
case .savedStickers:
|
||||
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0)
|
||||
case .recentStickers:
|
||||
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0)
|
||||
case .premium:
|
||||
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.premium.rawValue, id: 0)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func updateTheme(account: Account, theme: PresentationTheme, strings: PresentationStrings, expanded: Bool) {
|
||||
let imageSize = CGSize(width: 44.0, height: 42.0)
|
||||
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.textNodeContainer.frame = CGRect(origin: CGPoint(x: floor((expandedBoundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((expandedBoundingSize.height - imageSize.height) / 2.0) + 1.0), size: imageSize)
|
||||
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
|
||||
self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme)
|
||||
var title = ""
|
||||
if let item = self.item {
|
||||
switch item.type {
|
||||
case .savedStickers:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSavedStickersIcon(theme)
|
||||
title = strings.Stickers_Favorites
|
||||
case .recentStickers:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme)
|
||||
title = strings.Stickers_Recent
|
||||
case .stickersMode:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelStickersModeIcon(theme)
|
||||
title = strings.Stickers_Stickers
|
||||
case .savedGifs:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme)
|
||||
title = strings.Stickers_Gifs
|
||||
case .trendingGifs:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingGifsIcon(theme)
|
||||
title = strings.Stickers_Trending
|
||||
case .premium:
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelPremiumIcon(theme)
|
||||
title = strings.Stickers_PremiumStickers
|
||||
case let .gifEmoji(emoji, file):
|
||||
switch emoji {
|
||||
case "😡":
|
||||
title = strings.Gif_Emotion_Angry
|
||||
case "😮":
|
||||
title = strings.Gif_Emotion_Surprised
|
||||
case "😂":
|
||||
title = strings.Gif_Emotion_Joy
|
||||
case "😘":
|
||||
title = strings.Gif_Emotion_Kiss
|
||||
case "😍":
|
||||
title = strings.Gif_Emotion_Hearts
|
||||
case "👍":
|
||||
title = strings.Gif_Emotion_ThumbsUp
|
||||
case "👎":
|
||||
title = strings.Gif_Emotion_ThumbsDown
|
||||
case "🙄":
|
||||
title = strings.Gif_Emotion_RollEyes
|
||||
case "😎":
|
||||
title = strings.Gif_Emotion_Cool
|
||||
case "🥳":
|
||||
title = strings.Gif_Emotion_Party
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.imageNode.image = nil
|
||||
|
||||
if let file = file {
|
||||
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
|
||||
let animatedStickerNode: AnimatedStickerNode
|
||||
if let current = self.animatedStickerNode {
|
||||
animatedStickerNode = current
|
||||
} else {
|
||||
animatedStickerNode = DefaultAnimatedStickerNodeImpl()
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
self.scalingNode.addSubnode(animatedStickerNode)
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: file.resource), width: 128, height: 128, playbackMode: .loop, mode: .cached)
|
||||
}
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
|
||||
self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, 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.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize)
|
||||
self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize)
|
||||
|
||||
let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height)
|
||||
let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale
|
||||
let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate
|
||||
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 + 10.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)
|
||||
expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame)
|
||||
expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001)
|
||||
|
||||
let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate
|
||||
alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func updateIsHighlighted() {
|
||||
guard let inputNodeInteraction = self.inputNodeInteraction else {
|
||||
return
|
||||
}
|
||||
if let currentCollectionId = self.currentCollectionId {
|
||||
self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId
|
||||
} else if let item = self.item {
|
||||
var isHighlighted = false
|
||||
switch item.type {
|
||||
case .savedGifs:
|
||||
if case .recent = inputNodeInteraction.highlightedGifMode {
|
||||
isHighlighted = true
|
||||
}
|
||||
case .trendingGifs:
|
||||
if case .trending = inputNodeInteraction.highlightedGifMode {
|
||||
isHighlighted = true
|
||||
}
|
||||
case let .gifEmoji(emoji, _):
|
||||
if case .emojiSearch(emoji) = inputNodeInteraction.highlightedGifMode {
|
||||
isHighlighted = true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.highlightNode.isHidden = !isHighlighted
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppearanceTransition(transition: ContainedViewLayoutTransition) {
|
||||
if let inputNodeInteraction = self.inputNodeInteraction {
|
||||
transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition)
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
self.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,25 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
struct ChatMediaInputPaneScrollState {
|
||||
let absoluteOffset: CGFloat?
|
||||
let relativeChange: CGFloat
|
||||
}
|
||||
|
||||
class ChatMediaInputPane: ASDisplayNode {
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
var collectionListPanelOffset: CGFloat = 0.0
|
||||
var isEmpty: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
}
|
||||
}
|
@ -1,337 +0,0 @@
|
||||
import Postbox
|
||||
import UIKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import MergeLists
|
||||
import AccountContext
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
enum ChatMediaInputPanelAuxiliaryNamespace: Int32 {
|
||||
case savedStickers = 2
|
||||
case recentGifs = 3
|
||||
case recentStickers = 4
|
||||
case peerSpecific = 5
|
||||
case trending = 6
|
||||
case premium = 7
|
||||
case settings = 8
|
||||
}
|
||||
|
||||
enum ChatMediaInputPanelEntryStableId: Hashable {
|
||||
case recentGifs
|
||||
case savedStickers
|
||||
case recentPacks
|
||||
case stickerPack(Int64)
|
||||
case peerSpecific
|
||||
case trending
|
||||
case settings
|
||||
case stickersMode
|
||||
case savedGifs
|
||||
case trendingGifs
|
||||
case premium
|
||||
case gifEmotion(String)
|
||||
}
|
||||
|
||||
enum ChatMediaInputPanelEntry: Comparable, Identifiable {
|
||||
case recentGifs(PresentationTheme, PresentationStrings, Bool)
|
||||
case savedStickers(PresentationTheme, PresentationStrings, Bool)
|
||||
case recentPacks(PresentationTheme, PresentationStrings, Bool)
|
||||
case trending(Bool, PresentationTheme, PresentationStrings, Bool)
|
||||
case settings(PresentationTheme, PresentationStrings, Bool)
|
||||
case peerSpecific(theme: PresentationTheme, peer: Peer, expanded: Bool)
|
||||
case premium(PresentationTheme, PresentationStrings, Bool)
|
||||
case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, theme: PresentationTheme, expanded: Bool, reorderable: Bool)
|
||||
|
||||
case stickersMode(PresentationTheme, PresentationStrings, Bool)
|
||||
case savedGifs(PresentationTheme, PresentationStrings, Bool)
|
||||
case trendingGifs(PresentationTheme, PresentationStrings, Bool)
|
||||
case gifEmotion(Int, PresentationTheme, PresentationStrings, String, TelegramMediaFile?, Bool)
|
||||
|
||||
var stableId: ChatMediaInputPanelEntryStableId {
|
||||
switch self {
|
||||
case .recentGifs:
|
||||
return .recentGifs
|
||||
case .savedStickers:
|
||||
return .savedStickers
|
||||
case .recentPacks:
|
||||
return .recentPacks
|
||||
case .trending:
|
||||
return .trending
|
||||
case .settings:
|
||||
return .settings
|
||||
case .peerSpecific:
|
||||
return .peerSpecific
|
||||
case .premium:
|
||||
return .premium
|
||||
case let .stickerPack(_, info, _, _, _, _):
|
||||
return .stickerPack(info.id.id)
|
||||
case .stickersMode:
|
||||
return .stickersMode
|
||||
case .savedGifs:
|
||||
return .savedGifs
|
||||
case .trendingGifs:
|
||||
return .trendingGifs
|
||||
case let .gifEmotion(_, _, _, emoji, _, _):
|
||||
return .gifEmotion(emoji)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatMediaInputPanelEntry, rhs: ChatMediaInputPanelEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .recentGifs(lhsTheme, lhsStrings, lhsExpanded):
|
||||
if case let .recentGifs(rhsTheme, rhsStrings, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsExpanded == rhsExpanded {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .savedStickers(lhsTheme, lhsStrings, lhsExpanded):
|
||||
if case let .savedStickers(rhsTheme, rhsStrings, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsExpanded == rhsExpanded {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .recentPacks(lhsTheme, lhsStrings, lhsExpanded):
|
||||
if case let .recentPacks(rhsTheme, rhsStrings, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsExpanded == rhsExpanded {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .trending(lhsElevated, lhsTheme, lhsStrings, lhsExpanded):
|
||||
if case let .trending(rhsElevated, rhsTheme, rhsStrings, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsElevated == rhsElevated, lhsExpanded == rhsExpanded {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .settings(lhsTheme, lhsStrings, lhsExpanded):
|
||||
if case let .settings(rhsTheme, rhsStrings, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsExpanded == rhsExpanded {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .peerSpecific(lhsTheme, lhsPeer, lhsExpanded):
|
||||
if case let .peerSpecific(rhsTheme, rhsPeer, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsPeer.isEqual(rhsPeer), lhsExpanded == rhsExpanded {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .premium(lhsTheme, lhsStrings, lhsExpanded):
|
||||
if case let .premium(rhsTheme, rhsStrings, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsExpanded == rhsExpanded {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .stickerPack(index, info, topItem, lhsTheme, lhsExpanded, lhsReorderable):
|
||||
if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem, rhsTheme, rhsExpanded, rhsReorderable) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem, lhsTheme === rhsTheme, lhsExpanded == rhsExpanded, lhsReorderable == rhsReorderable {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .stickersMode(lhsTheme, lhsStrings, lhsExpanded):
|
||||
if case let .stickersMode(rhsTheme, rhsStrings, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsExpanded == rhsExpanded {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .savedGifs(lhsTheme, lhsStrings, lhsExpanded):
|
||||
if case let .savedGifs(rhsTheme, rhsStrings, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsExpanded == rhsExpanded {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .trendingGifs(lhsTheme, lhsStrings, lhsExpanded):
|
||||
if case let .trendingGifs(rhsTheme, rhsStrings, rhsExpanded) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsExpanded == rhsExpanded {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .gifEmotion(lhsIndex, lhsTheme, lhsStrings, lhsEmoji, lhsFile, lhsExpanded):
|
||||
if case let .gifEmotion(rhsIndex, rhsTheme, rhsStrings, rhsEmoji, rhsFile, rhsExpanded) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: ChatMediaInputPanelEntry, rhs: ChatMediaInputPanelEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .recentGifs:
|
||||
switch rhs {
|
||||
case .recentGifs:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .savedStickers:
|
||||
switch rhs {
|
||||
case .recentGifs, savedStickers:
|
||||
return false
|
||||
case let .trending(elevated, _, _, _) where elevated:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .recentPacks:
|
||||
switch rhs {
|
||||
case .recentGifs, .savedStickers, recentPacks:
|
||||
return false
|
||||
case let .trending(elevated, _, _, _) where elevated:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .peerSpecific:
|
||||
switch rhs {
|
||||
case .recentGifs, .savedStickers, recentPacks, .peerSpecific:
|
||||
return false
|
||||
case let .trending(elevated, _, _, _) where elevated:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .premium:
|
||||
switch rhs {
|
||||
case .recentGifs, .savedStickers, recentPacks, .peerSpecific, .premium:
|
||||
return false
|
||||
case let .trending(elevated, _, _, _) where elevated:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .stickerPack(lhsIndex, lhsInfo, _, _, _, _):
|
||||
switch rhs {
|
||||
case .recentGifs, .savedStickers, .recentPacks, .peerSpecific:
|
||||
return false
|
||||
case let .trending(elevated, _, _, _):
|
||||
if elevated {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
case .settings:
|
||||
return true
|
||||
case let .stickerPack(rhsIndex, rhsInfo, _, _, _, _):
|
||||
if lhsIndex == rhsIndex {
|
||||
return lhsInfo.id.id < rhsInfo.id.id
|
||||
} else {
|
||||
return lhsIndex <= rhsIndex
|
||||
}
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .trending(elevated, _, _, _):
|
||||
if elevated {
|
||||
switch rhs {
|
||||
case .recentGifs, .trending:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if case .settings = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
case .stickersMode:
|
||||
return false
|
||||
case .savedGifs:
|
||||
switch rhs {
|
||||
case .savedGifs:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .trendingGifs:
|
||||
switch rhs {
|
||||
case .stickersMode, .savedGifs, .trendingGifs:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .gifEmotion(lhsIndex, _, _, _, _, _):
|
||||
switch rhs {
|
||||
case .stickersMode, .savedGifs, .trendingGifs:
|
||||
return false
|
||||
case let .gifEmotion(rhsIndex, _, _, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case .settings:
|
||||
if case .settings = rhs {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(context: AccountContext, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ListViewItem {
|
||||
switch self {
|
||||
case let .recentGifs(theme, strings, expanded):
|
||||
return ChatMediaInputRecentGifsItem(inputNodeInteraction: inputNodeInteraction, theme: theme, strings: strings, expanded: expanded, selected: {
|
||||
let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue, id: 0)
|
||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||
})
|
||||
case let .savedStickers(theme, strings, expanded):
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .savedStickers, theme: theme, strings: strings, expanded: expanded, selected: {
|
||||
let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0)
|
||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||
})
|
||||
case let .recentPacks(theme, strings, expanded):
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .recentStickers, theme: theme, strings: strings, expanded: expanded, selected: {
|
||||
let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0)
|
||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||
})
|
||||
case let .trending(elevated, theme, strings, expanded):
|
||||
return ChatMediaInputTrendingItem(inputNodeInteraction: inputNodeInteraction, elevated: elevated, theme: theme, strings: strings, expanded: expanded, selected: {
|
||||
let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0)
|
||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||
})
|
||||
case let .settings(theme, strings, expanded):
|
||||
return ChatMediaInputSettingsItem(inputNodeInteraction: inputNodeInteraction, theme: theme, strings: strings, expanded: expanded, selected: {
|
||||
inputNodeInteraction.openSettings()
|
||||
})
|
||||
case let .peerSpecific(theme, peer, expanded):
|
||||
let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0)
|
||||
return ChatMediaInputPeerSpecificItem(context: context, inputNodeInteraction: inputNodeInteraction, collectionId: collectionId, peer: peer, theme: theme, expanded: expanded, selected: {
|
||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||
})
|
||||
case let .premium(theme, strings, expanded):
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .premium, theme: theme, strings: strings, expanded: expanded, selected: {
|
||||
let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.premium.rawValue, id: 0)
|
||||
inputNodeInteraction.navigateToCollectionId(collectionId)
|
||||
})
|
||||
case let .stickerPack(index, info, topItem, theme, expanded, reorderable):
|
||||
return ChatMediaInputStickerPackItem(account: context.account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, collectionInfo: info, stickerPackItem: topItem, index: index, theme: theme, expanded: expanded, reorderable: reorderable, selected: {
|
||||
inputNodeInteraction.navigateToCollectionId(info.id)
|
||||
})
|
||||
case let .stickersMode(theme, strings, expanded):
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .stickersMode, theme: theme, strings: strings, expanded: expanded, selected: {
|
||||
inputNodeInteraction.navigateBackToStickers()
|
||||
})
|
||||
case let .savedGifs(theme, strings, expanded):
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .savedGifs, theme: theme, strings: strings, expanded: expanded, selected: {
|
||||
inputNodeInteraction.setGifMode(.recent)
|
||||
})
|
||||
case let .trendingGifs(theme, strings, expanded):
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .trendingGifs, theme: theme, strings: strings, expanded: expanded, selected: {
|
||||
inputNodeInteraction.setGifMode(.trending)
|
||||
})
|
||||
case let .gifEmotion(_, theme, strings, emoji, file, expanded):
|
||||
return ChatMediaInputMetaSectionItem(account: context.account, inputNodeInteraction: inputNodeInteraction, type: .gifEmoji(emoji, file), theme: theme, strings: strings, expanded: expanded, selected: {
|
||||
inputNodeInteraction.setGifMode(.emojiSearch(emoji))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import AvatarNode
|
||||
import AccountContext
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
final class ChatMediaInputPeerSpecificItem: ListViewItem {
|
||||
let context: AccountContext
|
||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
let collectionId: ItemCollectionId
|
||||
let peer: Peer
|
||||
let expanded: Bool
|
||||
let selectedItem: () -> Void
|
||||
let theme: PresentationTheme
|
||||
|
||||
var selectable: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(context: AccountContext, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, peer: Peer, theme: PresentationTheme, expanded: Bool, selected: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.collectionId = collectionId
|
||||
self.peer = peer
|
||||
self.selectedItem = selected
|
||||
self.expanded = expanded
|
||||
self.theme = theme
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatMediaInputPeerSpecificItemNode()
|
||||
node.contentSize = self.expanded ? expandedBoundingSize : boundingSize
|
||||
node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in
|
||||
node.updateItem(context: self.context, peer: self.peer, collectionId: self.collectionId, theme: self.theme, expanded: self.expanded)
|
||||
node.updateAppearanceTransition(transition: .immediate)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in
|
||||
(node() as? ChatMediaInputPeerSpecificItemNode)?.updateItem(context: self.context, peer: self.peer, collectionId: self.collectionId, theme: self.theme, expanded: self.expanded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
self.selectedItem()
|
||||
}
|
||||
}
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 19.0)
|
||||
private let boundingSize = CGSize(width: 72.0, height: 41.0)
|
||||
private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0)
|
||||
private let boundingImageSize = CGSize(width: 45.0, height: 45.0)
|
||||
private let boundingImageScale: CGFloat = 0.625
|
||||
private let highlightSize = CGSize(width: 56.0, height: 56.0)
|
||||
private let verticalOffset: CGFloat = 3.0
|
||||
|
||||
final class ChatMediaInputPeerSpecificItemNode: ListViewItemNode {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let scalingNode: ASDisplayNode
|
||||
private let avatarNode: AvatarNode
|
||||
private let highlightNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
var currentCollectionId: ItemCollectionId?
|
||||
private var currentExpanded = false
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
|
||||
init() {
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
|
||||
self.scalingNode = ASDisplayNode()
|
||||
|
||||
self.highlightNode = ASImageNode()
|
||||
self.highlightNode.isLayerBacked = true
|
||||
self.highlightNode.isHidden = true
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
self.containerNode.addSubnode(self.scalingNode)
|
||||
|
||||
self.scalingNode.addSubnode(self.highlightNode)
|
||||
self.scalingNode.addSubnode(self.titleNode)
|
||||
self.scalingNode.addSubnode(self.avatarNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
func updateItem(context: AccountContext, peer: Peer, collectionId: ItemCollectionId, theme: PresentationTheme, expanded: Bool) {
|
||||
self.currentCollectionId = collectionId
|
||||
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
|
||||
self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme)
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: EnginePeer(peer).compactDisplayTitle, font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor)
|
||||
}
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize)
|
||||
self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize)
|
||||
|
||||
let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height)
|
||||
let imageSize = CGSize(width: 26.0 * 1.6, height: 26.0 * 1.6)
|
||||
|
||||
let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale
|
||||
let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate
|
||||
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 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.avatarNode.position.y - titleFrame.size.height), size: titleFrame.size)
|
||||
expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame)
|
||||
expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001)
|
||||
|
||||
let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate
|
||||
alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0)
|
||||
|
||||
self.currentExpanded = expanded
|
||||
|
||||
self.avatarNode.bounds = CGRect(origin: CGPoint(), size: imageSize)
|
||||
self.avatarNode.position = CGPoint(x: expandedBoundingSize.height / 2.0, y: expandedBoundingSize.width / 2.0)
|
||||
self.avatarNode.frame = self.avatarNode.frame
|
||||
expandTransition.updateFrame(node: self.highlightNode, frame: expanded ? titleFrame.insetBy(dx: -7.0, dy: -2.0) : CGRect(origin: CGPoint(x: self.avatarNode.position.x - highlightSize.width / 2.0, y: self.avatarNode.position.y - highlightSize.height / 2.0), size: highlightSize))
|
||||
|
||||
self.avatarNode.setPeer(context: context, theme: theme, peer: EnginePeer(peer))
|
||||
}
|
||||
|
||||
func updateIsHighlighted() {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction {
|
||||
self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppearanceTransition(transition: ContainedViewLayoutTransition) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
if let inputNodeInteraction = self.inputNodeInteraction {
|
||||
transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition)
|
||||
}
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
final class ChatMediaInputRecentGifsItem: ListViewItem {
|
||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
let selectedItem: () -> Void
|
||||
let expanded: Bool
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
|
||||
var selectable: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, strings: PresentationStrings, expanded: Bool, selected: @escaping () -> Void) {
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.selectedItem = selected
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.expanded = expanded
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatMediaInputRecentGifsItemNode()
|
||||
node.contentSize = self.expanded ? expandedBoundingSize : boundingSize
|
||||
node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
node.updateIsHighlighted()
|
||||
node.updateAppearanceTransition(transition: .immediate)
|
||||
Queue.mainQueue().async {
|
||||
node.updateTheme(theme: self.theme, strings: self.strings, expanded: self.expanded)
|
||||
completion(node, {
|
||||
return (nil, { _ in })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in
|
||||
(node() as? ChatMediaInputRecentGifsItemNode)?.updateTheme(theme: self.theme, strings: self.strings, expanded: self.expanded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
self.selectedItem()
|
||||
}
|
||||
}
|
||||
|
||||
private let boundingSize = CGSize(width: 72.0, height: 41.0)
|
||||
private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0)
|
||||
private let boundingImageScale: CGFloat = 0.625
|
||||
private let highlightSize = CGSize(width: 56.0, height: 56.0)
|
||||
private let verticalOffset: CGFloat = 3.0 + UIScreenPixel
|
||||
|
||||
final class ChatMediaInputRecentGifsItemNode: ListViewItemNode {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let scalingNode: ASDisplayNode
|
||||
private let imageNode: ASImageNode
|
||||
private let highlightNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
private var currentExpanded = false
|
||||
|
||||
var currentCollectionId: ItemCollectionId?
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
|
||||
var theme: PresentationTheme?
|
||||
|
||||
init() {
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
|
||||
self.scalingNode = ASDisplayNode()
|
||||
|
||||
self.highlightNode = ASImageNode()
|
||||
self.highlightNode.isLayerBacked = true
|
||||
self.highlightNode.isHidden = true
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isLayerBacked = true
|
||||
self.imageNode.contentMode = .scaleAspectFit
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
self.containerNode.addSubnode(self.scalingNode)
|
||||
|
||||
self.scalingNode.addSubnode(self.highlightNode)
|
||||
self.scalingNode.addSubnode(self.titleNode)
|
||||
self.scalingNode.addSubnode(self.imageNode)
|
||||
|
||||
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue, id: 0)
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme, strings: PresentationStrings, expanded: Bool) {
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
|
||||
self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme)
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentGifsIconImage(theme)
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: strings.Stickers_Gifs, font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor)
|
||||
}
|
||||
|
||||
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.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize)
|
||||
self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize)
|
||||
|
||||
let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height)
|
||||
let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale
|
||||
let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate
|
||||
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 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)
|
||||
expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame)
|
||||
expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001)
|
||||
|
||||
let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate
|
||||
alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0)
|
||||
|
||||
self.currentExpanded = expanded
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func updateIsHighlighted() {
|
||||
if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction {
|
||||
self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppearanceTransition(transition: ContainedViewLayoutTransition) {
|
||||
if let inputNodeInteraction = self.inputNodeInteraction {
|
||||
transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
final class ChatMediaInputSettingsItem: ListViewItem {
|
||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
let selectedItem: () -> Void
|
||||
let expanded: Bool
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
|
||||
var selectable: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, strings: PresentationStrings, expanded: Bool, selected: @escaping () -> Void) {
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.selectedItem = selected
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.expanded = expanded
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatMediaInputSettingsItemNode()
|
||||
node.contentSize = self.expanded ? expandedBoundingSize : boundingSize
|
||||
node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
node.updateAppearanceTransition(transition: .immediate)
|
||||
Queue.mainQueue().async {
|
||||
node.updateTheme(theme: self.theme, strings: self.strings, expanded: self.expanded)
|
||||
completion(node, {
|
||||
return (nil, { _ in })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in
|
||||
(node() as? ChatMediaInputSettingsItemNode)?.updateTheme(theme: self.theme, strings: self.strings, expanded: self.expanded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
self.selectedItem()
|
||||
}
|
||||
}
|
||||
|
||||
private let boundingSize = CGSize(width: 72.0, height: 41.0)
|
||||
private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0)
|
||||
private let boundingImageScale: CGFloat = 0.625
|
||||
private let verticalOffset: CGFloat = 3.0 + UIScreenPixel
|
||||
|
||||
final class ChatMediaInputSettingsItemNode: ListViewItemNode {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let scalingNode: ASDisplayNode
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
private let imageNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
private var currentExpanded = false
|
||||
|
||||
var currentCollectionId: ItemCollectionId?
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
|
||||
var theme: PresentationTheme?
|
||||
|
||||
init() {
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
|
||||
self.scalingNode = ASDisplayNode()
|
||||
|
||||
self.buttonNode = HighlightableButtonNode()
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isLayerBacked = true
|
||||
self.imageNode.contentMode = .scaleAspectFit
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
self.containerNode.addSubnode(self.scalingNode)
|
||||
|
||||
self.scalingNode.addSubnode(self.buttonNode)
|
||||
self.scalingNode.addSubnode(self.titleNode)
|
||||
self.scalingNode.addSubnode(self.imageNode)
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme, strings: PresentationStrings, 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)
|
||||
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelSettingsIconImage(theme)
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: strings.Stickers_Settings, font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor)
|
||||
}
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize)
|
||||
self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize)
|
||||
self.buttonNode.frame = self.scalingNode.bounds
|
||||
|
||||
let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height)
|
||||
let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale
|
||||
let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate
|
||||
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 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)
|
||||
expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame)
|
||||
expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001)
|
||||
|
||||
let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate
|
||||
alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0)
|
||||
|
||||
self.currentExpanded = expanded
|
||||
}
|
||||
|
||||
func updateAppearanceTransition(transition: ContainedViewLayoutTransition) {
|
||||
if let inputNodeInteraction = self.inputNodeInteraction {
|
||||
transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -139,19 +139,10 @@ final class ChatMediaInputStickerGridItem: GridItem {
|
||||
self.large = large
|
||||
self.isLocked = isLocked
|
||||
self.selected = selected
|
||||
if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue {
|
||||
self.section = nil
|
||||
} else {
|
||||
let accessory: ChatMediaInputStickerGridSectionAccessory
|
||||
if hasAccessory && stickerPackInfo?.id.namespace == ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, let canManage = canManagePeerSpecificPack, canManage {
|
||||
accessory = .setup
|
||||
} else if hasAccessory && stickerPackInfo?.id.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue {
|
||||
accessory = .clear
|
||||
} else {
|
||||
accessory = .none
|
||||
}
|
||||
self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, accessory: accessory, theme: theme, interaction: inputNodeInteraction)
|
||||
}
|
||||
|
||||
let accessory: ChatMediaInputStickerGridSectionAccessory
|
||||
accessory = .none
|
||||
self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, accessory: accessory, theme: theme, interaction: inputNodeInteraction)
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
|
@ -1,410 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import StickerResources
|
||||
import ItemListStickerPackItem
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ShimmerEffect
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
final class ChatMediaInputStickerPackItem: ListViewItem {
|
||||
let account: Account
|
||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
let collectionId: ItemCollectionId
|
||||
let collectionInfo: StickerPackCollectionInfo
|
||||
let stickerPackItem: StickerPackItem?
|
||||
let selectedItem: () -> Void
|
||||
let index: Int
|
||||
let theme: PresentationTheme
|
||||
let expanded: Bool
|
||||
let reorderable: Bool
|
||||
|
||||
var selectable: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo, stickerPackItem: StickerPackItem?, index: Int, theme: PresentationTheme, expanded: Bool, reorderable: Bool, selected: @escaping () -> Void) {
|
||||
self.account = account
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.collectionId = collectionId
|
||||
self.collectionInfo = collectionInfo
|
||||
self.stickerPackItem = stickerPackItem
|
||||
self.selectedItem = selected
|
||||
self.index = index
|
||||
self.theme = theme
|
||||
self.expanded = expanded
|
||||
self.reorderable = reorderable
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatMediaInputStickerPackItemNode()
|
||||
node.contentSize = self.expanded ? expandedBoundingSize : boundingSize
|
||||
node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in
|
||||
node.updateStickerPackItem(account: self.account, info: self.collectionInfo, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme, expanded: self.expanded, reorderable: self.reorderable)
|
||||
node.updateAppearanceTransition(transition: .immediate)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in
|
||||
(node() as? ChatMediaInputStickerPackItemNode)?.updateStickerPackItem(account: self.account, info: self.collectionInfo, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme, expanded: self.expanded, reorderable: self.reorderable)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
self.selectedItem()
|
||||
}
|
||||
}
|
||||
|
||||
private let boundingSize = CGSize(width: 72.0, height: 41.0)
|
||||
private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0)
|
||||
private let boundingImageSize = CGSize(width: 45.0, height: 45.0)
|
||||
private let boundingImageScale: CGFloat = 0.625
|
||||
private let highlightSize = CGSize(width: 56.0, height: 56.0)
|
||||
private let verticalOffset: CGFloat = -3.0
|
||||
|
||||
final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let scalingNode: ASDisplayNode
|
||||
private let imageNode: TransformImageNode
|
||||
private var animatedStickerNode: AnimatedStickerNode?
|
||||
private var placeholderNode: StickerShimmerEffectNode?
|
||||
private let highlightNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
var currentCollectionId: ItemCollectionId?
|
||||
private var account: Account?
|
||||
private var currentThumbnailItem: StickerPackThumbnailItem?
|
||||
private var currentExpanded = false
|
||||
private var theme: PresentationTheme?
|
||||
private var reorderable = false
|
||||
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
|
||||
self.scalingNode = ASDisplayNode()
|
||||
|
||||
self.highlightNode = ASImageNode()
|
||||
self.highlightNode.isLayerBacked = true
|
||||
self.highlightNode.isHidden = true
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
|
||||
|
||||
self.placeholderNode = StickerShimmerEffectNode()
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
self.containerNode.addSubnode(self.scalingNode)
|
||||
|
||||
self.scalingNode.addSubnode(self.highlightNode)
|
||||
self.scalingNode.addSubnode(self.titleNode)
|
||||
self.scalingNode.addSubnode(self.imageNode)
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.scalingNode.addSubnode(placeholderNode)
|
||||
}
|
||||
|
||||
var firstTime = true
|
||||
self.imageNode.imageUpdated = { [weak self] image in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if image != nil {
|
||||
strongSelf.removePlaceholder(animated: !firstTime)
|
||||
if firstTime {
|
||||
strongSelf.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
firstTime = false
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
private func removePlaceholder(animated: Bool) {
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.placeholderNode = nil
|
||||
if !animated {
|
||||
placeholderNode.removeFromSupernode()
|
||||
} else {
|
||||
placeholderNode.alpha = 0.0
|
||||
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in
|
||||
placeholderNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateStickerPackItem(account: Account, info: StickerPackCollectionInfo, item: StickerPackItem?, collectionId: ItemCollectionId, theme: PresentationTheme, expanded: Bool, reorderable: Bool) {
|
||||
self.currentCollectionId = collectionId
|
||||
self.account = account
|
||||
self.reorderable = reorderable
|
||||
var themeUpdated = false
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
themeUpdated = true
|
||||
|
||||
self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme)
|
||||
}
|
||||
|
||||
var thumbnailItem: StickerPackThumbnailItem?
|
||||
var resourceReference: MediaResourceReference?
|
||||
if let thumbnail = info.thumbnail {
|
||||
if info.flags.contains(.isAnimated) || info.flags.contains(.isVideo) {
|
||||
thumbnailItem = .animated(thumbnail.resource, thumbnail.dimensions, info.flags.contains(.isVideo))
|
||||
} else {
|
||||
thumbnailItem = .still(thumbnail)
|
||||
}
|
||||
resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)
|
||||
} else if let item = item {
|
||||
if item.file.isAnimatedSticker || item.file.isVideoSticker {
|
||||
thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100), item.file.isVideoSticker)
|
||||
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource)
|
||||
} else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource {
|
||||
thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
|
||||
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource)
|
||||
}
|
||||
}
|
||||
|
||||
if themeUpdated || self.titleNode.attributedText?.string != info.title {
|
||||
self.titleNode.attributedText = NSAttributedString(string: info.title, font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor)
|
||||
}
|
||||
|
||||
let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height)
|
||||
var imageSize = boundingImageSize
|
||||
|
||||
if self.currentThumbnailItem != thumbnailItem {
|
||||
self.currentThumbnailItem = thumbnailItem
|
||||
if let thumbnailItem = thumbnailItem {
|
||||
switch thumbnailItem {
|
||||
case let .still(representation):
|
||||
imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize)
|
||||
let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 6.0), imageSize: imageSize, boundingSize: boundingImageSize, intrinsicInsets: UIEdgeInsets()))
|
||||
imageApply()
|
||||
self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource, nilIfEmpty: true))
|
||||
case let .animated(resource, dimensions, isVideo):
|
||||
imageSize = dimensions.cgSize.aspectFitted(boundingImageSize)
|
||||
let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingImageSize, intrinsicInsets: UIEdgeInsets()))
|
||||
imageApply()
|
||||
self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true, nilIfEmpty: true))
|
||||
|
||||
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
|
||||
|
||||
let animatedStickerNode: AnimatedStickerNode
|
||||
if let current = self.animatedStickerNode {
|
||||
animatedStickerNode = current
|
||||
} else {
|
||||
animatedStickerNode = DefaultAnimatedStickerNodeImpl()
|
||||
animatedStickerNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
self?.removePlaceholder(animated: false)
|
||||
}
|
||||
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: resource, isVideo: isVideo), width: 128, height: 128, playbackMode: .loop, mode: .cached)
|
||||
}
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
}
|
||||
if let resourceReference = resourceReference {
|
||||
self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: resourceReference).start())
|
||||
}
|
||||
}
|
||||
|
||||
self.updateIsHighlighted()
|
||||
}
|
||||
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
var imageSize = PixelDimensions(width: 512, height: 512)
|
||||
var immediateThumbnailData: Data?
|
||||
if let data = info.immediateThumbnailData {
|
||||
if info.flags.contains(.isVideo) {
|
||||
imageSize = PixelDimensions(width: 100, height: 100)
|
||||
}
|
||||
immediateThumbnailData = data
|
||||
} else if let data = item?.file.immediateThumbnailData {
|
||||
immediateThumbnailData = data
|
||||
}
|
||||
|
||||
placeholderNode.update(backgroundColor: nil, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputPanel.panelBackgroundColor, alpha: 0.4), shimmeringColor: theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.withMultipliedAlpha(0.2), data: immediateThumbnailData, size: boundingImageSize, enableEffect: true, imageSize: imageSize.cgSize)
|
||||
}
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize)
|
||||
self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize)
|
||||
|
||||
let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale
|
||||
let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate
|
||||
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 - 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)
|
||||
expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame)
|
||||
expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001)
|
||||
|
||||
let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate
|
||||
alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0)
|
||||
|
||||
self.currentExpanded = expanded
|
||||
|
||||
self.imageNode.bounds = CGRect(origin: CGPoint(), size: imageSize)
|
||||
self.imageNode.position = CGPoint(x: expandedBoundingSize.height / 2.0, y: expandedBoundingSize.width / 2.0)
|
||||
if let animatedStickerNode = self.animatedStickerNode {
|
||||
animatedStickerNode.frame = self.imageNode.frame
|
||||
animatedStickerNode.updateLayout(size: self.imageNode.frame.size)
|
||||
}
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
placeholderNode.bounds = CGRect(origin: CGPoint(), size: boundingImageSize)
|
||||
placeholderNode.position = self.imageNode.position
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
placeholderNode.updateAbsoluteRect(rect, within: containerSize)
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsHighlighted() {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction {
|
||||
self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppearanceTransition(transition: ContainedViewLayoutTransition) {
|
||||
assert(Queue.mainQueue().isCurrent())
|
||||
if let inputNodeInteraction = self.inputNodeInteraction {
|
||||
transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition)
|
||||
}
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
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 {
|
||||
guard self.reorderable else {
|
||||
return false
|
||||
}
|
||||
if self.bounds.inset(by: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -28.0)).contains(point) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func snapshotForReordering() -> UIView? {
|
||||
if let account = self.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(radius: 6.0), 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, dimensions, isVideo):
|
||||
imageSize = dimensions.cgSize.aspectFitted(boundingImageSize)
|
||||
|
||||
let animatedStickerNode = DefaultAnimatedStickerNodeImpl()
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: 128, height: 128, mode: .cached)
|
||||
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
|
||||
}
|
||||
}
|
@ -1,202 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ChatInputNode
|
||||
import FeaturedStickersScreen
|
||||
|
||||
private func fixGridScrolling(_ gridNode: GridNode) {
|
||||
var searchItemNode: GridItemNode?
|
||||
var nextItemNode: GridItemNode?
|
||||
|
||||
gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? PaneSearchBarPlaceholderNode {
|
||||
searchItemNode = itemNode
|
||||
} else if searchItemNode != nil && nextItemNode == nil, let itemNode = itemNode as? GridItemNode {
|
||||
nextItemNode = itemNode
|
||||
}
|
||||
}
|
||||
|
||||
if let searchItemNode = searchItemNode {
|
||||
let contentInset = gridNode.scrollView.contentInset.top
|
||||
let itemFrame = gridNode.convert(searchItemNode.frame, to: gridNode.supernode)
|
||||
if itemFrame.contains(CGPoint(x: 0.0, y: contentInset)) {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
|
||||
|
||||
var scrollIndex: Int?
|
||||
if itemFrame.minY + itemFrame.height * 0.6 < contentInset {
|
||||
for i in 0 ..< gridNode.items.count {
|
||||
if let _ = gridNode.items[i] as? StickerPaneTrendingListGridItem {
|
||||
scrollIndex = i
|
||||
break
|
||||
} else if let _ = gridNode.items[i] as? ChatMediaInputStickerGridItem {
|
||||
scrollIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i in 0 ..< gridNode.items.count {
|
||||
if let _ = gridNode.items[i] as? PaneSearchBarPlaceholderItem {
|
||||
scrollIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let scrollIndex = scrollIndex {
|
||||
gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: scrollIndex, position: .top(0.0), transition: transition, directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { _ in })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatMediaInputStickerPaneOpaqueState {
|
||||
let hasLower: Bool
|
||||
|
||||
init(hasLower: Bool) {
|
||||
self.hasLower = hasLower
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatMediaInputStickerPane: ChatMediaInputPane {
|
||||
private var isExpanded: Bool?
|
||||
private var isPaneVisible = false
|
||||
let gridNode: GridNode
|
||||
private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void
|
||||
private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void
|
||||
private var didScrollPreviousOffset: CGFloat?
|
||||
private var didScrollPreviousState: ChatMediaInputPaneScrollState?
|
||||
|
||||
var beganScrolling: (() -> Void)?
|
||||
var endedScrolling: (() -> Void)?
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void) {
|
||||
self.gridNode = GridNode()
|
||||
self.paneDidScroll = paneDidScroll
|
||||
self.fixPaneScroll = fixPaneScroll
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.gridNode)
|
||||
self.gridNode.presentationLayoutUpdated = { [weak self] layout, transition in
|
||||
if let strongSelf = self, let opaqueState = strongSelf.gridNode.opaqueState as? ChatMediaInputStickerPaneOpaqueState {
|
||||
var offset: CGFloat
|
||||
if opaqueState.hasLower {
|
||||
offset = -(layout.contentOffset.y + 41.0)
|
||||
} else {
|
||||
offset = -(layout.contentOffset.y + 41.0)
|
||||
offset = min(0.0, offset + 56.0)
|
||||
}
|
||||
var relativeChange: CGFloat = 0.0
|
||||
if let didScrollPreviousOffset = strongSelf.didScrollPreviousOffset {
|
||||
relativeChange = offset - didScrollPreviousOffset
|
||||
}
|
||||
strongSelf.didScrollPreviousOffset = offset
|
||||
let state = ChatMediaInputPaneScrollState(absoluteOffset: offset, relativeChange: relativeChange)
|
||||
strongSelf.didScrollPreviousState = state
|
||||
if !transition.isAnimated {
|
||||
strongSelf.paneDidScroll(strongSelf, state, transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.gridNode.scrollingInitiated = { [weak self] in
|
||||
self?.beganScrolling?()
|
||||
}
|
||||
self.gridNode.scrollingCompleted = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if let didScrollPreviousState = strongSelf.didScrollPreviousState {
|
||||
strongSelf.fixPaneScroll(strongSelf, didScrollPreviousState)
|
||||
}
|
||||
fixGridScrolling(strongSelf.gridNode)
|
||||
strongSelf.endedScrolling?()
|
||||
}
|
||||
}
|
||||
self.gridNode.setupNode = { [weak self] itemNode in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let itemNode = itemNode as? ChatMediaInputStickerGridItemNode {
|
||||
itemNode.updateIsPanelVisible(strongSelf.isPaneVisible)
|
||||
} else if let itemNode = itemNode as? StickerPaneTrendingListGridItemNode {
|
||||
itemNode.updateIsPanelVisible(strongSelf.isPaneVisible)
|
||||
}
|
||||
}
|
||||
self.gridNode.scrollView.alwaysBounceVertical = true
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
|
||||
var changedIsExpanded = false
|
||||
if let previousIsExpanded = self.isExpanded {
|
||||
if previousIsExpanded != isExpanded {
|
||||
changedIsExpanded = true
|
||||
}
|
||||
}
|
||||
self.isExpanded = isExpanded
|
||||
|
||||
let maxItemSize: CGSize
|
||||
if case .tablet = deviceMetrics.type, size.width > 480.0 {
|
||||
maxItemSize = CGSize(width: 90.0, height: 96.0)
|
||||
} else {
|
||||
maxItemSize = CGSize(width: 75.0, height: 80.0)
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 2.0
|
||||
var itemSide: CGFloat = floor((size.width - sideInset * 2.0) / 5.0)
|
||||
itemSide = min(itemSide, maxItemSize.width)
|
||||
let itemSize = CGSize(width: itemSide, height: max(itemSide, maxItemSize.height))
|
||||
|
||||
var scrollToItem: GridNodeScrollToItem?
|
||||
if changedIsExpanded {
|
||||
if isExpanded {
|
||||
var scrollIndex: Int?
|
||||
for i in 0 ..< self.gridNode.items.count {
|
||||
if let _ = self.gridNode.items[i] as? PaneSearchBarPlaceholderItem {
|
||||
scrollIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if let scrollIndex = scrollIndex {
|
||||
scrollToItem = GridNodeScrollToItem(index: scrollIndex, position: .top(0.0), transition: transition, directionHint: .down, adjustForSection: true, adjustForTopInset: true)
|
||||
}
|
||||
} else {
|
||||
var scrollIndex: Int?
|
||||
for i in 0 ..< self.gridNode.items.count {
|
||||
if let _ = self.gridNode.items[i] as? ChatMediaInputStickerGridItem {
|
||||
scrollIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if let scrollIndex = scrollIndex {
|
||||
scrollToItem = GridNodeScrollToItem(index: scrollIndex, position: .top(0.0), transition: transition, directionHint: .down, adjustForSection: true, adjustForTopInset: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), preloadSize: isVisible ? 300.0 : 0.0, type: .fixed(itemSize: itemSize, fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
|
||||
transition.updateFrame(node: self.gridNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)))
|
||||
|
||||
if self.isPaneVisible != isVisible {
|
||||
self.isPaneVisible = isVisible
|
||||
self.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMediaInputStickerGridItemNode {
|
||||
itemNode.updateIsPanelVisible(isVisible)
|
||||
} else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
|
||||
itemNode.updateCanPlayMedia()
|
||||
} else if let itemNode = itemNode as? StickerPaneTrendingListGridItemNode {
|
||||
itemNode.updateIsPanelVisible(isVisible)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
|
||||
if let itemNode = self.gridNode.itemNodeAtPoint(self.view.convert(point, to: self.gridNode.view)) as? ChatMediaInputStickerGridItemNode, let stickerPackItem = itemNode.stickerPackItem {
|
||||
return (itemNode, stickerPackItem)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
final class ChatMediaInputTrendingItem: ListViewItem {
|
||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
let selectedItem: () -> Void
|
||||
let elevated: Bool
|
||||
let expanded: Bool
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
|
||||
var selectable: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(inputNodeInteraction: ChatMediaInputNodeInteraction, elevated: Bool, theme: PresentationTheme, strings: PresentationStrings, expanded: Bool, selected: @escaping () -> Void) {
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.elevated = elevated
|
||||
self.selectedItem = selected
|
||||
self.expanded = expanded
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatMediaInputTrendingItemNode()
|
||||
node.contentSize = self.expanded ? expandedBoundingSize : boundingSize
|
||||
node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
node.updateIsHighlighted()
|
||||
node.updateAppearanceTransition(transition: .immediate)
|
||||
Queue.mainQueue().async {
|
||||
node.updateTheme(elevated: self.elevated, theme: self.theme, strings: self.strings, expanded: self.expanded)
|
||||
completion(node, {
|
||||
return (nil, { _ in })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
completion(ListViewItemNodeLayout(contentSize: self.expanded ? expandedBoundingSize : boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in
|
||||
(node() as? ChatMediaInputTrendingItemNode)?.updateTheme(elevated: self.elevated, theme: self.theme, strings: self.strings, expanded: self.expanded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
self.selectedItem()
|
||||
}
|
||||
}
|
||||
|
||||
private let boundingSize = CGSize(width: 72.0, height: 41.0)
|
||||
private let expandedBoundingSize = CGSize(width: 72.0, height: 72.0)
|
||||
private let boundingImageScale: CGFloat = 0.625
|
||||
private let highlightSize = CGSize(width: 56.0, height: 56.0)
|
||||
private let verticalOffset: CGFloat = 3.0 + UIScreenPixel
|
||||
|
||||
final class ChatMediaInputTrendingItemNode: ListViewItemNode {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let scalingNode: ASDisplayNode
|
||||
private let imageNode: ASImageNode
|
||||
private let highlightNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
|
||||
private var currentExpanded = false
|
||||
|
||||
var currentCollectionId: ItemCollectionId?
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
|
||||
var elevated: Bool = false
|
||||
var theme: PresentationTheme?
|
||||
|
||||
let badgeBackground: ASImageNode
|
||||
|
||||
init() {
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
|
||||
self.scalingNode = ASDisplayNode()
|
||||
|
||||
self.highlightNode = ASImageNode()
|
||||
self.highlightNode.isLayerBacked = true
|
||||
self.highlightNode.isHidden = true
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isLayerBacked = true
|
||||
self.imageNode.contentMode = .scaleAspectFit
|
||||
|
||||
self.badgeBackground = ASImageNode()
|
||||
self.badgeBackground.displaysAsynchronously = false
|
||||
self.badgeBackground.displayWithoutProcessing = true
|
||||
self.badgeBackground.isHidden = true
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
self.containerNode.addSubnode(self.scalingNode)
|
||||
|
||||
self.scalingNode.addSubnode(self.highlightNode)
|
||||
self.scalingNode.addSubnode(self.titleNode)
|
||||
self.scalingNode.addSubnode(self.imageNode)
|
||||
self.scalingNode.addSubnode(self.badgeBackground)
|
||||
|
||||
self.currentCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0)
|
||||
}
|
||||
|
||||
func updateTheme(elevated: Bool, theme: PresentationTheme, strings: PresentationStrings, expanded: Bool) {
|
||||
let imageSize = CGSize(width: 26.0 * 1.85, height: 26.0 * 1.85)
|
||||
let imageFrame = 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 = imageFrame
|
||||
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
|
||||
self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme)
|
||||
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingIconImage(theme)
|
||||
self.badgeBackground.image = generateFilledCircleImage(diameter: 10.0, color: theme.chat.inputPanel.mediaRecordingDotColor)
|
||||
|
||||
if let image = self.badgeBackground.image {
|
||||
self.badgeBackground.frame = CGRect(origin: CGPoint(x: floor(imageFrame.maxX - image.size.width - 7.0), y: 18.0), size: image.size)
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: strings.Stickers_Trending, font: Font.regular(11.0), textColor: theme.chat.inputPanel.primaryTextColor)
|
||||
}
|
||||
|
||||
if self.elevated != elevated {
|
||||
self.elevated = elevated
|
||||
}
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: expandedBoundingSize)
|
||||
self.scalingNode.bounds = CGRect(origin: CGPoint(), size: expandedBoundingSize)
|
||||
|
||||
let boundsSize = expanded ? expandedBoundingSize : CGSize(width: boundingSize.height, height: boundingSize.height)
|
||||
let expandScale: CGFloat = expanded ? 1.0 : boundingImageScale
|
||||
let expandTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: 0.3, curve: .spring) : .immediate
|
||||
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 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)
|
||||
expandTransition.updateFrameAsPositionAndBounds(node: self.titleNode, frame: displayTitleFrame)
|
||||
expandTransition.updateTransformScale(node: self.titleNode, scale: expanded ? 1.0 : 0.001)
|
||||
|
||||
let alphaTransition: ContainedViewLayoutTransition = self.currentExpanded != expanded ? .animated(duration: expanded ? 0.15 : 0.1, curve: .linear) : .immediate
|
||||
alphaTransition.updateAlpha(node: self.titleNode, alpha: expanded ? 1.0 : 0.0, delay: expanded ? 0.05 : 0.0)
|
||||
|
||||
self.currentExpanded = expanded
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func updateIsHighlighted() {
|
||||
if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction {
|
||||
self.highlightNode.isHidden = inputNodeInteraction.highlightedItemCollectionId != currentCollectionId
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppearanceTransition(transition: ContainedViewLayoutTransition) {
|
||||
if let inputNodeInteraction = self.inputNodeInteraction {
|
||||
transition.updateSublayerTransformScale(node: self, scale: inputNodeInteraction.appearanceTransition)
|
||||
}
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
@ -1,191 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
|
||||
final class StickerPanePeerSpecificSetupGridItem: GridItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let setup: () -> Void
|
||||
let dismiss: (() -> Void)?
|
||||
|
||||
let section: GridSection? = nil
|
||||
let fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)?
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, setup: @escaping () -> Void, dismiss: (() -> Void)?) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.setup = setup
|
||||
self.dismiss = dismiss
|
||||
self.fillsRowWithDynamicHeight = { width in
|
||||
let makeDescriptionLayout = TextNode.asyncLayout(nil)
|
||||
let params = ListViewItemLayoutParams(width: width, leftInset: 0.0, rightInset: 0.0, availableHeight: 0.0)
|
||||
let leftInset: CGFloat = 12.0
|
||||
let rightInset: CGFloat = 16.0
|
||||
let (descriptionLayout, _) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: strings.Stickers_GroupStickersHelp, font: statusFont, textColor: .black), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
return 71.0 + descriptionLayout.size.height
|
||||
}
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = StickerPanePeerSpecificSetupGridItemNode()
|
||||
node.setup(item: self)
|
||||
return node
|
||||
}
|
||||
|
||||
func update(node: GridItemNode) {
|
||||
guard let node = node as? StickerPanePeerSpecificSetupGridItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
node.setup(item: self)
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.medium(12.0)
|
||||
private let statusFont = Font.regular(14.0)
|
||||
private let buttonFont = Font.medium(13.0)
|
||||
|
||||
class StickerPanePeerSpecificSetupGridItemNode: GridItemNode {
|
||||
private let titleNode: TextNode
|
||||
private let descriptionNode: TextNode
|
||||
private let installTextNode: TextNode
|
||||
private let installBackgroundNode: ASImageNode
|
||||
private let installButtonNode: HighlightTrackingButtonNode
|
||||
private let dismissButtonNode: HighlightTrackingButtonNode
|
||||
|
||||
private var item: StickerPanePeerSpecificSetupGridItem?
|
||||
private var appliedItem: StickerPanePeerSpecificSetupGridItem?
|
||||
|
||||
override init() {
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.descriptionNode = TextNode()
|
||||
self.descriptionNode.isUserInteractionEnabled = false
|
||||
self.descriptionNode.contentMode = .left
|
||||
self.descriptionNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.installTextNode = TextNode()
|
||||
self.installTextNode.isUserInteractionEnabled = false
|
||||
self.installTextNode.contentMode = .left
|
||||
self.installTextNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.installBackgroundNode = ASImageNode()
|
||||
self.installBackgroundNode.isLayerBacked = true
|
||||
self.installBackgroundNode.displayWithoutProcessing = true
|
||||
self.installBackgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.installButtonNode = HighlightTrackingButtonNode()
|
||||
self.dismissButtonNode = HighlightTrackingButtonNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.descriptionNode)
|
||||
self.addSubnode(self.installBackgroundNode)
|
||||
self.addSubnode(self.installTextNode)
|
||||
self.addSubnode(self.installButtonNode)
|
||||
self.addSubnode(self.dismissButtonNode)
|
||||
|
||||
self.installButtonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.installBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.installBackgroundNode.alpha = 0.4
|
||||
strongSelf.installTextNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.installTextNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.installBackgroundNode.alpha = 1.0
|
||||
strongSelf.installBackgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.installTextNode.alpha = 1.0
|
||||
strongSelf.installTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.installButtonNode.addTarget(self, action: #selector(self.installPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.dismissButtonNode.addTarget(self, action: #selector(self.dismissPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func setup(item: StickerPanePeerSpecificSetupGridItem) {
|
||||
self.item = item
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
||||
let params = ListViewItemLayoutParams(width: self.bounds.size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: self.bounds.size.height)
|
||||
|
||||
let makeInstallLayout = TextNode.asyncLayout(self.installTextNode)
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
|
||||
|
||||
let currentItem = self.appliedItem
|
||||
self.appliedItem = item
|
||||
|
||||
var updateButtonBackgroundImage: UIImage?
|
||||
if currentItem?.theme !== item.theme {
|
||||
updateButtonBackgroundImage = PresentationResourcesChat.chatInputMediaPanelAddPackButtonImage(item.theme)
|
||||
self.dismissButtonNode.setImage(PresentationResourcesChat.chatInputMediaPanelGridDismissImage(item.theme, color: item.theme.chat.inputMediaPanel.stickersSectionTextColor), for: [])
|
||||
}
|
||||
|
||||
let leftInset: CGFloat = 12.0
|
||||
let rightInset: CGFloat = 16.0
|
||||
let topOffset: CGFloat = 9.0
|
||||
let textSpacing: CGFloat = 3.0
|
||||
let buttonSpacing: CGFloat = 6.0
|
||||
|
||||
let (installLayout, installApply) = makeInstallLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_GroupChooseStickerPack, font: buttonFont, textColor: item.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_GroupStickers.uppercased(), font: titleFont, textColor: item.theme.chat.inputMediaPanel.stickersSectionTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0 - installLayout.size.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.strings.Stickers_GroupStickersHelp, font: statusFont, textColor: item.theme.chat.inputMediaPanel.stickersSectionTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
self.item = item
|
||||
|
||||
let _ = installApply()
|
||||
let _ = titleApply()
|
||||
let _ = descriptionApply()
|
||||
|
||||
if let updateButtonBackgroundImage = updateButtonBackgroundImage {
|
||||
self.installBackgroundNode.image = updateButtonBackgroundImage
|
||||
}
|
||||
|
||||
let installWidth: CGFloat = installLayout.size.width + 20.0
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: topOffset + titleLayout.size.height + textSpacing + descriptionLayout.size.height + buttonSpacing), size: CGSize(width: installWidth, height: 32.0))
|
||||
self.installBackgroundNode.frame = buttonFrame
|
||||
self.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - installLayout.size.height) / 2.0)), size: installLayout.size)
|
||||
self.installButtonNode.frame = buttonFrame
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: topOffset), size: titleLayout.size)
|
||||
let dismissButtonSize = CGSize(width: 12.0, height: 12.0)
|
||||
self.dismissButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - dismissButtonSize.width, y: topOffset - 1.0), size: dismissButtonSize)
|
||||
self.dismissButtonNode.isHidden = item.dismiss == nil
|
||||
self.titleNode.frame = titleFrame
|
||||
self.descriptionNode.frame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: topOffset + titleLayout.size.height + textSpacing), size: descriptionLayout.size)
|
||||
}
|
||||
|
||||
@objc private func installPressed() {
|
||||
if let item = self.item {
|
||||
item.setup()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func dismissPressed() {
|
||||
if let item = self.item {
|
||||
item.dismiss?()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,554 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import StickerResources
|
||||
import ItemListStickerPackItem
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ShimmerEffect
|
||||
import MergeLists
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
private let boundingSize = CGSize(width: 41.0, height: 41.0)
|
||||
private let boundingImageSize = CGSize(width: 28.0, height: 28.0)
|
||||
|
||||
private struct Transition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
}
|
||||
|
||||
private enum EntryStableId: Hashable {
|
||||
case stickerPack(Int64)
|
||||
}
|
||||
|
||||
private enum Entry: Comparable, Identifiable {
|
||||
case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, unread: Bool, theme: PresentationTheme)
|
||||
|
||||
var stableId: EntryStableId {
|
||||
switch self {
|
||||
case let .stickerPack(_, info, _, _, _):
|
||||
return .stickerPack(info.id.id)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: Entry, rhs: Entry) -> Bool {
|
||||
switch lhs {
|
||||
case let .stickerPack(index, info, topItem, lhsUnread, lhsTheme):
|
||||
if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem, rhsUnread, rhsTheme) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem, lhsUnread == rhsUnread, lhsTheme === rhsTheme {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: Entry, rhs: Entry) -> Bool {
|
||||
switch lhs {
|
||||
case let .stickerPack(lhsIndex, lhsInfo, _, _, _):
|
||||
switch rhs {
|
||||
case let .stickerPack(rhsIndex, rhsInfo, _, _, _):
|
||||
if lhsIndex == rhsIndex {
|
||||
return lhsInfo.id.id < rhsInfo.id.id
|
||||
} else {
|
||||
return lhsIndex <= rhsIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, isVisible: @escaping () -> Bool) -> ListViewItem {
|
||||
switch self {
|
||||
case let .stickerPack(index, info, topItem, unread, theme):
|
||||
return FeaturedPackItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, collectionInfo: info, stickerPackItem: topItem, unread: unread, index: index, theme: theme, selected: {
|
||||
inputNodeInteraction.openTrending(info.id)
|
||||
}, isVisible: isVisible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func preparedEntryTransition(account: Account, from fromEntries: [Entry], to toEntries: [Entry], inputNodeInteraction: ChatMediaInputNodeInteraction, isVisible: @escaping () -> Bool) -> Transition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction, isVisible: isVisible), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction, isVisible: isVisible), directionHint: nil) }
|
||||
|
||||
return Transition(deletions: deletions, insertions: insertions, updates: updates)
|
||||
}
|
||||
|
||||
private func panelEntries(featuredPacks: [FeaturedStickerPackItem], theme: PresentationTheme) -> [Entry] {
|
||||
var entries: [Entry] = []
|
||||
var index = 0
|
||||
for pack in featuredPacks {
|
||||
entries.append(.stickerPack(index: index, info: pack.info, topItem: pack.topItems.first, unread: pack.unread, theme: theme))
|
||||
index += 1
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
private final class FeaturedPackItem: ListViewItem {
|
||||
let account: Account
|
||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
let collectionId: ItemCollectionId
|
||||
let collectionInfo: StickerPackCollectionInfo
|
||||
let stickerPackItem: StickerPackItem?
|
||||
let unread: Bool
|
||||
let selectedItem: () -> Void
|
||||
let index: Int
|
||||
let theme: PresentationTheme
|
||||
let isVisible: () -> Bool
|
||||
|
||||
var selectable: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo, stickerPackItem: StickerPackItem?, unread: Bool, index: Int, theme: PresentationTheme, selected: @escaping () -> Void, isVisible: @escaping () -> Bool) {
|
||||
self.account = account
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.collectionId = collectionId
|
||||
self.collectionInfo = collectionInfo
|
||||
self.stickerPackItem = stickerPackItem
|
||||
self.unread = unread
|
||||
self.index = index
|
||||
self.theme = theme
|
||||
self.selectedItem = selected
|
||||
self.isVisible = isVisible
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = FeaturedPackItemNode()
|
||||
node.contentSize = boundingSize
|
||||
node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
node.panelIsVisible = self.isVisible
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in
|
||||
node.updateStickerPackItem(account: self.account, info: self.collectionInfo, item: self.stickerPackItem, collectionId: self.collectionId, unread: self.unread, theme: self.theme)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
completion(ListViewItemNodeLayout(contentSize: boundingSize, insets: ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)), { _ in
|
||||
(node() as? FeaturedPackItemNode)?.updateStickerPackItem(account: self.account, info: self.collectionInfo, item: self.stickerPackItem, collectionId: self.collectionId, unread: self.unread, theme: self.theme)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
self.selectedItem()
|
||||
}
|
||||
}
|
||||
|
||||
private final class FeaturedPackItemNode: ListViewItemNode {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let imageNode: TransformImageNode
|
||||
private var animatedStickerNode: AnimatedStickerNode?
|
||||
private var placeholderNode: StickerShimmerEffectNode?
|
||||
private let unreadNode: ASImageNode
|
||||
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
var currentCollectionId: ItemCollectionId?
|
||||
private var currentThumbnailItem: StickerPackThumbnailItem?
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
|
||||
var panelIsVisible: () -> Bool = {
|
||||
return true
|
||||
}
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
self.visibilityStatus = self.visibility != .none
|
||||
}
|
||||
}
|
||||
|
||||
var visibilityStatus: Bool = false {
|
||||
didSet {
|
||||
if self.visibilityStatus != oldValue {
|
||||
self.updateVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateVisibility() {
|
||||
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
|
||||
let panelVisible = self.panelIsVisible()
|
||||
self.animatedStickerNode?.visibility = self.visibilityStatus && loopAnimatedStickers && panelVisible
|
||||
}
|
||||
|
||||
init() {
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
|
||||
|
||||
self.placeholderNode = StickerShimmerEffectNode()
|
||||
|
||||
self.unreadNode = ASImageNode()
|
||||
self.unreadNode.isLayerBacked = true
|
||||
self.unreadNode.displayWithoutProcessing = true
|
||||
self.unreadNode.displaysAsynchronously = false
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
self.containerNode.addSubnode(self.imageNode)
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.containerNode.addSubnode(placeholderNode)
|
||||
}
|
||||
self.containerNode.addSubnode(self.unreadNode)
|
||||
|
||||
var firstTime = true
|
||||
self.imageNode.imageUpdated = { [weak self] image in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if image != nil {
|
||||
strongSelf.removePlaceholder(animated: !firstTime)
|
||||
if firstTime {
|
||||
strongSelf.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
firstTime = false
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
private func removePlaceholder(animated: Bool) {
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.placeholderNode = nil
|
||||
if !animated {
|
||||
placeholderNode.removeFromSupernode()
|
||||
} else {
|
||||
placeholderNode.alpha = 0.0
|
||||
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in
|
||||
placeholderNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateStickerPackItem(account: Account, info: StickerPackCollectionInfo, item: StickerPackItem?, collectionId: ItemCollectionId, unread: Bool, theme: PresentationTheme) {
|
||||
self.currentCollectionId = collectionId
|
||||
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
}
|
||||
|
||||
var thumbnailItem: StickerPackThumbnailItem?
|
||||
var resourceReference: MediaResourceReference?
|
||||
if let thumbnail = info.thumbnail {
|
||||
if info.flags.contains(.isAnimated) || info.flags.contains(.isVideo) {
|
||||
thumbnailItem = .animated(thumbnail.resource, thumbnail.dimensions, info.flags.contains(.isVideo))
|
||||
} else {
|
||||
thumbnailItem = .still(thumbnail)
|
||||
}
|
||||
resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource)
|
||||
} else if let item = item {
|
||||
if item.file.isAnimatedSticker || item.file.isVideoSticker {
|
||||
thumbnailItem = .animated(item.file.resource, item.file.dimensions ?? PixelDimensions(width: 100, height: 100), item.file.isVideoSticker)
|
||||
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource)
|
||||
} else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource {
|
||||
thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
|
||||
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource)
|
||||
}
|
||||
}
|
||||
|
||||
var imageSize = boundingImageSize
|
||||
|
||||
if self.currentThumbnailItem != thumbnailItem {
|
||||
self.currentThumbnailItem = thumbnailItem
|
||||
if let thumbnailItem = thumbnailItem {
|
||||
switch thumbnailItem {
|
||||
case let .still(representation):
|
||||
imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize)
|
||||
let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))
|
||||
imageApply()
|
||||
self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource, nilIfEmpty: true))
|
||||
case let .animated(resource, dimensions, isVideo):
|
||||
imageSize = dimensions.cgSize.aspectFitted(boundingImageSize)
|
||||
let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))
|
||||
imageApply()
|
||||
self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true, nilIfEmpty: true))
|
||||
|
||||
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
|
||||
|
||||
let animatedStickerNode: AnimatedStickerNode
|
||||
if let current = self.animatedStickerNode {
|
||||
animatedStickerNode = current
|
||||
} else {
|
||||
animatedStickerNode = DefaultAnimatedStickerNodeImpl()
|
||||
animatedStickerNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
self?.removePlaceholder(animated: false)
|
||||
}
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
self.containerNode.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode)
|
||||
} else {
|
||||
self.containerNode.addSubnode(animatedStickerNode)
|
||||
}
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: 128, height: 128, playbackMode: .loop, mode: .cached)
|
||||
}
|
||||
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||
}
|
||||
if let resourceReference = resourceReference {
|
||||
self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: resourceReference).start())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
var imageSize = PixelDimensions(width: 512, height: 512)
|
||||
var immediateThumbnailData: Data?
|
||||
if let data = info.immediateThumbnailData {
|
||||
if info.flags.contains(.isVideo) {
|
||||
imageSize = PixelDimensions(width: 100, height: 100)
|
||||
}
|
||||
immediateThumbnailData = data
|
||||
} else if let data = item?.file.immediateThumbnailData {
|
||||
immediateThumbnailData = data
|
||||
}
|
||||
|
||||
placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.15), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), data: immediateThumbnailData, size: boundingImageSize, enableEffect: true, imageSize: imageSize.cgSize)
|
||||
}
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize)
|
||||
|
||||
self.imageNode.bounds = CGRect(origin: CGPoint(), size: imageSize)
|
||||
self.imageNode.position = CGPoint(x: boundingSize.height / 2.0, y: boundingSize.width / 2.0)
|
||||
if let animatedStickerNode = self.animatedStickerNode {
|
||||
animatedStickerNode.frame = self.imageNode.frame
|
||||
animatedStickerNode.updateLayout(size: self.imageNode.frame.size)
|
||||
}
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
placeholderNode.bounds = CGRect(origin: CGPoint(), size: boundingImageSize)
|
||||
placeholderNode.position = self.imageNode.position
|
||||
}
|
||||
|
||||
let unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(theme)
|
||||
if unread {
|
||||
self.unreadNode.isHidden = false
|
||||
} else {
|
||||
self.unreadNode.isHidden = true
|
||||
}
|
||||
if let image = unreadImage {
|
||||
self.unreadNode.image = image
|
||||
self.unreadNode.frame = CGRect(origin: CGPoint(x: 35.0, y: 4.0), size: image.size)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
placeholderNode.updateAbsoluteRect(rect, within: containerSize)
|
||||
}
|
||||
}
|
||||
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class StickerPaneTrendingListGridItem: GridItem {
|
||||
let account: Account
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let trendingPacks: [FeaturedStickerPackItem]
|
||||
let isPremium: Bool
|
||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
let dismiss: (() -> Void)?
|
||||
|
||||
let section: GridSection? = nil
|
||||
let fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)?
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, trendingPacks: [FeaturedStickerPackItem], isPremium: Bool, inputNodeInteraction: ChatMediaInputNodeInteraction, dismiss: (() -> Void)?) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.trendingPacks = trendingPacks
|
||||
self.isPremium = isPremium
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
self.dismiss = dismiss
|
||||
self.fillsRowWithDynamicHeight = { _ in
|
||||
return 70.0
|
||||
}
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = StickerPaneTrendingListGridItemNode()
|
||||
node.setup(item: self)
|
||||
return node
|
||||
}
|
||||
|
||||
func update(node: GridItemNode) {
|
||||
guard let node = node as? StickerPaneTrendingListGridItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
node.setup(item: self)
|
||||
}
|
||||
}
|
||||
|
||||
private let titleFont = Font.medium(12.0)
|
||||
|
||||
class StickerPaneTrendingListGridItemNode: GridItemNode {
|
||||
private let titleNode: TextNode
|
||||
private let dismissButtonNode: HighlightTrackingButtonNode
|
||||
|
||||
private let listView: ListView
|
||||
|
||||
private var item: StickerPaneTrendingListGridItem?
|
||||
private var appliedItem: StickerPaneTrendingListGridItem?
|
||||
|
||||
private var isPanelVisible = false
|
||||
override var isVisibleInGrid: Bool {
|
||||
didSet {
|
||||
self.updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private let disposable = MetaDisposable()
|
||||
private var currentEntries: [Entry] = []
|
||||
|
||||
override init() {
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.dismissButtonNode = HighlightTrackingButtonNode()
|
||||
|
||||
self.listView = ListView()
|
||||
self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0)
|
||||
self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.listView)
|
||||
self.addSubnode(self.dismissButtonNode)
|
||||
|
||||
self.dismissButtonNode.addTarget(self, action: #selector(self.dismissPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
private func enqueuePanelTransition(_ transition: Transition, firstTime: Bool) {
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
if firstTime {
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
} else {
|
||||
options.insert(.AnimateInsertion)
|
||||
}
|
||||
|
||||
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
}
|
||||
|
||||
func setup(item: StickerPaneTrendingListGridItem) {
|
||||
self.item = item
|
||||
|
||||
let entries = panelEntries(featuredPacks: item.trendingPacks, theme: item.theme)
|
||||
let transition = preparedEntryTransition(account: item.account, from: self.currentEntries, to: entries, inputNodeInteraction: item.inputNodeInteraction, isVisible: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.isPanelVisible && strongSelf.isVisibleInGrid
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
self.enqueuePanelTransition(transition, firstTime: self.currentEntries.isEmpty)
|
||||
self.currentEntries = entries
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
func updateIsPanelVisible(_ isPanelVisible: Bool) {
|
||||
if self.isPanelVisible != isPanelVisible {
|
||||
self.isPanelVisible = isPanelVisible
|
||||
self.updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
func updateVisibility() {
|
||||
self.listView.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? FeaturedPackItemNode {
|
||||
itemNode.updateVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
||||
let params = ListViewItemLayoutParams(width: self.bounds.size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: self.bounds.size.height)
|
||||
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
let currentItem = self.appliedItem
|
||||
self.appliedItem = item
|
||||
|
||||
let width = self.bounds.size.width
|
||||
|
||||
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width)
|
||||
self.listView.position = CGPoint(x: width / 2.0, y: 26.0 + 41.0 / 2.0)
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: .immediate)
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: self.bounds.size.width), insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), duration: duration, curve: curve)
|
||||
|
||||
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
self.dismissButtonNode.setImage(PresentationResourcesChat.chatInputMediaPanelGridDismissImage(item.theme, color: item.theme.chat.inputMediaPanel.stickersSectionTextColor), for: [])
|
||||
}
|
||||
|
||||
let leftInset: CGFloat = 9.0
|
||||
let rightInset: CGFloat = 18.0 + UIScreenPixel
|
||||
let topOffset: CGFloat = 9.0
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: (item.isPremium ? item.strings.Stickers_TrendingPremiumStickers : item.strings.StickerPacksSettings_FeaturedPacks).uppercased(), font: titleFont, textColor: item.theme.chat.inputMediaPanel.stickersSectionTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - leftInset - rightInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
self.item = item
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: params.leftInset + leftInset, y: topOffset), size: titleLayout.size)
|
||||
let dismissButtonSize = CGSize(width: 12.0, height: 12.0)
|
||||
self.dismissButtonNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightInset - dismissButtonSize.width + 1.0, y: topOffset - 1.0), size: dismissButtonSize)
|
||||
self.dismissButtonNode.isHidden = item.dismiss == nil
|
||||
self.titleNode.frame = titleFrame
|
||||
}
|
||||
|
||||
@objc private func dismissPressed() {
|
||||
if let item = self.item {
|
||||
item.dismiss?()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,239 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import StickerResources
|
||||
import AccountContext
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
final class StickersChatInputContextPanelItem: ListViewItem {
|
||||
let account: Account
|
||||
let theme: PresentationTheme
|
||||
let index: Int
|
||||
let files: [TelegramMediaFile]
|
||||
let itemsInRow: Int
|
||||
let stickersInteraction: StickersChatInputContextPanelInteraction
|
||||
let interfaceInteraction: ChatPanelInterfaceInteraction
|
||||
|
||||
let selectable: Bool = false
|
||||
|
||||
public init(account: Account, theme: PresentationTheme, index: Int, files: [TelegramMediaFile], itemsInRow: Int, stickersInteraction: StickersChatInputContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.index = index
|
||||
self.files = files
|
||||
self.itemsInRow = itemsInRow
|
||||
self.stickersInteraction = stickersInteraction
|
||||
self.interfaceInteraction = interfaceInteraction
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
let configure = { () -> Void in
|
||||
let node = StickersChatInputContextPanelItemNode()
|
||||
|
||||
let nodeLayout = node.asyncLayout()
|
||||
let (top, bottom) = (previousItem != nil, nextItem != nil)
|
||||
let (layout, apply) = nodeLayout(self, params, top, bottom)
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply(.None) })
|
||||
})
|
||||
}
|
||||
}
|
||||
if Thread.isMainThread {
|
||||
async {
|
||||
configure()
|
||||
}
|
||||
} else {
|
||||
configure()
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? StickersChatInputContextPanelItemNode {
|
||||
let nodeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (top, bottom) = (previousItem != nil, nextItem != nil)
|
||||
|
||||
let (layout, apply) = nodeLayout(self, params, top, bottom)
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply(animation)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let itemSize = CGSize(width: 66.0, height: 66.0)
|
||||
private let inset: CGFloat = 3.0
|
||||
|
||||
final class StickersChatInputContextPanelItemNode: ListViewItemNode {
|
||||
private let topSeparatorNode: ASDisplayNode
|
||||
private var nodes: [TransformImageNode] = []
|
||||
private var item: StickersChatInputContextPanelItem?
|
||||
private let disposables = DisposableSet()
|
||||
|
||||
private var currentPreviewingIndex: Int?
|
||||
|
||||
init() {
|
||||
self.topSeparatorNode = ASDisplayNode()
|
||||
self.topSeparatorNode.isLayerBacked = true
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposables.dispose()
|
||||
}
|
||||
|
||||
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
if let item = item as? StickersChatInputContextPanelItem {
|
||||
let doLayout = self.asyncLayout()
|
||||
let merged = (top: previousItem != nil, bottom: nextItem != nil)
|
||||
let (layout, apply) = doLayout(item, params, merged.top, merged.bottom)
|
||||
self.contentSize = layout.contentSize
|
||||
self.insets = layout.insets
|
||||
apply(.None)
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
let location = gestureRecognizer.location(in: gestureRecognizer.view)
|
||||
for i in 0 ..< self.nodes.count {
|
||||
if self.nodes[i].frame.contains(location) {
|
||||
let file = item.files[i]
|
||||
let _ = item.interfaceInteraction.sendSticker(.standalone(media: file), true, self.nodes[i].view, self.nodes[i].bounds, nil, [])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stickerItem(at index: Int) -> StickerPackItem? {
|
||||
guard let item = self.item else {
|
||||
return nil
|
||||
}
|
||||
if index < item.files.count {
|
||||
return StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.files[index], indexKeys: [])
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func stickerItem(at location: CGPoint) -> (StickerPackItem, ASDisplayNode)? {
|
||||
guard let item = self.item else {
|
||||
return nil
|
||||
}
|
||||
for i in 0 ..< self.nodes.count {
|
||||
if self.nodes[i].frame.contains(location) {
|
||||
return (StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: item.files[i], indexKeys: []), self.nodes[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updatePreviewing(animated: Bool) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
||||
var previewingIndex: Int? = nil
|
||||
for i in 0 ..< item.files.count {
|
||||
if item.stickersInteraction.previewedStickerItem?.fileId == self.stickerItem(at: i)?.file.fileId {
|
||||
previewingIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if self.currentPreviewingIndex != previewingIndex {
|
||||
self.currentPreviewingIndex = previewingIndex
|
||||
|
||||
for i in 0 ..< self.nodes.count {
|
||||
let layer = self.nodes[i].layer
|
||||
if i == previewingIndex {
|
||||
layer.transform = CATransform3DMakeScale(0.8, 0.8, 1.0)
|
||||
if animated {
|
||||
let scale = ((layer.presentation()?.value(forKeyPath: "transform.scale") as? NSNumber)?.floatValue ?? (layer.value(forKeyPath: "transform.scale") as? NSNumber)?.floatValue) ?? 1.0
|
||||
layer.animateSpring(from: scale as NSNumber, to: 0.8 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||
}
|
||||
} else {
|
||||
layer.transform = CATransform3DIdentity
|
||||
if animated {
|
||||
let scale = ((layer.presentation()?.value(forKeyPath: "transform.scale") as? NSNumber)?.floatValue ?? (layer.value(forKeyPath: "transform.scale") as? NSNumber)?.floatValue) ?? 0.8
|
||||
layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: StickersChatInputContextPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
||||
return { [weak self] item, params, mergedTop, mergedBottom in
|
||||
let baseWidth = params.width - params.leftInset - params.rightInset
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 66.0), insets: UIEdgeInsets())
|
||||
|
||||
return (nodeLayout, { _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.backgroundColor = item.theme.list.plainBackgroundColor
|
||||
strongSelf.topSeparatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.item = item
|
||||
|
||||
if item.index == 0 && strongSelf.topSeparatorNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.topSeparatorNode)
|
||||
}
|
||||
strongSelf.topSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: UIScreenPixel))
|
||||
|
||||
let spacing = (baseWidth - itemSize.width * CGFloat(item.itemsInRow)) / (CGFloat(max(1, item.itemsInRow + 1)))
|
||||
|
||||
var i = 0
|
||||
for file in item.files {
|
||||
let imageNode: TransformImageNode
|
||||
if strongSelf.nodes.count > i {
|
||||
imageNode = strongSelf.nodes[i]
|
||||
} else {
|
||||
imageNode = TransformImageNode()
|
||||
strongSelf.nodes.append(imageNode)
|
||||
strongSelf.addSubnode(imageNode)
|
||||
}
|
||||
|
||||
imageNode.setSignal(chatMessageSticker(account: item.account, userLocation: .other, file: file, small: true))
|
||||
strongSelf.disposables.add(freeMediaFileResourceInteractiveFetched(account: item.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: true)).start())
|
||||
|
||||
var imageSize = itemSize
|
||||
if let dimensions = file.dimensions {
|
||||
imageSize = dimensions.cgSize.aspectFitted(CGSize(width: itemSize.width - 4.0, height: itemSize.height - 4.0))
|
||||
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
}
|
||||
|
||||
imageNode.frame = CGRect(x: spacing + params.leftInset + (itemSize.width + spacing) * CGFloat(i) + floor((itemSize.width - imageSize.width) / 2.0), y: floor((itemSize.height - imageSize.height) / 2.0), width: imageSize.width, height: imageSize.height)
|
||||
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,406 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import MergeLists
|
||||
import AccountContext
|
||||
import StickerPackPreviewUI
|
||||
import StickerPeekUI
|
||||
import ContextUI
|
||||
import ChatPresentationInterfaceState
|
||||
import PremiumUI
|
||||
import UndoUI
|
||||
import ChatControllerInteraction
|
||||
|
||||
private struct StickersChatInputContextPanelEntryStableId: Hashable {
|
||||
let ids: [MediaId]
|
||||
}
|
||||
|
||||
final class StickersChatInputContextPanelInteraction {
|
||||
var previewedStickerItem: TelegramMediaFile?
|
||||
}
|
||||
|
||||
private struct StickersChatInputContextPanelEntry: Identifiable, Comparable {
|
||||
let index: Int
|
||||
let theme: PresentationTheme
|
||||
let files: [TelegramMediaFile]
|
||||
let itemsInRow: Int
|
||||
|
||||
var stableId: StickersChatInputContextPanelEntryStableId {
|
||||
return StickersChatInputContextPanelEntryStableId(ids: files.compactMap { $0.id })
|
||||
}
|
||||
|
||||
static func ==(lhs: StickersChatInputContextPanelEntry, rhs: StickersChatInputContextPanelEntry) -> Bool {
|
||||
return lhs.index == rhs.index && lhs.stableId == rhs.stableId
|
||||
}
|
||||
|
||||
static func <(lhs: StickersChatInputContextPanelEntry, rhs: StickersChatInputContextPanelEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func withUpdatedTheme(_ theme: PresentationTheme) -> StickersChatInputContextPanelEntry {
|
||||
return StickersChatInputContextPanelEntry(index: self.index, theme: theme, files: self.files, itemsInRow: itemsInRow)
|
||||
}
|
||||
|
||||
func item(account: Account, stickersInteraction: StickersChatInputContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> ListViewItem {
|
||||
return StickersChatInputContextPanelItem(account: account, theme: self.theme, index: self.index, files: self.files, itemsInRow: self.itemsInRow, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private struct StickersChatInputContextPanelTransition {
|
||||
let deletions: [ListViewDeleteItem]
|
||||
let insertions: [ListViewInsertItem]
|
||||
let updates: [ListViewUpdateItem]
|
||||
}
|
||||
|
||||
private func preparedTransition(from fromEntries: [StickersChatInputContextPanelEntry], to toEntries: [StickersChatInputContextPanelEntry], account: Account, stickersInteraction: StickersChatInputContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> StickersChatInputContextPanelTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction), directionHint: nil) }
|
||||
|
||||
return StickersChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates)
|
||||
}
|
||||
|
||||
private let itemSize = CGSize(width: 66.0, height: 66.0)
|
||||
|
||||
final class StickersChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
private let strings: PresentationStrings
|
||||
|
||||
private let listView: ListView
|
||||
private var results: [TelegramMediaFile] = []
|
||||
private var currentEntries: [StickersChatInputContextPanelEntry]?
|
||||
|
||||
private var enqueuedTransitions: [(StickersChatInputContextPanelTransition, Bool)] = []
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState)?
|
||||
|
||||
public var controllerInteraction: ChatControllerInteraction?
|
||||
private let stickersInteraction: StickersChatInputContextPanelInteraction
|
||||
|
||||
private var stickerPreviewController: StickerPreviewController?
|
||||
|
||||
override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) {
|
||||
self.strings = strings
|
||||
|
||||
self.listView = ListView()
|
||||
self.listView.isOpaque = false
|
||||
self.listView.stackFromBottom = true
|
||||
self.listView.keepBottomItemOverscrollBackground = theme.list.plainBackgroundColor
|
||||
self.listView.limitHitTestToNodes = true
|
||||
self.listView.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.listView.accessibilityPageScrolledString = { row, count in
|
||||
return strings.VoiceOver_ScrollStatus(row, count).string
|
||||
}
|
||||
|
||||
self.stickersInteraction = StickersChatInputContextPanelInteraction()
|
||||
|
||||
super.init(context: context, theme: theme, strings: strings, fontSize: fontSize, chatPresentationContext: chatPresentationContext)
|
||||
|
||||
self.isOpaque = false
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.listView)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
||||
if let strongSelf = self {
|
||||
let convertedPoint = strongSelf.listView.view.convert(point, from: strongSelf.view)
|
||||
guard strongSelf.listView.bounds.contains(convertedPoint) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var stickersNode: StickersChatInputContextPanelItemNode?
|
||||
strongSelf.listView.forEachVisibleItemNode({ itemNode in
|
||||
if itemNode.frame.contains(convertedPoint), let node = itemNode as? StickersChatInputContextPanelItemNode {
|
||||
stickersNode = node
|
||||
}
|
||||
})
|
||||
|
||||
if let stickersNode = stickersNode {
|
||||
let point = strongSelf.listView.view.convert(point, to: stickersNode.view)
|
||||
if let (item, itemNode) = stickersNode.stickerItem(at: point) {
|
||||
return strongSelf.context.engine.stickers.isStickerSaved(id: item.file.fileId)
|
||||
|> deliverOnMainQueue
|
||||
|> map { isStarred -> (UIView, CGRect, PeekControllerContent)? in
|
||||
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
|
||||
var menuItems: [ContextMenuItem] = []
|
||||
menuItems = [
|
||||
.action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
f(.default)
|
||||
|
||||
let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode.view, itemNode.bounds, nil, [])
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
if let strongSelf = self {
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = (strongSelf.context.engine.stickers.toggleStickerSaved(file: item.file, saved: !isStarred)
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
switch result {
|
||||
case .generic:
|
||||
strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil)
|
||||
case let .limitExceeded(limit, premiumLimit):
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 })
|
||||
let text: String
|
||||
if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
|
||||
text = strongSelf.strings.Premium_MaxFavedStickersFinalText
|
||||
} else {
|
||||
text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
|
||||
}
|
||||
strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
|
||||
if let strongSelf = self {
|
||||
if case .info = action {
|
||||
let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers)
|
||||
strongSelf.controllerInteraction?.navigationController()?.pushViewController(controller)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}), nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
||||
f(.default)
|
||||
|
||||
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
|
||||
loop: for attribute in item.file.attributes {
|
||||
switch attribute {
|
||||
case let .Sticker(_, packReference, _):
|
||||
if let packReference = packReference {
|
||||
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in
|
||||
if let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction {
|
||||
return controllerInteraction.sendSticker(file, false, false, nil, true, sourceNode, sourceRect, nil, [])
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
controllerInteraction.navigationController()?.view.window?.endEditing(true)
|
||||
controllerInteraction.presentController(controller, nil)
|
||||
}
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
]
|
||||
return (itemNode.view, itemNode.bounds, StickerPreviewPeekContent(context: strongSelf.context, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item.file), menu: menuItems, openPremiumIntro: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let controller = PremiumIntroScreen(context: strongSelf.context, source: .stickers)
|
||||
strongSelf.controllerInteraction?.navigationController()?.pushViewController(controller)
|
||||
}))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, present: { [weak self] content, sourceView, sourceRect in
|
||||
if let strongSelf = self {
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = PeekController(presentationData: presentationData, content: content, sourceView: {
|
||||
return (sourceView, sourceRect)
|
||||
})
|
||||
strongSelf.interfaceInteraction?.presentGlobalOverlayController(controller, nil)
|
||||
return controller
|
||||
}
|
||||
return nil
|
||||
}, updateContent: { [weak self] content in
|
||||
if let strongSelf = self {
|
||||
var item: TelegramMediaFile?
|
||||
if let content = content as? StickerPreviewPeekContent, case let .pack(contentItem) = content.item {
|
||||
item = contentItem
|
||||
}
|
||||
strongSelf.updatePreviewingItem(file: item, animated: true)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
private func updatePreviewingItem(file: TelegramMediaFile?, animated: Bool) {
|
||||
if self.stickersInteraction.previewedStickerItem?.fileId != file?.fileId {
|
||||
self.stickersInteraction.previewedStickerItem = file
|
||||
|
||||
self.listView.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? StickersChatInputContextPanelItemNode {
|
||||
itemNode.updatePreviewing(animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateResults(_ results: [TelegramMediaFile]) {
|
||||
self.results = results
|
||||
|
||||
self.commitResults(updateLayout: true)
|
||||
}
|
||||
|
||||
private func commitResults(updateLayout: Bool = false) {
|
||||
guard let validLayout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
var entries: [StickersChatInputContextPanelEntry] = []
|
||||
|
||||
let itemsInRow = Int(floor((validLayout.0.width - validLayout.1 - validLayout.2) / itemSize.width))
|
||||
|
||||
var files: [TelegramMediaFile] = []
|
||||
var index = entries.count
|
||||
for i in 0 ..< self.results.count {
|
||||
files.append(results[i])
|
||||
if files.count == itemsInRow {
|
||||
entries.append(StickersChatInputContextPanelEntry(index: index, theme: self.theme, files: files, itemsInRow: itemsInRow))
|
||||
index += 1
|
||||
files.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
if !files.isEmpty {
|
||||
entries.append(StickersChatInputContextPanelEntry(index: index, theme: self.theme, files: files, itemsInRow: itemsInRow))
|
||||
}
|
||||
|
||||
if updateLayout {
|
||||
self.updateLayout(size: validLayout.0, leftInset: validLayout.1, rightInset: validLayout.2, bottomInset: validLayout.3, transition: .immediate, interfaceState: validLayout.4)
|
||||
}
|
||||
|
||||
self.prepareTransition(from: self.currentEntries, to: entries)
|
||||
}
|
||||
|
||||
private func prepareTransition(from: [StickersChatInputContextPanelEntry]? , to: [StickersChatInputContextPanelEntry]) {
|
||||
let firstTime = from == nil
|
||||
let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, stickersInteraction: self.stickersInteraction, interfaceInteraction: self.interfaceInteraction!)
|
||||
self.currentEntries = to
|
||||
self.enqueueTransition(transition, firstTime: firstTime)
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: StickersChatInputContextPanelTransition, firstTime: Bool) {
|
||||
self.enqueuedTransitions.append((transition, firstTime))
|
||||
|
||||
if self.validLayout != nil {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
if firstTime {
|
||||
//options.insert(.Synchronous)
|
||||
//options.insert(.LowLatency)
|
||||
} else {
|
||||
options.insert(.AnimateTopItemPosition)
|
||||
options.insert(.AnimateCrossfade)
|
||||
}
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
insets.top = topInsetForLayout(size: validLayout.0)
|
||||
insets.left = validLayout.1
|
||||
insets.right = validLayout.2
|
||||
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: validLayout.0, insets: insets, duration: 0.0, curve: .Default(duration: nil))
|
||||
|
||||
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: updateSizeAndInsets, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
if let strongSelf = self, firstTime {
|
||||
var topItemOffset: CGFloat?
|
||||
strongSelf.listView.forEachItemNode { itemNode in
|
||||
if topItemOffset == nil {
|
||||
topItemOffset = itemNode.frame.minY
|
||||
}
|
||||
}
|
||||
|
||||
if let topItemOffset = topItemOffset {
|
||||
let position = strongSelf.listView.layer.position
|
||||
strongSelf.listView.position = CGPoint(x: position.x, y: position.y + (strongSelf.listView.bounds.size.height - topItemOffset))
|
||||
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring).animateView {
|
||||
strongSelf.listView.position = position
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func topInsetForLayout(size: CGSize) -> CGFloat {
|
||||
let minimumItemHeights: CGFloat = floor(itemSize.height * 1.5)
|
||||
|
||||
return max(size.height - minimumItemHeights, 0.0)
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) {
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
self.validLayout = (size, leftInset, rightInset, bottomInset, interfaceState)
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
insets.top = self.topInsetForLayout(size: size)
|
||||
insets.left = leftInset
|
||||
insets.right = rightInset
|
||||
|
||||
transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: size, insets: insets, duration: duration, curve: curve)
|
||||
|
||||
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
self.commitResults(updateLayout: false)
|
||||
|
||||
if !hadValidLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
|
||||
if self.theme !== interfaceState.theme {
|
||||
self.theme = interfaceState.theme
|
||||
self.listView.keepBottomItemOverscrollBackground = self.theme.list.plainBackgroundColor
|
||||
|
||||
let new = self.currentEntries?.map({$0.withUpdatedTheme(interfaceState.theme)}) ?? []
|
||||
self.prepareTransition(from: self.currentEntries, to: new)
|
||||
}
|
||||
}
|
||||
|
||||
override func animateOut(completion: @escaping () -> Void) {
|
||||
var topItemOffset: CGFloat?
|
||||
self.listView.forEachItemNode { itemNode in
|
||||
if topItemOffset == nil {
|
||||
topItemOffset = itemNode.frame.minY
|
||||
}
|
||||
}
|
||||
|
||||
if let topItemOffset = topItemOffset {
|
||||
let position = self.listView.layer.position
|
||||
self.listView.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + (self.listView.bounds.size.height - topItemOffset)), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let listViewFrame = self.listView.frame
|
||||
return self.listView.hitTest(CGPoint(x: point.x - listViewFrame.minX, y: point.y - listViewFrame.minY), with: event)
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// TelegramUI.h
|
||||
// TelegramUI
|
||||
//
|
||||
// Created by Peter on 5/3/17.
|
||||
// Copyright © 2017 Telegram. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
//! Project version number for TelegramUI.
|
||||
FOUNDATION_EXPORT double TelegramUIVersionNumber;
|
||||
|
||||
//! Project version string for TelegramUI.
|
||||
FOUNDATION_EXPORT const unsigned char TelegramUIVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <TelegramUI/PublicHeader.h>
|
||||
|
Loading…
x
Reference in New Issue
Block a user