This commit is contained in:
Ali 2023-02-21 22:12:36 +04:00
parent 32bf3ad1ca
commit fa93c6d791
20 changed files with 18 additions and 6846 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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