mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
Added recent stickers clearing Added sending logs via email Added forward recipient change on forward acccessory panel tap Tweaked undo panel design Various UI fixes
1626 lines
93 KiB
Swift
1626 lines
93 KiB
Swift
import Foundation
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
|
|
private struct PeerSpecificPackData {
|
|
let peer: Peer
|
|
let info: StickerPackCollectionInfo
|
|
let items: [ItemCollectionItem]
|
|
}
|
|
|
|
private enum CanInstallPeerSpecificPack {
|
|
case none
|
|
case available(peer: Peer, dismissed: Bool)
|
|
}
|
|
|
|
private struct ChatMediaInputPanelTransition {
|
|
let deletions: [ListViewDeleteItem]
|
|
let insertions: [ListViewInsertItem]
|
|
let updates: [ListViewUpdateItem]
|
|
}
|
|
|
|
private struct ChatMediaInputGridTransition {
|
|
let deletions: [Int]
|
|
let insertions: [GridNodeInsertItem]
|
|
let updates: [GridNodeUpdateItem]
|
|
let updateFirstIndexInSectionOffset: Int?
|
|
let stationaryItems: GridNodeStationaryItems
|
|
let scrollToItem: GridNodeScrollToItem?
|
|
let updateOpaqueState: ChatMediaInputStickerPaneOpaqueState?
|
|
let animated: Bool
|
|
}
|
|
|
|
private func preparedChatMediaInputPanelEntryTransition(account: Account, from fromEntries: [ChatMediaInputPanelEntry], to toEntries: [ChatMediaInputPanelEntry], inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputPanelTransition {
|
|
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), directionHint: nil) }
|
|
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, inputNodeInteraction: inputNodeInteraction), directionHint: nil) }
|
|
|
|
return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates)
|
|
}
|
|
|
|
private func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemCollectionsView, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ChatMediaInputGridTransition {
|
|
var stationaryItems: GridNodeStationaryItems = .none
|
|
var scrollToItem: GridNodeScrollToItem?
|
|
var animated = false
|
|
switch update {
|
|
case .initial:
|
|
for i in (0 ..< toEntries.count).reversed() {
|
|
switch toEntries[i] {
|
|
case .search, .peerSpecificSetup:
|
|
break
|
|
case .sticker:
|
|
scrollToItem = GridNodeScrollToItem(index: i, position: .top(0.0), transition: .immediate, directionHint: .down, adjustForSection: true, adjustForTopInset: true)
|
|
}
|
|
}
|
|
case .generic:
|
|
animated = true
|
|
case .scroll:
|
|
var fromStableIds = Set<ChatMediaInputGridEntryStableId>()
|
|
for entry in fromEntries {
|
|
fromStableIds.insert(entry.stableId)
|
|
}
|
|
var index = 0
|
|
var indices = Set<Int>()
|
|
for entry in toEntries {
|
|
if fromStableIds.contains(entry.stableId) {
|
|
indices.insert(index)
|
|
}
|
|
index += 1
|
|
}
|
|
stationaryItems = .indices(indices)
|
|
case let .navigate(index, collectionId):
|
|
if let index = index.flatMap({ ChatMediaInputGridEntryIndex.collectionIndex($0) }) {
|
|
for i in 0 ..< toEntries.count {
|
|
if toEntries[i].index >= index {
|
|
var directionHint: GridNodePreviousItemsTransitionDirectionHint = .up
|
|
if !fromEntries.isEmpty && fromEntries[0].index < toEntries[i].index {
|
|
directionHint = .down
|
|
}
|
|
scrollToItem = GridNodeScrollToItem(index: i, position: .top(0.0), transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true, adjustForTopInset: true)
|
|
break
|
|
}
|
|
}
|
|
} else if !toEntries.isEmpty {
|
|
if let collectionId = collectionId {
|
|
for i in 0 ..< toEntries.count {
|
|
var indexMatches = false
|
|
switch toEntries[i].index {
|
|
case let .collectionIndex(collectionIndex):
|
|
if collectionIndex.collectionId == collectionId {
|
|
indexMatches = true
|
|
}
|
|
case .peerSpecificSetup:
|
|
if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue {
|
|
indexMatches = true
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
if indexMatches {
|
|
var directionHint: GridNodePreviousItemsTransitionDirectionHint = .up
|
|
if !fromEntries.isEmpty && fromEntries[0].index < toEntries[i].index {
|
|
directionHint = .down
|
|
}
|
|
scrollToItem = GridNodeScrollToItem(index: i, position: .top(0.0), transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true, adjustForTopInset: true)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if scrollToItem == nil {
|
|
scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .animated(duration: 0.45, curve: .spring), directionHint: .up, adjustForSection: true, adjustForTopInset: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
|
|
|
let deletions = deleteIndices
|
|
let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) }
|
|
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction)) }
|
|
|
|
var firstIndexInSectionOffset = 0
|
|
if !toEntries.isEmpty {
|
|
switch toEntries[0].index {
|
|
case .search, .peerSpecificSetup:
|
|
break
|
|
case let .collectionIndex(index):
|
|
firstIndexInSectionOffset = Int(index.itemIndex.index)
|
|
}
|
|
}
|
|
|
|
if case .initial = update {
|
|
switch toEntries[0].index {
|
|
case .search:
|
|
if toEntries.count > 1 {
|
|
//scrollToItem = GridNodeScrollToItem(index: 1, position: .top, transition: .immediate, directionHint: .up, adjustForSection: true)
|
|
}
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
let opaqueState = ChatMediaInputStickerPaneOpaqueState(hasLower: view.lower != nil)
|
|
|
|
return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, updateOpaqueState: opaqueState, animated: animated)
|
|
}
|
|
|
|
private func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasUnreadTrending: Bool, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] {
|
|
var entries: [ChatMediaInputPanelEntry] = []
|
|
entries.append(.recentGifs(theme))
|
|
if hasUnreadTrending {
|
|
entries.append(.trending(true, theme))
|
|
}
|
|
if let savedStickers = savedStickers, !savedStickers.items.isEmpty {
|
|
entries.append(.savedStickers(theme))
|
|
}
|
|
var savedStickerIds = Set<Int64>()
|
|
if let savedStickers = savedStickers, !savedStickers.items.isEmpty {
|
|
for i in 0 ..< savedStickers.items.count {
|
|
if let item = savedStickers.items[i].contents as? SavedStickerItem {
|
|
savedStickerIds.insert(item.file.fileId.id)
|
|
}
|
|
}
|
|
}
|
|
if let recentStickers = recentStickers, !recentStickers.items.isEmpty {
|
|
var found = false
|
|
for item in recentStickers.items {
|
|
if let item = item.contents as? RecentMediaItem, let _ = item.media as? TelegramMediaFile, let mediaId = item.media.id {
|
|
if !savedStickerIds.contains(mediaId.id) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if found {
|
|
entries.append(.recentPacks(theme))
|
|
}
|
|
}
|
|
if let peerSpecificPack = peerSpecificPack {
|
|
entries.append(.peerSpecific(theme: theme, peer: peerSpecificPack.peer))
|
|
} else if case let .available(peer, false) = canInstallPeerSpecificPack {
|
|
entries.append(.peerSpecific(theme: theme, peer: peer))
|
|
}
|
|
var index = 0
|
|
for (_, info, item) in view.collectionInfos {
|
|
if let info = info as? StickerPackCollectionInfo {
|
|
entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem, theme: theme))
|
|
index += 1
|
|
}
|
|
}
|
|
|
|
if peerSpecificPack == nil, case let .available(peer, true) = canInstallPeerSpecificPack {
|
|
entries.append(.peerSpecific(theme: theme, peer: peer))
|
|
}
|
|
|
|
if !hasUnreadTrending {
|
|
entries.append(.trending(false, theme))
|
|
}
|
|
entries.append(.settings(theme))
|
|
return entries
|
|
}
|
|
|
|
private func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] {
|
|
var entries: [ChatMediaInputGridEntry] = []
|
|
|
|
if view.lower == nil {
|
|
entries.append(.search(theme: theme, strings: strings))
|
|
}
|
|
|
|
var stickerPackInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:]
|
|
for (id, info, _) in view.collectionInfos {
|
|
if let info = info as? StickerPackCollectionInfo {
|
|
stickerPackInfos[id] = info
|
|
}
|
|
}
|
|
|
|
if view.lower == nil {
|
|
var savedStickerIds = Set<Int64>()
|
|
if let savedStickers = savedStickers, !savedStickers.items.isEmpty {
|
|
let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_FavoriteStickers.uppercased(), shortName: "", thumbnail: nil, hash: 0, count: 0)
|
|
for i in 0 ..< savedStickers.items.count {
|
|
if let item = savedStickers.items[i].contents as? SavedStickerItem {
|
|
savedStickerIds.insert(item.file.fileId.id)
|
|
let index = ItemCollectionItemIndex(index: Int32(i), id: item.file.fileId.id)
|
|
let stickerItem = StickerPackItem(index: index, file: item.file, indexKeys: [])
|
|
entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -3, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, canManagePeerSpecificPack: nil, theme: theme))
|
|
}
|
|
}
|
|
}
|
|
|
|
if let recentStickers = recentStickers, !recentStickers.items.isEmpty {
|
|
let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_FrequentlyUsed.uppercased(), shortName: "", thumbnail: nil, hash: 0, count: 0)
|
|
var addedCount = 0
|
|
for i in 0 ..< recentStickers.items.count {
|
|
if addedCount >= 20 {
|
|
break
|
|
}
|
|
if let item = recentStickers.items[i].contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile, let mediaId = item.media.id {
|
|
if !savedStickerIds.contains(mediaId.id) {
|
|
let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id)
|
|
let stickerItem = StickerPackItem(index: index, file: file, indexKeys: [])
|
|
entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -2, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, canManagePeerSpecificPack: nil, theme: theme))
|
|
addedCount += 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var canManagePeerSpecificPack = false
|
|
if case .available(_, false) = canInstallPeerSpecificPack {
|
|
canManagePeerSpecificPack = true
|
|
}
|
|
|
|
if peerSpecificPack == nil && canManagePeerSpecificPack {
|
|
entries.append(.peerSpecificSetup(theme: theme, strings: strings, dismissed: false))
|
|
}
|
|
|
|
if let peerSpecificPack = peerSpecificPack {
|
|
for i in 0 ..< peerSpecificPack.items.count {
|
|
let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_GroupStickers, shortName: "", thumbnail: nil, hash: 0, count: 0)
|
|
|
|
if let item = peerSpecificPack.items[i] as? StickerPackItem {
|
|
let index = ItemCollectionItemIndex(index: Int32(i), id: item.file.fileId.id)
|
|
let stickerItem = StickerPackItem(index: index, file: item.file, indexKeys: [])
|
|
entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, canManagePeerSpecificPack: canManagePeerSpecificPack, theme: theme))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for entry in view.entries {
|
|
if let item = entry.item as? StickerPackItem {
|
|
entries.append(.sticker(index: entry.index, stickerItem: item, stickerPackInfo: stickerPackInfos[entry.index.collectionId], canManagePeerSpecificPack: false, theme: theme))
|
|
}
|
|
}
|
|
|
|
if view.higher == nil {
|
|
if peerSpecificPack == nil, case .available(_, true) = canInstallPeerSpecificPack {
|
|
entries.append(.peerSpecificSetup(theme: theme, strings: strings, dismissed: true))
|
|
}
|
|
}
|
|
return entries
|
|
}
|
|
|
|
private enum StickerPacksCollectionPosition: Equatable {
|
|
case initial
|
|
case scroll(aroundIndex: ItemCollectionViewEntryIndex?)
|
|
case navigate(index: ItemCollectionViewEntryIndex?, collectionId: ItemCollectionId?)
|
|
|
|
static func ==(lhs: StickerPacksCollectionPosition, rhs: StickerPacksCollectionPosition) -> Bool {
|
|
switch lhs {
|
|
case .initial:
|
|
if case .initial = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .scroll(lhsAroundIndex):
|
|
if case let .scroll(rhsAroundIndex) = rhs, lhsAroundIndex == rhsAroundIndex {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .navigate:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum StickerPacksCollectionUpdate {
|
|
case initial
|
|
case generic
|
|
case scroll
|
|
case navigate(ItemCollectionViewEntryIndex?, ItemCollectionId?)
|
|
}
|
|
|
|
final class ChatMediaInputNodeInteraction {
|
|
let navigateToCollectionId: (ItemCollectionId) -> Void
|
|
let openSettings: () -> Void
|
|
let toggleSearch: (Bool, ChatMediaInputSearchMode?) -> Void
|
|
let openPeerSpecificSettings: () -> Void
|
|
let dismissPeerSpecificSettings: () -> Void
|
|
let clearRecentlyUsedStickers: () -> Void
|
|
|
|
var highlightedStickerItemCollectionId: ItemCollectionId?
|
|
var highlightedItemCollectionId: ItemCollectionId?
|
|
var previewedStickerPackItem: StickerPreviewPeekItem?
|
|
var appearanceTransition: CGFloat = 1.0
|
|
|
|
init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) {
|
|
self.navigateToCollectionId = navigateToCollectionId
|
|
self.openSettings = openSettings
|
|
self.toggleSearch = toggleSearch
|
|
self.openPeerSpecificSettings = openPeerSpecificSettings
|
|
self.dismissPeerSpecificSettings = dismissPeerSpecificSettings
|
|
self.clearRecentlyUsedStickers = clearRecentlyUsedStickers
|
|
}
|
|
}
|
|
|
|
private func clipScrollPosition(_ position: StickerPacksCollectionPosition) -> StickerPacksCollectionPosition {
|
|
switch position {
|
|
case let .scroll(index):
|
|
if let index = index, index.collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue || index.collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue {
|
|
return .scroll(aroundIndex: nil)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
return position
|
|
}
|
|
|
|
private enum ChatMediaInputPaneType {
|
|
case gifs
|
|
case stickers
|
|
case trending
|
|
}
|
|
|
|
private struct ChatMediaInputPaneArrangement {
|
|
let panes: [ChatMediaInputPaneType]
|
|
let currentIndex: Int
|
|
let indexTransition: CGFloat
|
|
|
|
func withIndexTransition(_ indexTransition: CGFloat) -> ChatMediaInputPaneArrangement {
|
|
return ChatMediaInputPaneArrangement(panes: self.panes, currentIndex: currentIndex, indexTransition: indexTransition)
|
|
}
|
|
|
|
func withCurrentIndex(_ currentIndex: Int) -> ChatMediaInputPaneArrangement {
|
|
return ChatMediaInputPaneArrangement(panes: self.panes, currentIndex: currentIndex, indexTransition: self.indexTransition)
|
|
}
|
|
}
|
|
|
|
private final class CollectionListContainerNode: ASDisplayNode {
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
for subview in self.view.subviews {
|
|
if let result = subview.hitTest(point.offsetBy(dx: -subview.frame.minX, dy: -subview.frame.minY), with: event) {
|
|
return result
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
final class ChatMediaInputNode: ChatInputNode {
|
|
private let context: AccountContext
|
|
private let peerId: PeerId?
|
|
private let controllerInteraction: ChatControllerInteraction
|
|
private let gifPaneIsActiveUpdated: (Bool) -> Void
|
|
|
|
private var inputNodeInteraction: ChatMediaInputNodeInteraction!
|
|
|
|
private let collectionListPanel: ASDisplayNode
|
|
private let collectionListSeparator: ASDisplayNode
|
|
private let collectionListContainer: CollectionListContainerNode
|
|
|
|
private let disposable = MetaDisposable()
|
|
|
|
private let listView: ListView
|
|
private var searchContainerNode: PaneSearchContainerNode?
|
|
private let searchContainerNodeLoadedDisposable = MetaDisposable()
|
|
|
|
private let stickerPane: ChatMediaInputStickerPane
|
|
private var animatingStickerPaneOut = false
|
|
private let gifPane: ChatMediaInputGifPane
|
|
private var animatingGifPaneOut = false
|
|
private let trendingPane: ChatMediaInputTrendingPane
|
|
private var animatingTrendingPaneOut = false
|
|
|
|
private var panRecognizer: UIPanGestureRecognizer?
|
|
|
|
private let itemCollectionsViewPosition = Promise<StickerPacksCollectionPosition>()
|
|
private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition?
|
|
private var currentView: ItemCollectionsView?
|
|
private let dismissedPeerSpecificStickerPack = Promise<Bool>()
|
|
|
|
private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState, Bool)?
|
|
private var paneArrangement: ChatMediaInputPaneArrangement
|
|
private var initializedArrangement = false
|
|
|
|
private var theme: PresentationTheme
|
|
private var strings: PresentationStrings
|
|
private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)>
|
|
|
|
private let _ready = Promise<Void>()
|
|
private var didSetReady = false
|
|
override var ready: Signal<Void, NoError> {
|
|
return self._ready.get()
|
|
}
|
|
|
|
init(context: AccountContext, peerId: PeerId?, controllerInteraction: ChatControllerInteraction, theme: PresentationTheme, strings: PresentationStrings, gifPaneIsActiveUpdated: @escaping (Bool) -> Void) {
|
|
self.context = context
|
|
self.peerId = peerId
|
|
self.controllerInteraction = controllerInteraction
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.gifPaneIsActiveUpdated = gifPaneIsActiveUpdated
|
|
|
|
self.themeAndStringsPromise = Promise((theme, strings))
|
|
|
|
self.collectionListPanel = ASDisplayNode()
|
|
self.collectionListPanel.clipsToBounds = true
|
|
self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColor
|
|
|
|
self.collectionListSeparator = ASDisplayNode()
|
|
self.collectionListSeparator.isLayerBacked = true
|
|
self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor
|
|
|
|
self.collectionListContainer = CollectionListContainerNode()
|
|
self.collectionListContainer.clipsToBounds = true
|
|
|
|
self.listView = ListView()
|
|
self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0)
|
|
|
|
var paneDidScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void)?
|
|
var fixPaneScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void)?
|
|
|
|
self.stickerPane = ChatMediaInputStickerPane(theme: theme, strings: strings, paneDidScroll: { pane, state, transition in
|
|
paneDidScrollImpl?(pane, state, transition)
|
|
}, fixPaneScroll: { pane, state in
|
|
fixPaneScrollImpl?(pane, state)
|
|
})
|
|
self.gifPane = ChatMediaInputGifPane(account: context.account, theme: theme, strings: strings, controllerInteraction: controllerInteraction, paneDidScroll: { pane, state, transition in
|
|
paneDidScrollImpl?(pane, state, transition)
|
|
}, fixPaneScroll: { pane, state in
|
|
fixPaneScrollImpl?(pane, state)
|
|
})
|
|
|
|
var getItemIsPreviewedImpl: ((StickerPackItem) -> Bool)?
|
|
self.trendingPane = ChatMediaInputTrendingPane(context: context, controllerInteraction: controllerInteraction, getItemIsPreviewed: { item in
|
|
return getItemIsPreviewedImpl?(item) ?? false
|
|
})
|
|
|
|
self.paneArrangement = ChatMediaInputPaneArrangement(panes: [.gifs, .stickers, .trending], currentIndex: 1, indexTransition: 0.0)
|
|
|
|
super.init()
|
|
|
|
self.inputNodeInteraction = ChatMediaInputNodeInteraction(navigateToCollectionId: { [weak self] collectionId in
|
|
if let strongSelf = self, let currentView = strongSelf.currentView, (collectionId != strongSelf.inputNodeInteraction.highlightedItemCollectionId || true) {
|
|
var index: Int32 = 0
|
|
if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue {
|
|
strongSelf.setCurrentPane(.gifs, transition: .animated(duration: 0.25, curve: .spring))
|
|
} else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue {
|
|
strongSelf.setCurrentPane(.trending, transition: .animated(duration: 0.25, curve: .spring))
|
|
} else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue {
|
|
strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring), collectionIdHint: collectionId.namespace)
|
|
strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId)
|
|
strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId)))
|
|
} else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue {
|
|
strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring), collectionIdHint: collectionId.namespace)
|
|
strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId)
|
|
strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId)))
|
|
} else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue {
|
|
strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring))
|
|
strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId)
|
|
strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId)))
|
|
} else {
|
|
strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring))
|
|
for (id, _, _) in currentView.collectionInfos {
|
|
if id.namespace == collectionId.namespace {
|
|
if id == collectionId {
|
|
let itemIndex = ItemCollectionViewEntryIndex.lowerBound(collectionIndex: index, collectionId: id)
|
|
strongSelf.currentStickerPacksCollectionPosition = .navigate(index: itemIndex, collectionId: nil)
|
|
strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: itemIndex, collectionId: nil)))
|
|
break
|
|
}
|
|
index += 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, openSettings: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction.navigationController()?.pushViewController(installedStickerPacksController(context: context, mode: .modal))
|
|
}
|
|
}, toggleSearch: { [weak self] value, searchMode in
|
|
if let strongSelf = self {
|
|
if let searchMode = searchMode, value {
|
|
var searchContainerNode: PaneSearchContainerNode?
|
|
if let current = strongSelf.searchContainerNode {
|
|
searchContainerNode = current
|
|
} else {
|
|
searchContainerNode = PaneSearchContainerNode(context: strongSelf.context, theme: strongSelf.theme, strings: strongSelf.strings, controllerInteraction: strongSelf.controllerInteraction, inputNodeInteraction: strongSelf.inputNodeInteraction, mode: searchMode, trendingGifsPromise: strongSelf.gifPane.trendingPromise, cancel: {
|
|
self?.searchContainerNode?.deactivate()
|
|
self?.inputNodeInteraction.toggleSearch(false, nil)
|
|
})
|
|
strongSelf.searchContainerNode = searchContainerNode
|
|
}
|
|
if let searchContainerNode = searchContainerNode {
|
|
strongSelf.searchContainerNodeLoadedDisposable.set((searchContainerNode.ready
|
|
|> deliverOnMainQueue).start(next: {
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction.updateInputMode { current in
|
|
switch current {
|
|
case let .media(mode, _):
|
|
return .media(mode: mode, expanded: .search(searchMode))
|
|
default:
|
|
return current
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
} else {
|
|
strongSelf.controllerInteraction.updateInputMode { current in
|
|
switch current {
|
|
case let .media(mode, _):
|
|
return .media(mode: mode, expanded: nil)
|
|
default:
|
|
return current
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, openPeerSpecificSettings: { [weak self] in
|
|
guard let peerId = peerId, peerId.namespace == Namespaces.Peer.CloudChannel else {
|
|
return
|
|
}
|
|
|
|
let _ = (context.account.postbox.transaction { transaction -> StickerPackCollectionInfo? in
|
|
return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.stickerPack
|
|
}
|
|
|> deliverOnMainQueue).start(next: { info in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.controllerInteraction.presentController(groupStickerPackSetupController(context: context, peerId: peerId, currentPackInfo: info), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
})
|
|
}, dismissPeerSpecificSettings: { [weak self] in
|
|
self?.dismissPeerSpecificPackSetup()
|
|
}, clearRecentlyUsedStickers: { [weak self] in
|
|
if let strongSelf = self {
|
|
let actionSheet = ActionSheetController(presentationTheme: strongSelf.theme)
|
|
var items: [ActionSheetItem] = []
|
|
items.append(ActionSheetButtonItem(title: strongSelf.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
let _ = (context.account.postbox.transaction { transaction in
|
|
clearRecentlyUsedStickers(transaction: transaction)
|
|
}).start()
|
|
}))
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
strongSelf.controllerInteraction.presentController(actionSheet, nil)
|
|
}
|
|
})
|
|
|
|
getItemIsPreviewedImpl = { [weak self] item in
|
|
if let strongSelf = self {
|
|
return strongSelf.inputNodeInteraction.previewedStickerPackItem == .pack(item)
|
|
}
|
|
return false
|
|
}
|
|
|
|
self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor
|
|
|
|
self.collectionListPanel.addSubnode(self.listView)
|
|
self.collectionListContainer.addSubnode(self.collectionListPanel)
|
|
self.collectionListContainer.addSubnode(self.collectionListSeparator)
|
|
self.addSubnode(self.collectionListContainer)
|
|
|
|
let itemCollectionsView = self.itemCollectionsViewPosition.get()
|
|
|> distinctUntilChanged
|
|
|> mapToSignal { position -> Signal<(ItemCollectionsView, StickerPacksCollectionUpdate), NoError> in
|
|
switch position {
|
|
case .initial:
|
|
var firstTime = true
|
|
return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50)
|
|
|> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in
|
|
let update: StickerPacksCollectionUpdate
|
|
if firstTime {
|
|
firstTime = false
|
|
update = .initial
|
|
} else {
|
|
update = .generic
|
|
}
|
|
return (view, update)
|
|
}
|
|
case let .scroll(aroundIndex):
|
|
var firstTime = true
|
|
return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 300)
|
|
|> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in
|
|
let update: StickerPacksCollectionUpdate
|
|
if firstTime {
|
|
firstTime = false
|
|
update = .scroll
|
|
} else {
|
|
update = .generic
|
|
}
|
|
return (view, update)
|
|
}
|
|
case let .navigate(index, collectionId):
|
|
var firstTime = true
|
|
return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index, count: 300)
|
|
|> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in
|
|
let update: StickerPacksCollectionUpdate
|
|
if firstTime {
|
|
firstTime = false
|
|
update = .navigate(index, collectionId)
|
|
} else {
|
|
update = .generic
|
|
}
|
|
return (view, update)
|
|
}
|
|
}
|
|
}
|
|
|
|
let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], []))
|
|
|
|
let inputNodeInteraction = self.inputNodeInteraction!
|
|
let peerSpecificPack: Signal<(PeerSpecificPackData?, CanInstallPeerSpecificPack), NoError>
|
|
if let peerId = peerId {
|
|
self.dismissedPeerSpecificStickerPack.set(context.account.postbox.transaction { transaction -> Bool in
|
|
guard let state = transaction.getPeerChatInterfaceState(peerId) as? ChatInterfaceState else {
|
|
return false
|
|
}
|
|
if state.messageActionsState.closedPeerSpecificPackSetup {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
})
|
|
peerSpecificPack = combineLatest(peerSpecificStickerPack(postbox: context.account.postbox, network: context.account.network, peerId: peerId), context.account.postbox.multiplePeersView([peerId]), self.dismissedPeerSpecificStickerPack.get())
|
|
|> map { packData, peersView, dismissedPeerSpecificPack -> (PeerSpecificPackData?, CanInstallPeerSpecificPack) in
|
|
if let peer = peersView.peers[peerId] {
|
|
var canInstall: CanInstallPeerSpecificPack = .none
|
|
if packData.canSetup {
|
|
canInstall = .available(peer: peer, dismissed: dismissedPeerSpecificPack)
|
|
}
|
|
if let (info, items) = packData.packInfo {
|
|
return (PeerSpecificPackData(peer: peer, info: info, items: items), canInstall)
|
|
} else {
|
|
return (nil, canInstall)
|
|
}
|
|
}
|
|
return (nil, .none)
|
|
}
|
|
} else {
|
|
peerSpecificPack = .single((nil, .none))
|
|
}
|
|
|
|
let hasUnreadTrending = context.account.viewTracker.featuredStickerPacks()
|
|
|> map { packs -> Bool in
|
|
for pack in packs {
|
|
if pack.unread {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let previousView = Atomic<ItemCollectionsView?>(value: nil)
|
|
let transitions = combineLatest(itemCollectionsView, peerSpecificPack, hasUnreadTrending, self.themeAndStringsPromise.get())
|
|
|> map { viewAndUpdate, peerSpecificPack, hasUnreadTrending, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in
|
|
let (view, viewUpdate) = viewAndUpdate
|
|
let previous = previousView.swap(view)
|
|
var update = viewUpdate
|
|
if previous === view {
|
|
update = .generic
|
|
}
|
|
let (theme, strings) = themeAndStrings
|
|
|
|
var savedStickers: OrderedItemListView?
|
|
var recentStickers: OrderedItemListView?
|
|
for orderedView in view.orderedItemListsViews {
|
|
if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers {
|
|
recentStickers = orderedView
|
|
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers {
|
|
savedStickers = orderedView
|
|
}
|
|
}
|
|
let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, hasUnreadTrending: hasUnreadTrending, theme: theme)
|
|
let gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, strings: strings, theme: theme)
|
|
let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries))
|
|
return (view, preparedChatMediaInputPanelEntryTransition(account: context.account, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty)
|
|
}
|
|
|
|
self.disposable.set((transitions
|
|
|> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, panelFirstTime, gridTransition, gridFirstTime) in
|
|
if let strongSelf = self {
|
|
strongSelf.currentView = view
|
|
strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime)
|
|
if !strongSelf.initializedArrangement {
|
|
strongSelf.initializedArrangement = true
|
|
var currentPane = strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex]
|
|
if view.entries.isEmpty {
|
|
currentPane = .trending
|
|
}
|
|
if currentPane != strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] {
|
|
strongSelf.setCurrentPane(currentPane, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
|
|
self.stickerPane.gridNode.visibleItemsUpdated = { [weak self] visibleItems in
|
|
if let strongSelf = self {
|
|
var topVisibleCollectionId: ItemCollectionId?
|
|
|
|
if let topVisibleSection = visibleItems.topSectionVisible as? ChatMediaInputStickerGridSection {
|
|
topVisibleCollectionId = topVisibleSection.collectionId
|
|
} else if let topVisible = visibleItems.topVisible {
|
|
if let item = topVisible.1 as? ChatMediaInputStickerGridItem {
|
|
topVisibleCollectionId = item.index.collectionId
|
|
} else if let _ = topVisible.1 as? StickerPanePeerSpecificSetupGridItem {
|
|
topVisibleCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0)
|
|
}
|
|
}
|
|
if let collectionId = topVisibleCollectionId {
|
|
if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId {
|
|
strongSelf.setHighlightedItemCollectionId(collectionId)
|
|
}
|
|
}
|
|
|
|
if let currentView = strongSelf.currentView, let (topIndex, topItem) = visibleItems.top, let (bottomIndex, bottomItem) = visibleItems.bottom {
|
|
if topIndex <= 10 && currentView.lower != nil {
|
|
let position: StickerPacksCollectionPosition = clipScrollPosition(.scroll(aroundIndex: (topItem as! ChatMediaInputStickerGridItem).index))
|
|
if strongSelf.currentStickerPacksCollectionPosition != position {
|
|
strongSelf.currentStickerPacksCollectionPosition = position
|
|
strongSelf.itemCollectionsViewPosition.set(.single(position))
|
|
}
|
|
} else if bottomIndex >= visibleItems.count - 10 && currentView.higher != nil {
|
|
var position: StickerPacksCollectionPosition?
|
|
if let bottomItem = bottomItem as? ChatMediaInputStickerGridItem {
|
|
position = clipScrollPosition(.scroll(aroundIndex: bottomItem.index))
|
|
}
|
|
|
|
if let position = position, strongSelf.currentStickerPacksCollectionPosition != position {
|
|
strongSelf.currentStickerPacksCollectionPosition = position
|
|
strongSelf.itemCollectionsViewPosition.set(.single(position))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.currentStickerPacksCollectionPosition = .initial
|
|
self.itemCollectionsViewPosition.set(.single(.initial))
|
|
|
|
self.stickerPane.inputNodeInteraction = self.inputNodeInteraction
|
|
self.gifPane.inputNodeInteraction = self.inputNodeInteraction
|
|
|
|
paneDidScrollImpl = { [weak self] pane, state, transition in
|
|
self?.updatePaneDidScroll(pane: pane, state: state, transition: transition)
|
|
}
|
|
|
|
fixPaneScrollImpl = { [weak self] pane, state in
|
|
self?.fixPaneScroll(pane: pane, state: state)
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
self.disposable.dispose()
|
|
self.searchContainerNodeLoadedDisposable.dispose()
|
|
}
|
|
|
|
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
|
if self.theme !== theme || self.strings !== strings {
|
|
self.theme = theme
|
|
self.strings = strings
|
|
|
|
self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColor
|
|
self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor
|
|
self.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor
|
|
|
|
self.searchContainerNode?.updateThemeAndStrings(theme: theme, strings: strings)
|
|
|
|
self.stickerPane.updateThemeAndStrings(theme: theme, strings: strings)
|
|
self.gifPane.updateThemeAndStrings(theme: theme, strings: strings)
|
|
self.trendingPane.updateThemeAndStrings(theme: theme, strings: strings)
|
|
|
|
self.themeAndStringsPromise.set(.single((theme, strings)))
|
|
}
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
|
self.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
|
if let strongSelf = self {
|
|
let panes: [ASDisplayNode]
|
|
if let searchContainerNode = strongSelf.searchContainerNode {
|
|
panes = []
|
|
|
|
if let (itemNode, item) = searchContainerNode.itemAt(point: point.offsetBy(dx: -searchContainerNode.frame.minX, dy: -searchContainerNode.frame.minY)) {
|
|
if let item = item as? StickerPreviewPeekItem {
|
|
return strongSelf.context.account.postbox.transaction { transaction -> Bool in
|
|
return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId)
|
|
}
|
|
|> deliverOnMainQueue
|
|
|> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in
|
|
if let strongSelf = self {
|
|
var menuItems: [PeekControllerMenuItem] = []
|
|
menuItems = [
|
|
PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: {
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false)
|
|
}
|
|
}),
|
|
PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: {
|
|
if let strongSelf = self {
|
|
if isStarred {
|
|
let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start()
|
|
} else {
|
|
let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start()
|
|
}
|
|
}
|
|
}),
|
|
PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: {
|
|
if let strongSelf = self {
|
|
loop: for attribute in item.file.attributes {
|
|
switch attribute {
|
|
case let .Sticker(_, packReference, _):
|
|
if let packReference = packReference {
|
|
let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: strongSelf.controllerInteraction.navigationController())
|
|
controller.sendSticker = { file in
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction.sendSticker(file, false)
|
|
}
|
|
}
|
|
|
|
strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true)
|
|
strongSelf.controllerInteraction.presentController(controller, nil)
|
|
}
|
|
break loop
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: {})
|
|
]
|
|
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems))
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
} else if let file = item as? FileMediaReference {
|
|
return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.context.account, contextResult: .internalReference(queryId: 0, id: "", type: "gif", title: nil, description: nil, image: nil, file: file.media, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [
|
|
PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: {
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction.sendGif(file)
|
|
}
|
|
}),
|
|
PeekControllerMenuItem(title: strongSelf.strings.Preview_SaveGif, color: .accent, action: {
|
|
if let strongSelf = self {
|
|
let _ = addSavedGif(postbox: strongSelf.context.account.postbox, fileReference: file).start()
|
|
}
|
|
})
|
|
])))
|
|
}
|
|
}
|
|
} else {
|
|
panes = [strongSelf.gifPane, strongSelf.stickerPane, strongSelf.trendingPane]
|
|
}
|
|
for pane in panes {
|
|
if pane.supernode != nil, pane.frame.contains(point) {
|
|
if let pane = pane as? ChatMediaInputGifPane {
|
|
if let file = pane.fileAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) {
|
|
return .single((strongSelf, ChatContextResultPeekContent(account: strongSelf.context.account, contextResult: .internalReference(queryId: 0, id: "", type: "gif", title: nil, description: nil, image: nil, file: file.media, message: .auto(caption: "", entities: nil, replyMarkup: nil)), menu: [
|
|
PeekControllerMenuItem(title: strongSelf.strings.ShareMenu_Send, color: .accent, font: .bold, action: {
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction.sendGif(file)
|
|
}
|
|
}),
|
|
PeekControllerMenuItem(title: strongSelf.strings.Common_Delete, color: .destructive, action: {
|
|
if let strongSelf = self {
|
|
let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.media.fileId).start()
|
|
}
|
|
})
|
|
])))
|
|
}
|
|
} else if pane is ChatMediaInputStickerPane || pane is ChatMediaInputTrendingPane {
|
|
var itemNodeAndItem: (ASDisplayNode, StickerPackItem)?
|
|
if let pane = pane as? ChatMediaInputStickerPane {
|
|
itemNodeAndItem = pane.itemAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY))
|
|
} else if let pane = pane as? ChatMediaInputTrendingPane {
|
|
itemNodeAndItem = pane.itemAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY))
|
|
}
|
|
|
|
if let (itemNode, item) = itemNodeAndItem {
|
|
return strongSelf.context.account.postbox.transaction { transaction -> Bool in
|
|
return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId)
|
|
}
|
|
|> deliverOnMainQueue
|
|
|> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in
|
|
if let strongSelf = self {
|
|
var menuItems: [PeekControllerMenuItem] = []
|
|
menuItems = [
|
|
PeekControllerMenuItem(title: strongSelf.strings.StickerPack_Send, color: .accent, font: .bold, action: {
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false)
|
|
}
|
|
}),
|
|
PeekControllerMenuItem(title: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: {
|
|
if let strongSelf = self {
|
|
if isStarred {
|
|
let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start()
|
|
} else {
|
|
let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start()
|
|
}
|
|
}
|
|
}),
|
|
PeekControllerMenuItem(title: strongSelf.strings.StickerPack_ViewPack, color: .accent, action: {
|
|
if let strongSelf = self {
|
|
loop: for attribute in item.file.attributes {
|
|
switch attribute {
|
|
case let .Sticker(_, packReference, _):
|
|
if let packReference = packReference {
|
|
let controller = StickerPackPreviewController(context: strongSelf.context, stickerPack: packReference, parentNavigationController: strongSelf.controllerInteraction.navigationController())
|
|
controller.sendSticker = { file in
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction.sendSticker(file, false)
|
|
}
|
|
}
|
|
|
|
strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true)
|
|
strongSelf.controllerInteraction.presentController(controller, nil)
|
|
}
|
|
break loop
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
PeekControllerMenuItem(title: strongSelf.strings.Common_Cancel, color: .accent, action: {})
|
|
]
|
|
return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems))
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}, present: { [weak self] content, sourceNode in
|
|
if let strongSelf = self {
|
|
let controller = PeekController(theme: PeekControllerTheme(presentationTheme: strongSelf.theme), content: content, sourceNode: {
|
|
return sourceNode
|
|
})
|
|
strongSelf.controllerInteraction.presentGlobalOverlayController(controller, nil)
|
|
return controller
|
|
}
|
|
return nil
|
|
}, updateContent: { [weak self] content in
|
|
if let strongSelf = self {
|
|
var item: StickerPreviewPeekItem?
|
|
if let content = content as? StickerPreviewPeekContent {
|
|
item = content.item
|
|
}
|
|
strongSelf.updatePreviewingItem(item: item, animated: true)
|
|
}
|
|
}))
|
|
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
|
self.panRecognizer = panRecognizer
|
|
self.view.addGestureRecognizer(panRecognizer)
|
|
}
|
|
|
|
private func setCurrentPane(_ pane: ChatMediaInputPaneType, transition: ContainedViewLayoutTransition, collectionIdHint: Int32? = nil) {
|
|
if let index = self.paneArrangement.panes.index(of: pane), index != self.paneArrangement.currentIndex {
|
|
let previousGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs
|
|
self.paneArrangement = self.paneArrangement.withIndexTransition(0.0).withCurrentIndex(index)
|
|
if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, isVisible) = self.validLayout {
|
|
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, isVisible: isVisible)
|
|
self.updateAppearanceTransition(transition: transition)
|
|
}
|
|
let updatedGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs
|
|
if updatedGifPanelWasActive != previousGifPanelWasActive {
|
|
self.gifPaneIsActiveUpdated(updatedGifPanelWasActive)
|
|
}
|
|
switch pane {
|
|
case .gifs:
|
|
self.setHighlightedItemCollectionId(ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue, id: 0))
|
|
case .stickers:
|
|
if let highlightedStickerCollectionId = self.inputNodeInteraction.highlightedStickerItemCollectionId {
|
|
self.setHighlightedItemCollectionId(highlightedStickerCollectionId)
|
|
} else if let collectionIdHint = collectionIdHint {
|
|
self.setHighlightedItemCollectionId(ItemCollectionId(namespace: collectionIdHint, id: 0))
|
|
}
|
|
case .trending:
|
|
self.setHighlightedItemCollectionId(ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue, id: 0))
|
|
}
|
|
} else {
|
|
if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, isVisible) = self.validLayout {
|
|
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, isVisible: isVisible)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setHighlightedItemCollectionId(_ collectionId: ItemCollectionId) {
|
|
if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue {
|
|
if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs {
|
|
self.inputNodeInteraction.highlightedItemCollectionId = collectionId
|
|
}
|
|
} else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue {
|
|
if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .trending {
|
|
self.inputNodeInteraction.highlightedItemCollectionId = collectionId
|
|
}
|
|
} else {
|
|
self.inputNodeInteraction.highlightedStickerItemCollectionId = collectionId
|
|
if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .stickers {
|
|
self.inputNodeInteraction.highlightedItemCollectionId = collectionId
|
|
}
|
|
}
|
|
var ensuredNodeVisible = false
|
|
var firstVisibleCollectionId: ItemCollectionId?
|
|
self.listView.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode {
|
|
if firstVisibleCollectionId == nil {
|
|
firstVisibleCollectionId = itemNode.currentCollectionId
|
|
}
|
|
itemNode.updateIsHighlighted()
|
|
if itemNode.currentCollectionId == collectionId {
|
|
self.listView.ensureItemNodeVisible(itemNode)
|
|
ensuredNodeVisible = true
|
|
}
|
|
} else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode {
|
|
itemNode.updateIsHighlighted()
|
|
if itemNode.currentCollectionId == collectionId {
|
|
self.listView.ensureItemNodeVisible(itemNode)
|
|
ensuredNodeVisible = true
|
|
}
|
|
} else if let itemNode = itemNode as? ChatMediaInputRecentGifsItemNode {
|
|
itemNode.updateIsHighlighted()
|
|
if itemNode.currentCollectionId == collectionId {
|
|
self.listView.ensureItemNodeVisible(itemNode)
|
|
ensuredNodeVisible = true
|
|
}
|
|
} else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode {
|
|
itemNode.updateIsHighlighted()
|
|
if itemNode.currentCollectionId == collectionId {
|
|
self.listView.ensureItemNodeVisible(itemNode)
|
|
ensuredNodeVisible = true
|
|
}
|
|
} else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode {
|
|
itemNode.updateIsHighlighted()
|
|
if itemNode.currentCollectionId == collectionId {
|
|
self.listView.ensureItemNodeVisible(itemNode)
|
|
ensuredNodeVisible = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if let currentView = self.currentView, let firstVisibleCollectionId = firstVisibleCollectionId, !ensuredNodeVisible {
|
|
let targetIndex = currentView.collectionInfos.index(where: { id, _, _ in return id == collectionId })
|
|
let firstVisibleIndex = currentView.collectionInfos.index(where: { id, _, _ in return id == firstVisibleCollectionId })
|
|
if let targetIndex = targetIndex, let firstVisibleIndex = firstVisibleIndex {
|
|
let toRight = targetIndex > firstVisibleIndex
|
|
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func currentCollectionListPanelOffset() -> CGFloat {
|
|
let paneOffsets = self.paneArrangement.panes.map { pane -> CGFloat in
|
|
switch pane {
|
|
case .stickers:
|
|
return self.stickerPane.collectionListPanelOffset
|
|
case .gifs:
|
|
return self.gifPane.collectionListPanelOffset
|
|
case .trending:
|
|
return self.trendingPane.collectionListPanelOffset
|
|
}
|
|
}
|
|
|
|
let mainOffset = paneOffsets[self.paneArrangement.currentIndex]
|
|
if self.paneArrangement.indexTransition.isZero {
|
|
return mainOffset
|
|
} else {
|
|
var sideOffset: CGFloat?
|
|
if self.paneArrangement.indexTransition < 0.0 {
|
|
if self.paneArrangement.currentIndex != 0 {
|
|
sideOffset = paneOffsets[self.paneArrangement.currentIndex - 1]
|
|
}
|
|
} else {
|
|
if self.paneArrangement.currentIndex != paneOffsets.count - 1 {
|
|
sideOffset = paneOffsets[self.paneArrangement.currentIndex + 1]
|
|
}
|
|
}
|
|
if let sideOffset = sideOffset {
|
|
let interpolator = CGFloat.interpolator()
|
|
let value = interpolator(mainOffset, sideOffset, abs(self.paneArrangement.indexTransition)) as! CGFloat
|
|
return value
|
|
} else {
|
|
return mainOffset
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateAppearanceTransition(transition: ContainedViewLayoutTransition) {
|
|
var value: CGFloat = 1.0 - abs(self.currentCollectionListPanelOffset() / 41.0)
|
|
value = min(1.0, max(0.0, value))
|
|
|
|
self.inputNodeInteraction.appearanceTransition = max(0.1, value)
|
|
transition.updateAlpha(node: self.listView, alpha: value)
|
|
self.listView.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode {
|
|
itemNode.updateAppearanceTransition(transition: transition)
|
|
} else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode {
|
|
itemNode.updateAppearanceTransition(transition: transition)
|
|
} else if let itemNode = itemNode as? ChatMediaInputRecentGifsItemNode {
|
|
itemNode.updateAppearanceTransition(transition: transition)
|
|
} else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode {
|
|
itemNode.updateAppearanceTransition(transition: transition)
|
|
} else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode {
|
|
itemNode.updateAppearanceTransition(transition: transition)
|
|
} else if let itemNode = itemNode as? ChatMediaInputSettingsItemNode {
|
|
itemNode.updateAppearanceTransition(transition: transition)
|
|
}
|
|
}
|
|
}
|
|
|
|
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, isVisible: Bool) -> (CGFloat, CGFloat) {
|
|
var searchMode: ChatMediaInputSearchMode?
|
|
if let (_, _, _, _, _, _, _, _, interfaceState, _) = self.validLayout, case let .media(_, maybeExpanded) = interfaceState.inputMode, let expanded = maybeExpanded, case let .search(mode) = expanded {
|
|
searchMode = mode
|
|
}
|
|
|
|
self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, isVisible)
|
|
|
|
if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings {
|
|
self.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings)
|
|
}
|
|
|
|
var displaySearch = false
|
|
let separatorHeight = UIScreenPixel
|
|
let panelHeight: CGFloat
|
|
|
|
var isExpanded: Bool = false
|
|
if case let .media(_, maybeExpanded) = interfaceState.inputMode, let expanded = maybeExpanded {
|
|
isExpanded = true
|
|
switch expanded {
|
|
case .content:
|
|
panelHeight = maximumHeight
|
|
case let .search(mode):
|
|
panelHeight = maximumHeight
|
|
displaySearch = true
|
|
searchMode = mode
|
|
}
|
|
self.stickerPane.collectionListPanelOffset = 0.0
|
|
self.gifPane.collectionListPanelOffset = 0.0
|
|
self.trendingPane.collectionListPanelOffset = 0.0
|
|
self.updateAppearanceTransition(transition: transition)
|
|
} else {
|
|
panelHeight = standardInputHeight
|
|
}
|
|
|
|
if displaySearch {
|
|
if let searchContainerNode = self.searchContainerNode {
|
|
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -inputPanelHeight), size: CGSize(width: width, height: panelHeight + inputPanelHeight))
|
|
if searchContainerNode.supernode != nil {
|
|
transition.updateFrame(node: searchContainerNode, frame: containerFrame)
|
|
searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: transition)
|
|
} else {
|
|
self.searchContainerNode = searchContainerNode
|
|
self.insertSubnode(searchContainerNode, belowSubnode: self.collectionListContainer)
|
|
searchContainerNode.frame = containerFrame
|
|
searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, transition: .immediate)
|
|
var placeholderNode: PaneSearchBarPlaceholderNode?
|
|
if let searchMode = searchMode {
|
|
switch searchMode {
|
|
case .gif:
|
|
placeholderNode = self.gifPane.searchPlaceholderNode
|
|
case .sticker:
|
|
self.stickerPane.gridNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? PaneSearchBarPlaceholderNode {
|
|
placeholderNode = itemNode
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let placeholderNode = placeholderNode {
|
|
searchContainerNode.animateIn(from: placeholderNode, transition: transition, completion: { [weak self] in
|
|
self?.gifPane.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let contentVerticalOffset: CGFloat = displaySearch ? -(inputPanelHeight + 41.0) : 0.0
|
|
|
|
let collectionListPanelOffset = self.currentCollectionListPanelOffset()
|
|
|
|
transition.updateFrame(node: self.collectionListContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: contentVerticalOffset), size: CGSize(width: width, height: max(0.0, 41.0 + UIScreenPixel))))
|
|
transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: CGSize(width: width, height: 41.0)))
|
|
transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight)))
|
|
|
|
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0, height: width)
|
|
transition.updatePosition(node: self.listView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0))
|
|
|
|
var duration: Double = 0.0
|
|
var curve: UInt = 0
|
|
switch transition {
|
|
case .immediate:
|
|
break
|
|
case let .animated(animationDuration, animationCurve):
|
|
duration = animationDuration
|
|
switch animationCurve {
|
|
case .easeInOut:
|
|
break
|
|
case .spring:
|
|
curve = 7
|
|
}
|
|
}
|
|
|
|
let listViewCurve: ListViewAnimationCurve
|
|
if curve == 7 {
|
|
listViewCurve = .Spring(duration: duration)
|
|
} else {
|
|
listViewCurve = .Default(duration: duration)
|
|
}
|
|
|
|
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0, height: width), insets: UIEdgeInsets(top: 4.0 + leftInset, left: 0.0, bottom: 4.0 + rightInset, right: 0.0), duration: duration, curve: listViewCurve)
|
|
|
|
self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
|
|
var visiblePanes: [(ChatMediaInputPaneType, CGFloat)] = []
|
|
|
|
var paneIndex = 0
|
|
for pane in self.paneArrangement.panes {
|
|
let paneOrigin = CGFloat(paneIndex - self.paneArrangement.currentIndex) * width - self.paneArrangement.indexTransition * width
|
|
if paneOrigin.isLess(than: width) && CGFloat(0.0).isLess(than: (paneOrigin + width)) {
|
|
visiblePanes.append((pane, paneOrigin))
|
|
}
|
|
paneIndex += 1
|
|
}
|
|
|
|
for (pane, paneOrigin) in visiblePanes {
|
|
let paneFrame = CGRect(origin: CGPoint(x: paneOrigin + leftInset, y: 0.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight))
|
|
switch pane {
|
|
case .gifs:
|
|
if self.gifPane.supernode == nil {
|
|
if !displaySearch {
|
|
self.insertSubnode(self.gifPane, belowSubnode: self.collectionListContainer)
|
|
if self.searchContainerNode == nil {
|
|
self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 0.0), size: CGSize(width: width, height: panelHeight))
|
|
}
|
|
}
|
|
}
|
|
if self.gifPane.frame != paneFrame {
|
|
self.gifPane.layer.removeAnimation(forKey: "position")
|
|
transition.updateFrame(node: self.gifPane, frame: paneFrame)
|
|
}
|
|
case .stickers:
|
|
if self.stickerPane.supernode == nil {
|
|
self.insertSubnode(self.stickerPane, belowSubnode: self.collectionListContainer)
|
|
self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight))
|
|
}
|
|
if self.stickerPane.frame != paneFrame {
|
|
self.stickerPane.layer.removeAnimation(forKey: "position")
|
|
transition.updateFrame(node: self.stickerPane, frame: paneFrame)
|
|
}
|
|
case .trending:
|
|
if self.trendingPane.supernode == nil {
|
|
self.insertSubnode(self.trendingPane, belowSubnode: self.collectionListContainer)
|
|
self.trendingPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight))
|
|
}
|
|
if self.trendingPane.frame != paneFrame {
|
|
self.trendingPane.layer.removeAnimation(forKey: "position")
|
|
transition.updateFrame(node: self.trendingPane, frame: paneFrame)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.gifPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, isExpanded: isExpanded, isVisible: isVisible, transition: transition)
|
|
|
|
self.stickerPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, isExpanded: isExpanded, isVisible: isVisible, transition: transition)
|
|
self.trendingPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, isExpanded: isExpanded, isVisible: isVisible, transition: transition)
|
|
|
|
if self.gifPane.supernode != nil {
|
|
if !visiblePanes.contains(where: { $0.0 == .gifs }) {
|
|
if case .animated = transition {
|
|
if !self.animatingGifPaneOut {
|
|
self.animatingGifPaneOut = true
|
|
var toLeft = false
|
|
if let index = self.paneArrangement.panes.index(of: .gifs), index < self.paneArrangement.currentIndex {
|
|
toLeft = true
|
|
}
|
|
transition.animatePosition(node: self.gifPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.gifPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in
|
|
if let strongSelf = self, value {
|
|
strongSelf.animatingGifPaneOut = false
|
|
strongSelf.gifPane.removeFromSupernode()
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
self.animatingGifPaneOut = false
|
|
self.gifPane.removeFromSupernode()
|
|
}
|
|
}
|
|
} else {
|
|
self.animatingGifPaneOut = false
|
|
}
|
|
|
|
if self.stickerPane.supernode != nil {
|
|
if !visiblePanes.contains(where: { $0.0 == .stickers }) {
|
|
if case .animated = transition {
|
|
if !self.animatingStickerPaneOut {
|
|
self.animatingStickerPaneOut = true
|
|
var toLeft = false
|
|
if let index = self.paneArrangement.panes.index(of: .stickers), index < self.paneArrangement.currentIndex {
|
|
toLeft = true
|
|
}
|
|
transition.animatePosition(node: self.stickerPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.stickerPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in
|
|
if let strongSelf = self, value {
|
|
strongSelf.animatingStickerPaneOut = false
|
|
strongSelf.stickerPane.removeFromSupernode()
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
self.animatingStickerPaneOut = false
|
|
self.stickerPane.removeFromSupernode()
|
|
}
|
|
}
|
|
} else {
|
|
self.animatingStickerPaneOut = false
|
|
}
|
|
|
|
if self.trendingPane.supernode != nil {
|
|
if !visiblePanes.contains(where: { $0.0 == .trending }) {
|
|
if case .animated = transition {
|
|
if !self.animatingTrendingPaneOut {
|
|
self.animatingTrendingPaneOut = true
|
|
var toLeft = false
|
|
if let index = self.paneArrangement.panes.index(of: .trending), index < self.paneArrangement.currentIndex {
|
|
toLeft = true
|
|
}
|
|
transition.animatePosition(node: self.trendingPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.trendingPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in
|
|
if let strongSelf = self, value {
|
|
strongSelf.animatingTrendingPaneOut = false
|
|
strongSelf.trendingPane.removeFromSupernode()
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
self.animatingTrendingPaneOut = false
|
|
self.trendingPane.removeFromSupernode()
|
|
}
|
|
}
|
|
} else {
|
|
self.animatingTrendingPaneOut = false
|
|
}
|
|
|
|
if !displaySearch, let searchContainerNode = self.searchContainerNode {
|
|
self.searchContainerNode = nil
|
|
self.searchContainerNodeLoadedDisposable.set(nil)
|
|
|
|
var paneIsEmpty = false
|
|
var placeholderNode: PaneSearchBarPlaceholderNode?
|
|
if let searchMode = searchMode {
|
|
switch searchMode {
|
|
case .gif:
|
|
placeholderNode = self.gifPane.searchPlaceholderNode
|
|
paneIsEmpty = self.gifPane.isEmpty
|
|
case .sticker:
|
|
self.stickerPane.gridNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? PaneSearchBarPlaceholderNode {
|
|
placeholderNode = itemNode
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let placeholderNode = placeholderNode {
|
|
searchContainerNode.animateOut(to: placeholderNode, animateOutSearchBar: !paneIsEmpty, transition: transition, completion: { [weak searchContainerNode] in
|
|
searchContainerNode?.removeFromSupernode()
|
|
})
|
|
} else {
|
|
searchContainerNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if let panRecognizer = self.panRecognizer, panRecognizer.isEnabled != !displaySearch {
|
|
panRecognizer.isEnabled = !displaySearch
|
|
}
|
|
|
|
return (standardInputHeight, max(0.0, panelHeight - standardInputHeight))
|
|
}
|
|
|
|
private func enqueuePanelTransition(_ transition: ChatMediaInputPanelTransition, firstTime: Bool, thenGridTransition gridTransition: ChatMediaInputGridTransition, gridFirstTime: 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, updateOpaqueState: nil, completion: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime)
|
|
if !strongSelf.didSetReady {
|
|
strongSelf.didSetReady = true
|
|
strongSelf._ready.set(.single(Void()))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
private func enqueueGridTransition(_ transition: ChatMediaInputGridTransition, firstTime: Bool) {
|
|
var itemTransition: ContainedViewLayoutTransition = .immediate
|
|
if transition.animated {
|
|
itemTransition = .animated(duration: 0.3, curve: .spring)
|
|
}
|
|
self.stickerPane.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset, updateOpaqueState: transition.updateOpaqueState), completion: { _ in })
|
|
}
|
|
|
|
private func updatePreviewingItem(item: StickerPreviewPeekItem?, animated: Bool) {
|
|
if self.inputNodeInteraction.previewedStickerPackItem != item {
|
|
self.inputNodeInteraction.previewedStickerPackItem = item
|
|
|
|
self.stickerPane.gridNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMediaInputStickerGridItemNode {
|
|
itemNode.updatePreviewing(animated: animated)
|
|
}
|
|
}
|
|
|
|
self.searchContainerNode?.contentNode.updatePreviewing(animated: animated)
|
|
self.trendingPane.updatePreviewing(animated: animated)
|
|
}
|
|
}
|
|
|
|
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .began:
|
|
if self.animatingGifPaneOut {
|
|
self.animatingGifPaneOut = false
|
|
self.gifPane.removeFromSupernode()
|
|
}
|
|
self.gifPane.layer.removeAllAnimations()
|
|
self.stickerPane.layer.removeAllAnimations()
|
|
if self.animatingStickerPaneOut {
|
|
self.animatingStickerPaneOut = false
|
|
self.stickerPane.removeFromSupernode()
|
|
}
|
|
self.trendingPane.layer.removeAllAnimations()
|
|
if self.animatingTrendingPaneOut {
|
|
self.animatingTrendingPaneOut = false
|
|
self.trendingPane.removeFromSupernode()
|
|
}
|
|
case .changed:
|
|
if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, isVisible) = self.validLayout {
|
|
let translationX = -recognizer.translation(in: self.view).x
|
|
var indexTransition = translationX / width
|
|
if self.paneArrangement.currentIndex == 0 {
|
|
indexTransition = max(0.0, indexTransition)
|
|
} else if self.paneArrangement.currentIndex == self.paneArrangement.panes.count - 1 {
|
|
indexTransition = min(0.0, indexTransition)
|
|
}
|
|
self.paneArrangement = self.paneArrangement.withIndexTransition(indexTransition)
|
|
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, isVisible: isVisible)
|
|
}
|
|
case .ended:
|
|
if let (width, _, _, _, _, _, _, _, _, _) = self.validLayout {
|
|
var updatedIndex = self.paneArrangement.currentIndex
|
|
if abs(self.paneArrangement.indexTransition * width) > 30.0 {
|
|
if self.paneArrangement.indexTransition < 0.0 {
|
|
updatedIndex = max(0, self.paneArrangement.currentIndex - 1)
|
|
} else {
|
|
updatedIndex = min(self.paneArrangement.panes.count - 1, self.paneArrangement.currentIndex + 1)
|
|
}
|
|
}
|
|
self.paneArrangement = self.paneArrangement.withIndexTransition(0.0)
|
|
self.setCurrentPane(self.paneArrangement.panes[updatedIndex], transition: .animated(duration: 0.25, curve: .spring))
|
|
}
|
|
case .cancelled:
|
|
if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, isVisible) = self.validLayout {
|
|
self.paneArrangement = self.paneArrangement.withIndexTransition(0.0)
|
|
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, isVisible: isVisible)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private var isExpanded: Bool {
|
|
var isExpanded: Bool = false
|
|
if let validLayout = self.validLayout, case let .media(_, maybeExpanded) = validLayout.8.inputMode, maybeExpanded != nil {
|
|
isExpanded = true
|
|
}
|
|
return isExpanded
|
|
}
|
|
|
|
private func updatePaneDidScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState, transition: ContainedViewLayoutTransition) {
|
|
if self.isExpanded {
|
|
pane.collectionListPanelOffset = 0.0
|
|
} else {
|
|
var computedAbsoluteOffset: CGFloat
|
|
if let absoluteOffset = state.absoluteOffset, absoluteOffset >= 0.0 {
|
|
computedAbsoluteOffset = 0.0
|
|
} else {
|
|
computedAbsoluteOffset = pane.collectionListPanelOffset + state.relativeChange
|
|
}
|
|
computedAbsoluteOffset = max(-41.0, min(computedAbsoluteOffset, 0.0))
|
|
pane.collectionListPanelOffset = computedAbsoluteOffset
|
|
if transition.isAnimated {
|
|
if pane.collectionListPanelOffset < -41.0 / 2.0 {
|
|
pane.collectionListPanelOffset = -41.0
|
|
} else {
|
|
pane.collectionListPanelOffset = 0.0
|
|
}
|
|
}
|
|
}
|
|
|
|
let collectionListPanelOffset = self.currentCollectionListPanelOffset()
|
|
|
|
self.updateAppearanceTransition(transition: transition)
|
|
transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size))
|
|
transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: self.collectionListSeparator.bounds.size))
|
|
transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0))
|
|
}
|
|
|
|
private func fixPaneScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState) {
|
|
if let absoluteOffset = state.absoluteOffset, absoluteOffset >= 0.0 {
|
|
pane.collectionListPanelOffset = 0.0
|
|
} else {
|
|
if pane.collectionListPanelOffset < -41.0 / 2.0 {
|
|
pane.collectionListPanelOffset = -41.0
|
|
} else {
|
|
pane.collectionListPanelOffset = 0.0
|
|
}
|
|
}
|
|
|
|
let collectionListPanelOffset = self.currentCollectionListPanelOffset()
|
|
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .spring)
|
|
self.updateAppearanceTransition(transition: transition)
|
|
transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size))
|
|
transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: self.collectionListSeparator.bounds.size))
|
|
transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0))
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if let searchContainerNode = self.searchContainerNode {
|
|
if let result = searchContainerNode.hitTest(point.offsetBy(dx: -searchContainerNode.frame.minX, dy: -searchContainerNode.frame.minY), with: event) {
|
|
return result
|
|
}
|
|
}
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
static func setupPanelIconInsets(item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> UIEdgeInsets {
|
|
var insets = UIEdgeInsets()
|
|
if previousItem != nil {
|
|
insets.top += 3.0
|
|
}
|
|
if nextItem != nil {
|
|
insets.bottom += 3.0
|
|
}
|
|
return insets
|
|
}
|
|
|
|
private func dismissPeerSpecificPackSetup() {
|
|
guard let peerId = self.peerId else {
|
|
return
|
|
}
|
|
self.dismissedPeerSpecificStickerPack.set(.single(true))
|
|
let _ = (self.context.account.postbox.transaction { transaction -> Void in
|
|
transaction.updatePeerChatInterfaceState(peerId, update: { current in
|
|
if let current = current as? ChatInterfaceState {
|
|
return current.withUpdatedMessageActionsState({ value in
|
|
var value = value
|
|
value.closedPeerSpecificPackSetup = true
|
|
return value
|
|
})
|
|
} else {
|
|
return current
|
|
}
|
|
})
|
|
}).start()
|
|
}
|
|
}
|