mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 17:30:12 +00:00
GIF-related improvements
This commit is contained in:
parent
29b23c767f
commit
2ab830e3a1
@ -1 +1 @@
|
||||
11.4.1
|
||||
11.5
|
||||
|
||||
@ -114,11 +114,13 @@ public final class ActivityIndicator: ASDisplayNode {
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let indicatorView = UIActivityIndicatorView(style: .whiteLarge)
|
||||
let indicatorView: UIActivityIndicatorView
|
||||
switch self.type {
|
||||
case let .navigationAccent(color):
|
||||
indicatorView = UIActivityIndicatorView(style: .whiteLarge)
|
||||
indicatorView.color = color
|
||||
case let .custom(color, _, _, forceCustom):
|
||||
case let .custom(color, diameter, _, forceCustom):
|
||||
indicatorView = UIActivityIndicatorView(style: diameter < 15.0 ? .white : .whiteLarge)
|
||||
indicatorView.color = convertIndicatorColor(color)
|
||||
if !forceCustom {
|
||||
self.view.addSubview(indicatorView)
|
||||
|
||||
@ -433,8 +433,8 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
}
|
||||
|
||||
func updateAlpha(node: ASDisplayNode, alpha: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if node.alpha.isEqual(to: alpha) {
|
||||
func updateAlpha(node: ASDisplayNode, alpha: CGFloat, beginWithCurrentState: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if node.alpha.isEqual(to: alpha) && !force {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
|
||||
@ -489,7 +489,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
strongSelf.playOnContentOwnership = false
|
||||
strongSelf.initiallyActivated = true
|
||||
strongSelf.skipInitialPause = true
|
||||
strongSelf.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop)
|
||||
strongSelf.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: isAnimated ? .loop : .stop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,17 +12,21 @@ public enum RequestChatContextResultsError {
|
||||
|
||||
public final class CachedChatContextResult: PostboxCoding {
|
||||
public let data: Data
|
||||
public let timestamp: Int32
|
||||
|
||||
public init(data: Data) {
|
||||
public init(data: Data, timestamp: Int32) {
|
||||
self.data = data
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.data = decoder.decodeDataForKey("data") ?? Data()
|
||||
self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0)
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeData(self.data, forKey: "data")
|
||||
encoder.encodeInt32(self.timestamp, forKey: "timestamp")
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +39,7 @@ private struct RequestData: Codable {
|
||||
let query: String
|
||||
}
|
||||
|
||||
private let requestVersion = "1"
|
||||
private let requestVersion = "3"
|
||||
|
||||
public func requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String) -> Signal<ChatContextResultCollection?, RequestChatContextResultsError> {
|
||||
return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in
|
||||
@ -69,7 +73,10 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P
|
||||
let key = ValueBoxKey(MemoryBuffer(data: keyData))
|
||||
if let cachedEntry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key)) as? CachedChatContextResult {
|
||||
if let cachedResult = try? JSONDecoder().decode(ChatContextResultCollection.self, from: cachedEntry.data) {
|
||||
return .single(cachedResult)
|
||||
let timestamp = Int32(Date().timeIntervalSince1970)
|
||||
if cachedEntry.timestamp + cachedResult.cacheTimeout > timestamp {
|
||||
return .single(cachedResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,12 +109,12 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P
|
||||
}
|
||||
|
||||
return account.postbox.transaction { transaction -> ChatContextResultCollection? in
|
||||
if result.cacheTimeout > 10 {
|
||||
if result.cacheTimeout > 10 && offset.isEmpty {
|
||||
if let resultData = try? JSONEncoder().encode(result) {
|
||||
let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query)
|
||||
if let keyData = try? JSONEncoder().encode(requestData) {
|
||||
let key = ValueBoxKey(MemoryBuffer(data: keyData))
|
||||
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key), entry: CachedChatContextResult(data: resultData), collectionSpec: collectionSpec)
|
||||
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key), entry: CachedChatContextResult(data: resultData, timestamp: Int32(Date().timeIntervalSince1970)), collectionSpec: collectionSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,7 +253,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
|
||||
}
|
||||
|
||||
if let videoFileReference = videoFileReference {
|
||||
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: videoFileReference)
|
||||
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: videoFileReference, synchronousLoad: false)
|
||||
self.layer.addSublayer(thumbnailLayer)
|
||||
let layerHolder = takeSampleBufferLayer()
|
||||
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
||||
|
||||
@ -203,6 +203,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
private let searching = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
private let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>()
|
||||
private let loadingMessage = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
private let performingInlineSearch = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
|
||||
private var preloadHistoryPeerId: PeerId?
|
||||
private let preloadHistoryPeerIdDisposable = MetaDisposable()
|
||||
@ -4543,7 +4544,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get()))
|
||||
}, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get()))
|
||||
|
||||
switch self.chatLocation {
|
||||
case let .peer(peerId):
|
||||
@ -5127,13 +5128,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if case .contextRequest = kind {
|
||||
self.performingInlineSearch.set(false)
|
||||
}
|
||||
case let .update(query, signal):
|
||||
let currentQueryAndDisposable = self.contextQueryStates[kind]
|
||||
currentQueryAndDisposable?.1.dispose()
|
||||
|
||||
var inScope = true
|
||||
var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)?
|
||||
self.contextQueryStates[kind] = (query, (signal |> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
self.contextQueryStates[kind] = (query, (signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let strongSelf = self {
|
||||
if Thread.isMainThread && inScope {
|
||||
inScope = false
|
||||
@ -5148,13 +5153,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
if let strongSelf = self {
|
||||
if case .contextRequest = kind {
|
||||
strongSelf.performingInlineSearch.set(false)
|
||||
}
|
||||
|
||||
switch error {
|
||||
case let .inlineBotLocationRequest(peerId):
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: Int32(Date().timeIntervalSince1970 + 10 * 60)).start()
|
||||
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||
let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).start()
|
||||
})]), in: .window(.root))
|
||||
case let .inlineBotLocationRequest(peerId):
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: Int32(Date().timeIntervalSince1970 + 10 * 60)).start()
|
||||
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||
let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).start()
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
}, completed: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
if case .contextRequest = kind {
|
||||
strongSelf.performingInlineSearch.set(false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
@ -5163,6 +5178,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { previousResult in
|
||||
return inScopeResult(previousResult)
|
||||
})
|
||||
} else {
|
||||
if case .contextRequest = kind {
|
||||
self.performingInlineSearch.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
|
||||
@ -1201,7 +1201,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
if transition.isAnimated, let derivedLayoutState = self.derivedLayoutState {
|
||||
let offset = derivedLayoutState.inputContextPanelsOverMainPanelFrame.maxY - inputContextPanelsOverMainPanelFrame.maxY
|
||||
transition.animateOffsetAdditive(node: self.inputContextPanelContainer, offset: -offset)
|
||||
//transition.animateOffsetAdditive(node: self.inputContextPanelContainer, offset: -offset)
|
||||
}
|
||||
|
||||
if let inputContextPanelNode = self.inputContextPanelNode {
|
||||
|
||||
@ -71,14 +71,14 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa
|
||||
switch inputQueryResult {
|
||||
case let .stickers(results):
|
||||
if !results.isEmpty {
|
||||
if let currentPanel = currentPanel as? HorizontalStickersChatContextPanelNode {
|
||||
currentPanel.updateResults(results.map({ $0.file }))
|
||||
if let currentPanel = currentPanel as? InlineReactionSearchPanel {
|
||||
currentPanel.updateResults(results: results.map({ $0.file }))
|
||||
return currentPanel
|
||||
} else {
|
||||
let panel = HorizontalStickersChatContextPanelNode(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize)
|
||||
let panel = InlineReactionSearchPanel(context: context, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, fontSize: chatPresentationInterfaceState.fontSize)
|
||||
panel.controllerInteraction = controllerInteraction
|
||||
panel.interfaceInteraction = interfaceInteraction
|
||||
panel.updateResults(results.map({ $0.file }))
|
||||
panel.updateResults(results: results.map({ $0.file }))
|
||||
return panel
|
||||
}
|
||||
}
|
||||
@ -94,7 +94,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa
|
||||
return panel
|
||||
}
|
||||
}
|
||||
case let .emojis(results, range):
|
||||
case let .emojis(results, _):
|
||||
if !results.isEmpty {
|
||||
if let currentPanel = currentPanel as? EmojisChatInputContextPanelNode {
|
||||
currentPanel.updateResults(results)
|
||||
|
||||
@ -26,13 +26,25 @@ private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) {
|
||||
|
||||
final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
private let account: Account
|
||||
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: (FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void
|
||||
|
||||
let searchPlaceholderNode: PaneSearchBarPlaceholderNode
|
||||
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
|
||||
|
||||
@ -46,6 +58,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void, openGifContextMenu: @escaping (FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.paneDidScroll = paneDidScroll
|
||||
self.fixPaneScroll = fixPaneScroll
|
||||
@ -64,7 +78,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
self.addSubnode(self.emptyNode)
|
||||
|
||||
self.searchPlaceholderNode.activate = { [weak self] in
|
||||
self?.inputNodeInteraction?.toggleSearch(true, .gif)
|
||||
self?.inputNodeInteraction?.toggleSearch(true, .gif, "")
|
||||
}
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
@ -75,6 +89,9 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
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)
|
||||
@ -108,7 +125,11 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
override var isEmpty: Bool {
|
||||
return self.multiplexedNode?.files.isEmpty ?? true
|
||||
if let files = self.multiplexedNode?.files {
|
||||
return files.trending.isEmpty && files.saved.isEmpty
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override func willEnterHierarchy() {
|
||||
@ -118,7 +139,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
private func updateMultiplexedNodeLayout(changedIsExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
||||
guard let (size, topInset, bottomInset, isExpanded, isVisible, deviceMetrics) = self.validLayout else {
|
||||
guard let (size, topInset, bottomInset, isExpanded, _, deviceMetrics) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -137,52 +158,79 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
|
||||
|
||||
var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size)
|
||||
if changedIsExpanded {
|
||||
targetBounds.origin.y = isExpanded || multiplexedNode.files.isEmpty ? 0.0 : 60.0
|
||||
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.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: targetBounds)
|
||||
transition.updateFrame(node: multiplexedNode, frame: nodeFrame)
|
||||
|
||||
multiplexedNode.updateLayout(size: nodeFrame.size, transition: transition)
|
||||
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(account: account, query: "", updateActivity: nil))
|
||||
self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, updateActivity: nil)
|
||||
|> map { items -> [FileMediaReference]? in
|
||||
if let (items, _) = items {
|
||||
return items
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
let multiplexedNode = MultiplexedVideoNode(account: account)
|
||||
let multiplexedNode = MultiplexedVideoNode(account: self.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)
|
||||
|
||||
let gifs = self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)])
|
||||
|> map { view -> [FileMediaReference] in
|
||||
let gifs = combineLatest(self.trendingPromise.get(), self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]))
|
||||
|> map { trending, view -> MultiplexedVideoNodeFiles in
|
||||
var recentGifs: OrderedItemListView?
|
||||
if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] {
|
||||
recentGifs = orderedView as? OrderedItemListView
|
||||
}
|
||||
|
||||
var saved: [FileMediaReference] = []
|
||||
|
||||
if let recentGifs = recentGifs {
|
||||
return recentGifs.items.map { item in
|
||||
saved = recentGifs.items.map { item in
|
||||
let file = (item.contents as! RecentMediaItem).media as! TelegramMediaFile
|
||||
return .savedGif(media: file)
|
||||
}
|
||||
} else {
|
||||
return []
|
||||
saved = []
|
||||
}
|
||||
|
||||
return MultiplexedVideoNodeFiles(saved: saved, trending: trending ?? [])
|
||||
}
|
||||
self.disposable.set((gifs
|
||||
|> deliverOnMainQueue).start(next: { [weak self] gifs in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] files in
|
||||
if let strongSelf = self {
|
||||
let previousFiles = strongSelf.multiplexedNode?.files
|
||||
strongSelf.multiplexedNode?.files = gifs
|
||||
strongSelf.emptyNode.isHidden = !gifs.isEmpty
|
||||
if (previousFiles ?? []).isEmpty && !gifs.isEmpty {
|
||||
strongSelf.multiplexedNode?.files = files
|
||||
let wasEmpty: Bool
|
||||
if let previousFiles = previousFiles {
|
||||
wasEmpty = previousFiles.trending.isEmpty && previousFiles.saved.isEmpty
|
||||
} else {
|
||||
wasEmpty = true
|
||||
}
|
||||
let isEmpty = files.trending.isEmpty && files.saved.isEmpty
|
||||
strongSelf.emptyNode.isHidden = !isEmpty
|
||||
if wasEmpty && isEmpty {
|
||||
strongSelf.multiplexedNode?.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 60.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable {
|
||||
switch self {
|
||||
case let .search(theme, strings):
|
||||
return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: {
|
||||
inputNodeInteraction.toggleSearch(true, .sticker)
|
||||
inputNodeInteraction.toggleSearch(true, .sticker, "")
|
||||
})
|
||||
case let .peerSpecificSetup(theme, strings, dismissed):
|
||||
return StickerPanePeerSpecificSetupGridItem(theme: theme, strings: strings, setup: {
|
||||
|
||||
@ -332,7 +332,7 @@ private enum StickerPacksCollectionUpdate {
|
||||
final class ChatMediaInputNodeInteraction {
|
||||
let navigateToCollectionId: (ItemCollectionId) -> Void
|
||||
let openSettings: () -> Void
|
||||
let toggleSearch: (Bool, ChatMediaInputSearchMode?) -> Void
|
||||
let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void
|
||||
let openPeerSpecificSettings: () -> Void
|
||||
let dismissPeerSpecificSettings: () -> Void
|
||||
let clearRecentlyUsedStickers: () -> Void
|
||||
@ -343,7 +343,7 @@ final class ChatMediaInputNodeInteraction {
|
||||
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) {
|
||||
init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) {
|
||||
self.navigateToCollectionId = navigateToCollectionId
|
||||
self.openSettings = openSettings
|
||||
self.toggleSearch = toggleSearch
|
||||
@ -551,7 +551,7 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
controller.navigationPresentation = .modal
|
||||
strongSelf.controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
}
|
||||
}, toggleSearch: { [weak self] value, searchMode in
|
||||
}, toggleSearch: { [weak self] value, searchMode, query in
|
||||
if let strongSelf = self {
|
||||
if let searchMode = searchMode, value {
|
||||
var searchContainerNode: PaneSearchContainerNode?
|
||||
@ -560,9 +560,14 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
} 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)
|
||||
self?.inputNodeInteraction.toggleSearch(false, nil, "")
|
||||
})
|
||||
strongSelf.searchContainerNode = searchContainerNode
|
||||
if !query.isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
searchContainerNode?.updateQuery(query)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let searchContainerNode = searchContainerNode {
|
||||
strongSelf.searchContainerNodeLoadedDisposable.set((searchContainerNode.ready
|
||||
@ -1407,10 +1412,12 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
searchContainerNode.frame = containerFrame
|
||||
searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: .immediate)
|
||||
var placeholderNode: PaneSearchBarPlaceholderNode?
|
||||
var anchorTop = CGPoint(x: 0.0, y: 0.0)
|
||||
var anchorTopView: UIView = self.view
|
||||
if let searchMode = searchMode {
|
||||
switch searchMode {
|
||||
case .gif:
|
||||
placeholderNode = self.gifPane.searchPlaceholderNode
|
||||
placeholderNode = self.gifPane.visibleSearchPlaceholderNode
|
||||
case .sticker:
|
||||
self.stickerPane.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? PaneSearchBarPlaceholderNode {
|
||||
@ -1427,11 +1434,9 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
}
|
||||
|
||||
if let placeholderNode = placeholderNode {
|
||||
searchContainerNode.animateIn(from: placeholderNode, transition: transition, completion: { [weak self] in
|
||||
self?.gifPane.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
searchContainerNode.animateIn(from: placeholderNode, anchorTop: anchorTop, anhorTopView: anchorTopView, transition: transition, completion: { [weak self] in
|
||||
self?.gifPane.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1589,8 +1594,8 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
if let searchMode = searchMode {
|
||||
switch searchMode {
|
||||
case .gif:
|
||||
placeholderNode = self.gifPane.searchPlaceholderNode
|
||||
paneIsEmpty = self.gifPane.isEmpty
|
||||
placeholderNode = self.gifPane.visibleSearchPlaceholderNode
|
||||
paneIsEmpty = placeholderNode != nil
|
||||
case .sticker:
|
||||
paneIsEmpty = true
|
||||
self.stickerPane.gridNode.forEachItemNode { itemNode in
|
||||
@ -1612,11 +1617,14 @@ final class ChatMediaInputNode: ChatInputNode {
|
||||
}
|
||||
}
|
||||
if let placeholderNode = placeholderNode {
|
||||
placeholderNode.isHidden = false
|
||||
searchContainerNode.animateOut(to: placeholderNode, animateOutSearchBar: !paneIsEmpty, transition: transition, completion: { [weak searchContainerNode] in
|
||||
searchContainerNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
searchContainerNode.removeFromSupernode()
|
||||
transition.updateAlpha(node: searchContainerNode, alpha: 0.0, completion: { [weak searchContainerNode] _ in
|
||||
searchContainerNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -338,7 +338,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
||||
}
|
||||
}, getItemIsPreviewed: self.getItemIsPreviewed,
|
||||
openSearch: { [weak self] in
|
||||
self?.inputNodeInteraction?.toggleSearch(true, .trending)
|
||||
self?.inputNodeInteraction?.toggleSearch(true, .trending, "")
|
||||
})
|
||||
|
||||
let isPane = self.isPane
|
||||
|
||||
@ -726,7 +726,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
let mediaManager = context.sharedContext.mediaManager
|
||||
|
||||
let streamVideo = isMediaStreamable(message: message, media: updatedVideoFile)
|
||||
let videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor)
|
||||
let loopVideo = updatedVideoFile.isAnimated
|
||||
let videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor)
|
||||
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
videoNode.ownsContentNodeUpdated = { [weak self] owns in
|
||||
|
||||
@ -21,13 +21,15 @@ final class ChatPanelInterfaceInteractionStatuses {
|
||||
let unblockingPeer: Signal<Bool, NoError>
|
||||
let searching: Signal<Bool, NoError>
|
||||
let loadingMessage: Signal<Bool, NoError>
|
||||
let inlineSearch: Signal<Bool, NoError>
|
||||
|
||||
init(editingMessage: Signal<Float?, NoError>, startingBot: Signal<Bool, NoError>, unblockingPeer: Signal<Bool, NoError>, searching: Signal<Bool, NoError>, loadingMessage: Signal<Bool, NoError>) {
|
||||
init(editingMessage: Signal<Float?, NoError>, startingBot: Signal<Bool, NoError>, unblockingPeer: Signal<Bool, NoError>, searching: Signal<Bool, NoError>, loadingMessage: Signal<Bool, NoError>, inlineSearch: Signal<Bool, NoError>) {
|
||||
self.editingMessage = editingMessage
|
||||
self.startingBot = startingBot
|
||||
self.unblockingPeer = unblockingPeer
|
||||
self.searching = searching
|
||||
self.loadingMessage = loadingMessage
|
||||
self.inlineSearch = inlineSearch
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
@ -11,18 +12,7 @@ import TextFormat
|
||||
import AccountContext
|
||||
import TouchDownGesture
|
||||
import ImageTransparency
|
||||
|
||||
private let searchLayoutProgressImage = generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(UIColor(rgb: 0x9099A2, alpha: 0.6).cgColor)
|
||||
|
||||
let lineWidth: CGFloat = 2.0
|
||||
let cutoutWidth: CGFloat = 4.0
|
||||
context.setLineWidth(lineWidth)
|
||||
|
||||
context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth)))
|
||||
context.clear(CGRect(origin: CGPoint(x: (size.width - cutoutWidth) / 2.0, y: 0.0), size: CGSize(width: cutoutWidth, height: size.height / 2.0)))
|
||||
})
|
||||
import ActivityIndicator
|
||||
|
||||
private let accessoryButtonFont = Font.medium(14.0)
|
||||
|
||||
@ -217,7 +207,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let attachmentButton: HighlightableButtonNode
|
||||
let attachmentButtonDisabledNode: HighlightableButtonNode
|
||||
let searchLayoutClearButton: HighlightableButton
|
||||
let searchLayoutProgressView: UIImageView
|
||||
private let searchLayoutClearImageNode: ASImageNode
|
||||
private var searchActivityIndicator: ActivityIndicator?
|
||||
var audioRecordingInfoContainerNode: ASDisplayNode?
|
||||
var audioRecordingDotNode: ASImageNode?
|
||||
var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode?
|
||||
@ -281,6 +272,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private let statusDisposable = MetaDisposable()
|
||||
override var interfaceInteraction: ChatPanelInterfaceInteraction? {
|
||||
didSet {
|
||||
if let statuses = self.interfaceInteraction?.statuses {
|
||||
self.statusDisposable.set((statuses.inlineSearch
|
||||
|> distinctUntilChanged
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
self?.updateIsProcessingInlineRequest(value)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) {
|
||||
if state.inputText.length != 0 && self.textInputNode == nil {
|
||||
self.loadTextInputNode()
|
||||
@ -390,8 +394,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
self.attachmentButton.isAccessibilityElement = true
|
||||
self.attachmentButtonDisabledNode = HighlightableButtonNode()
|
||||
self.searchLayoutClearButton = HighlightableButton()
|
||||
self.searchLayoutProgressView = UIImageView(image: searchLayoutProgressImage)
|
||||
self.searchLayoutProgressView.isHidden = true
|
||||
self.searchLayoutClearImageNode = ASImageNode()
|
||||
self.searchLayoutClearImageNode.isUserInteractionEnabled = false
|
||||
self.searchLayoutClearButton.addSubnode(self.searchLayoutClearImageNode)
|
||||
|
||||
self.actionButtons = ChatTextInputActionButtonsNode(theme: presentationInterfaceState.theme, strings: presentationInterfaceState.strings, presentController: presentController)
|
||||
|
||||
@ -466,8 +471,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
self.searchLayoutClearButton.addTarget(self, action: #selector(self.searchLayoutClearButtonPressed), for: .touchUpInside)
|
||||
self.searchLayoutClearButton.alpha = 0.0
|
||||
|
||||
self.searchLayoutClearButton.addSubview(self.searchLayoutProgressView)
|
||||
|
||||
self.addSubnode(self.textInputContainer)
|
||||
self.addSubnode(self.textInputBackgroundNode)
|
||||
|
||||
@ -495,6 +498,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.statusDisposable.dispose()
|
||||
}
|
||||
|
||||
func loadTextInputNodeIfNeeded() {
|
||||
if self.textInputNode == nil {
|
||||
self.loadTextInputNode()
|
||||
@ -735,7 +742,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight)
|
||||
|
||||
self.searchLayoutClearButton.setImage(PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme), for: [])
|
||||
self.searchLayoutClearImageNode.image = PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme)
|
||||
|
||||
if let audioRecordingDotNode = self.audioRecordingDotNode {
|
||||
audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme)
|
||||
@ -1102,9 +1109,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight)
|
||||
let textFieldInsets = self.textFieldInsets(metrics: metrics)
|
||||
transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize))
|
||||
|
||||
let searchProgressSize = self.searchLayoutProgressView.bounds.size
|
||||
transition.updateFrame(layer: self.searchLayoutProgressView.layer, frame: CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - searchProgressSize.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - searchProgressSize.height) / 2.0)), size: searchProgressSize))
|
||||
if let image = self.searchLayoutClearImageNode.image {
|
||||
self.searchLayoutClearImageNode.frame = CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - image.size.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
|
||||
let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top + audioRecordingItemsVerticalOffset, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
|
||||
transition.updateFrame(node: self.textInputContainer, frame: textInputFrame)
|
||||
@ -1423,6 +1430,26 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsProcessingInlineRequest(_ value: Bool) {
|
||||
if value {
|
||||
if self.searchActivityIndicator == nil, let currentState = self.presentationInterfaceState {
|
||||
let searchActivityIndicator = ActivityIndicator(type: .custom(currentState.theme.list.itemAccentColor, 22.0, 1.0, false))
|
||||
searchActivityIndicator.isUserInteractionEnabled = false
|
||||
self.searchActivityIndicator = searchActivityIndicator
|
||||
let indicatorSize = searchActivityIndicator.measure(CGSize(width: 100.0, height: 100.0))
|
||||
let size = self.searchLayoutClearButton.bounds.size
|
||||
searchActivityIndicator.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height) / 2.0) + 1.0), size: indicatorSize)
|
||||
self.searchLayoutClearImageNode.isHidden = true
|
||||
self.searchLayoutClearButton.addSubnode(searchActivityIndicator)
|
||||
searchActivityIndicator.layer.sublayerTransform = CATransform3DMakeScale(0.5, 0.5, 1.0)
|
||||
}
|
||||
} else if let searchActivityIndicator = self.searchActivityIndicator {
|
||||
self.searchActivityIndicator = nil
|
||||
self.searchLayoutClearImageNode.isHidden = false
|
||||
searchActivityIndicator.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
|
||||
if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero {
|
||||
self.sendButtonPressed()
|
||||
|
||||
@ -210,7 +210,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
|
||||
}
|
||||
|
||||
private func dequeueTransition() {
|
||||
if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first {
|
||||
if let validLayout = self.validLayout, let (transition, _) = self.enqueuedTransitions.first {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
|
||||
@ -274,7 +274,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
|
||||
},
|
||||
openSettings: {
|
||||
},
|
||||
toggleSearch: { _, _ in
|
||||
toggleSearch: { _, _, _ in
|
||||
},
|
||||
openPeerSpecificSettings: {
|
||||
},
|
||||
|
||||
@ -11,7 +11,7 @@ import AccountContext
|
||||
import WebSearchUI
|
||||
import AppBundle
|
||||
|
||||
func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bool) -> Void)?) -> Signal<[FileMediaReference]?, NoError> {
|
||||
func paneGifSearchForQuery(account: Account, query: String, offset: String?, updateActivity: ((Bool) -> Void)?) -> Signal<([FileMediaReference], String?)?, NoError> {
|
||||
let delayRequest = true
|
||||
|
||||
let contextBot = account.postbox.transaction { transaction -> String in
|
||||
@ -34,7 +34,7 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo
|
||||
}
|
||||
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in
|
||||
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
|
||||
let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, limit: 15)
|
||||
let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, offset: offset ?? "", limit: 50)
|
||||
|> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
return { _ in
|
||||
return .contextRequestResult(user, results)
|
||||
@ -66,25 +66,46 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo
|
||||
}
|
||||
}
|
||||
return contextBot
|
||||
|> mapToSignal { result -> Signal<[FileMediaReference]?, NoError> in
|
||||
|> mapToSignal { result -> Signal<([FileMediaReference], String?)?, NoError> in
|
||||
if let r = result(nil), case let .contextRequestResult(_, collection) = r, let results = collection?.results {
|
||||
var references: [FileMediaReference] = []
|
||||
for result in results {
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
var imageResource: TelegramMediaResource?
|
||||
var thumbnailResource: TelegramMediaResource?
|
||||
var thumbnailIsVideo: Bool = false
|
||||
var uniqueId: Int64?
|
||||
if let content = externalReference.content {
|
||||
imageResource = content.resource
|
||||
if let resource = content.resource as? WebFileReferenceMediaResource {
|
||||
uniqueId = Int64(HashFunctions.murMurHash32(resource.url))
|
||||
}
|
||||
} else if let thumbnail = externalReference.thumbnail {
|
||||
imageResource = thumbnail.resource
|
||||
}
|
||||
if let thumbnail = externalReference.thumbnail {
|
||||
thumbnailResource = thumbnail.resource
|
||||
if thumbnail.mimeType.hasPrefix("video/") {
|
||||
thumbnailIsVideo = true
|
||||
}
|
||||
}
|
||||
|
||||
if externalReference.type == "gif", let thumbnailResource = imageResource, let content = externalReference.content, let dimensions = content.dimensions {
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
|
||||
if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions {
|
||||
var previews: [TelegramMediaImageRepresentation] = []
|
||||
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
|
||||
if let thumbnailResource = thumbnailResource {
|
||||
if thumbnailIsVideo {
|
||||
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
|
||||
dimensions: dimensions,
|
||||
resource: thumbnailResource
|
||||
))
|
||||
} else {
|
||||
previews.append(TelegramMediaImageRepresentation(
|
||||
dimensions: dimensions,
|
||||
resource: thumbnailResource
|
||||
))
|
||||
}
|
||||
}
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
|
||||
references.append(FileMediaReference.standalone(media: file))
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
@ -93,7 +114,7 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo
|
||||
}
|
||||
}
|
||||
}
|
||||
return .single(references)
|
||||
return .single((references, collection?.nextOffset))
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
@ -119,6 +140,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
private let notFoundNode: ASImageNode
|
||||
private let notFoundLabel: ImmediateTextNode
|
||||
|
||||
private var nextOffset: (String, String)?
|
||||
private var isLoadingNextResults: Bool = false
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private let trendingPromise: Promise<[FileMediaReference]?>
|
||||
@ -131,6 +155,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
|
||||
var deactivateSearchBar: (() -> Void)?
|
||||
var updateActivity: ((Bool) -> Void)?
|
||||
var requestUpdateQuery: ((String) -> Void)?
|
||||
|
||||
private var hasInitialText = false
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<[FileMediaReference]?>) {
|
||||
self.context = context
|
||||
@ -167,27 +194,82 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
}
|
||||
|
||||
func updateText(_ text: String, languageCode: String?) {
|
||||
let signal: Signal<[FileMediaReference]?, NoError>
|
||||
self.hasInitialText = true
|
||||
self.isLoadingNextResults = true
|
||||
|
||||
let signal: Signal<([FileMediaReference], String?)?, NoError>
|
||||
if !text.isEmpty {
|
||||
signal = paneGifSearchForQuery(account: self.context.account, query: text, updateActivity: self.updateActivity)
|
||||
signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: "", updateActivity: self.updateActivity)
|
||||
self.updateActivity?(true)
|
||||
} else {
|
||||
signal = self.trendingPromise.get()
|
||||
|> map { items -> ([FileMediaReference], String?)? in
|
||||
if let items = items {
|
||||
return (items, nil)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
self.updateActivity?(false)
|
||||
}
|
||||
|
||||
self.searchDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self, let result = result else {
|
||||
guard let strongSelf = self, let (result, nextOffset) = result else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.multiplexedNode?.files = result
|
||||
strongSelf.isLoadingNextResults = false
|
||||
if let nextOffset = nextOffset {
|
||||
strongSelf.nextOffset = (text, nextOffset)
|
||||
} else {
|
||||
strongSelf.nextOffset = nil
|
||||
}
|
||||
strongSelf.multiplexedNode?.files = MultiplexedVideoNodeFiles(saved: [], trending: result)
|
||||
strongSelf.updateActivity?(false)
|
||||
strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty
|
||||
}))
|
||||
}
|
||||
|
||||
private func loadMore() {
|
||||
if self.isLoadingNextResults {
|
||||
return
|
||||
}
|
||||
guard let (text, nextOffsetValue) = self.nextOffset else {
|
||||
return
|
||||
}
|
||||
self.isLoadingNextResults = true
|
||||
|
||||
let signal: Signal<([FileMediaReference], String?)?, NoError>
|
||||
signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity)
|
||||
|
||||
self.searchDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self, let (result, nextOffset) = result else {
|
||||
return
|
||||
}
|
||||
|
||||
var files = strongSelf.multiplexedNode?.files.trending ?? []
|
||||
var currentIds = Set(files.map { $0.media.fileId })
|
||||
for item in result {
|
||||
if currentIds.contains(item.media.fileId) {
|
||||
continue
|
||||
}
|
||||
currentIds.insert(item.media.fileId)
|
||||
files.append(item)
|
||||
}
|
||||
|
||||
strongSelf.isLoadingNextResults = false
|
||||
if let nextOffset = nextOffset {
|
||||
strongSelf.nextOffset = (text, nextOffset)
|
||||
} else {
|
||||
strongSelf.nextOffset = nil
|
||||
}
|
||||
strongSelf.multiplexedNode?.files = MultiplexedVideoNodeFiles(saved: [], trending: files)
|
||||
strongSelf.notFoundNode.isHidden = text.isEmpty || !files.isEmpty
|
||||
}))
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.notFoundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GifsNotFoundIcon"), color: theme.list.freeMonoIconColor)
|
||||
self.notFoundLabel.attributedText = NSAttributedString(string: strings.Gif_NoGifsFound, font: Font.medium(14.0), textColor: theme.list.freeTextColor)
|
||||
@ -223,10 +305,10 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||
|
||||
transition.updateFrame(layer: multiplexedNode.layer, frame: nodeFrame)
|
||||
multiplexedNode.updateLayout(size: nodeFrame.size, transition: transition)
|
||||
multiplexedNode.updateLayout(theme: self.theme, strings: self.strings, size: nodeFrame.size, transition: transition)
|
||||
}
|
||||
|
||||
if firstLayout {
|
||||
if firstLayout && !self.hasInitialText {
|
||||
self.updateText("", languageCode: nil)
|
||||
}
|
||||
}
|
||||
@ -235,7 +317,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
super.willEnterHierarchy()
|
||||
|
||||
if self.multiplexedNode == nil {
|
||||
let multiplexedNode = MultiplexedVideoNode(account: self.context.account)
|
||||
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)
|
||||
@ -248,7 +330,19 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
}
|
||||
|
||||
multiplexedNode.didScroll = { [weak self] offset, height in
|
||||
self?.deactivateSearchBar?()
|
||||
guard let strongSelf = self, let multiplexedNode = strongSelf.multiplexedNode else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.deactivateSearchBar?()
|
||||
|
||||
if offset >= height - multiplexedNode.bounds.height - 200.0 {
|
||||
strongSelf.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
multiplexedNode.reactionSelected = { [weak self] reaction in
|
||||
self?.requestUpdateQuery?(reaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,7 +281,10 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
|
||||
if let (transition, firstTime) = self.enqueuedTransitions.first {
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
let options = ListViewDeleteAndInsertOptions()
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
options.insert(.Synchronous)
|
||||
options.insert(.LowLatency)
|
||||
options.insert(.PreferSynchronousResourceLoading)
|
||||
if firstTime {
|
||||
//options.insert(.Synchronous)
|
||||
//options.insert(.LowLatency)
|
||||
|
||||
@ -40,7 +40,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply(.None) })
|
||||
return (nil, { _ in apply(synchronousLoads, .None) })
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -64,7 +64,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
|
||||
let (layout, apply) = nodeLayout(self, params, top, bottom)
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply(animation)
|
||||
apply(false, animation)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -188,11 +188,11 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
let (layout, apply) = doLayout(item, params, merged.top, merged.bottom)
|
||||
self.contentSize = layout.contentSize
|
||||
self.insets = layout.insets
|
||||
apply(.None)
|
||||
apply(false, .None)
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: HorizontalListContextResultsChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
||||
func asyncLayout() -> (_ item: HorizontalListContextResultsChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (Bool, ListViewItemUpdateAnimation) -> Void) {
|
||||
let imageLayout = self.imageNode.asyncLayout()
|
||||
let currentImageResource = self.currentImageResource
|
||||
let currentVideoFile = self.currentVideoFile
|
||||
@ -315,7 +315,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
} else {
|
||||
let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0)), resource: imageResource)
|
||||
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
|
||||
updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage))
|
||||
updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage), synchronousLoad: true)
|
||||
}
|
||||
} else {
|
||||
updateImageSignal = .complete()
|
||||
@ -324,7 +324,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: height, height: croppedImageDimensions.width + sideInset), insets: UIEdgeInsets())
|
||||
|
||||
return (nodeLayout, { _ in
|
||||
return (nodeLayout, { synchronousLoads, _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.currentImageResource = imageResource
|
||||
@ -333,7 +333,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
|
||||
if let imageApply = imageApply {
|
||||
if let updateImageSignal = updateImageSignal {
|
||||
strongSelf.imageNode.setSignal(updateImageSignal)
|
||||
strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: true)
|
||||
}
|
||||
|
||||
strongSelf.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
|
||||
@ -351,7 +351,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
}
|
||||
|
||||
if let videoFile = videoFile {
|
||||
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, fileReference: .standalone(media: videoFile))
|
||||
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, fileReference: .standalone(media: videoFile), synchronousLoad: synchronousLoads)
|
||||
thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
strongSelf.layer.addSublayer(thumbnailLayer)
|
||||
let layerHolder = takeSampleBufferLayer()
|
||||
|
||||
@ -14,22 +14,22 @@ import TelegramAnimatedStickerNode
|
||||
final class HorizontalStickerGridItem: GridItem {
|
||||
let account: Account
|
||||
let file: TelegramMediaFile
|
||||
let stickersInteraction: HorizontalStickersChatContextPanelInteraction
|
||||
let interfaceInteraction: ChatPanelInterfaceInteraction
|
||||
let isPreviewed: (HorizontalStickerGridItem) -> Bool
|
||||
let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Void
|
||||
|
||||
let section: GridSection? = nil
|
||||
|
||||
init(account: Account, file: TelegramMediaFile, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) {
|
||||
init(account: Account, file: TelegramMediaFile, isPreviewed: @escaping (HorizontalStickerGridItem) -> Bool, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Void) {
|
||||
self.account = account
|
||||
self.file = file
|
||||
self.stickersInteraction = stickersInteraction
|
||||
self.interfaceInteraction = interfaceInteraction
|
||||
self.isPreviewed = isPreviewed
|
||||
self.sendSticker = sendSticker
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = HorizontalStickerGridItemNode()
|
||||
node.setup(account: self.account, item: self)
|
||||
node.interfaceInteraction = self.interfaceInteraction
|
||||
node.sendSticker = self.sendSticker
|
||||
return node
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ final class HorizontalStickerGridItem: GridItem {
|
||||
return
|
||||
}
|
||||
node.setup(account: self.account, item: self)
|
||||
node.interfaceInteraction = self.interfaceInteraction
|
||||
node.sendSticker = self.sendSticker
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ final class HorizontalStickerGridItemNode: GridItemNode {
|
||||
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
|
||||
var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||||
var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)?
|
||||
|
||||
private var currentIsPreviewing: Bool = false
|
||||
|
||||
@ -108,11 +108,14 @@ final class HorizontalStickerGridItemNode: GridItemNode {
|
||||
self.addSubnode(animationNode)
|
||||
self.animationNode = animationNode
|
||||
}
|
||||
|
||||
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: item.file, small: true, size: fittedDimensions, synchronousLoad: false))
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.alpha = 0.0
|
||||
}
|
||||
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
|
||||
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start())
|
||||
@ -158,8 +161,8 @@ final class HorizontalStickerGridItemNode: GridItemNode {
|
||||
}
|
||||
|
||||
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
|
||||
if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state {
|
||||
let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), self, self.bounds)
|
||||
if let (_, item, _) = self.currentState, case .ended = recognizer.state {
|
||||
self.sendSticker?(.standalone(media: item.file), self, self.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,7 +173,7 @@ final class HorizontalStickerGridItemNode: GridItemNode {
|
||||
func updatePreviewing(animated: Bool) {
|
||||
var isPreviewing = false
|
||||
if let (_, item, _) = self.currentState {
|
||||
isPreviewing = item.stickersInteraction.previewedStickerItem == self.stickerItem
|
||||
//isPreviewing = item.isPreviewed(self.stickerItem)
|
||||
}
|
||||
if self.currentIsPreviewing != isPreviewing {
|
||||
self.currentIsPreviewing = isPreviewing
|
||||
|
||||
@ -66,7 +66,11 @@ private struct StickerEntry: Identifiable, Comparable {
|
||||
}
|
||||
|
||||
func item(account: Account, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> GridItem {
|
||||
return HorizontalStickerGridItem(account: account, file: self.file, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction)
|
||||
return HorizontalStickerGridItem(account: account, file: self.file, isPreviewed: { item in
|
||||
return false//stickersInteraction.previewedStickerItem == item
|
||||
}, sendSticker: { file, node, rect in
|
||||
let _ = interfaceInteraction.sendSticker(file, node, rect)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
388
submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift
Normal file
388
submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift
Normal file
@ -0,0 +1,388 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
|
||||
private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private final class DisplayItem {
|
||||
let file: TelegramMediaFile
|
||||
let frame: CGRect
|
||||
|
||||
init(file: TelegramMediaFile, frame: CGRect) {
|
||||
self.file = file
|
||||
self.frame = frame
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
|
||||
private let scrollNode: ASScrollNode
|
||||
private var items: [TelegramMediaFile] = []
|
||||
private var displayItems: [DisplayItem] = []
|
||||
private var topInset: CGFloat?
|
||||
private var itemNodes: [MediaId: HorizontalStickerGridItemNode] = [:]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var animateInOnLayout: Bool = false
|
||||
|
||||
var updateBackgroundOffset: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
|
||||
var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)?
|
||||
|
||||
init(context: AccountContext) {
|
||||
self.context = context
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
|
||||
super.init()
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.scrollNode.view.alwaysBounceVertical = true
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.delegate = self
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateVisibleItems(synchronous: false)
|
||||
self.updateBackground(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateBackground(transition: ContainedViewLayoutTransition) {
|
||||
if let topInset = self.topInset {
|
||||
self.updateBackgroundOffset?(max(0.0, -self.scrollNode.view.contentOffset.y + topInset), transition)
|
||||
}
|
||||
}
|
||||
|
||||
func updateScrollNode() {
|
||||
guard let size = self.validLayout else {
|
||||
return
|
||||
}
|
||||
var contentHeight: CGFloat = 0.0
|
||||
if let item = self.displayItems.last {
|
||||
let maxY = item.frame.maxY + 4.0
|
||||
|
||||
var topInset = size.height - floor(item.frame.height * 1.5)
|
||||
if topInset + maxY < size.height {
|
||||
topInset = size.height - maxY
|
||||
}
|
||||
self.topInset = topInset
|
||||
contentHeight = topInset + maxY
|
||||
} else {
|
||||
self.topInset = size.height
|
||||
}
|
||||
self.scrollNode.view.contentSize = CGSize(width: size.width, height: max(contentHeight, size.height))
|
||||
}
|
||||
|
||||
func updateItems(items: [TelegramMediaFile]) {
|
||||
self.items = items
|
||||
|
||||
var previousBackgroundOffset: CGFloat?
|
||||
if let topInset = self.topInset {
|
||||
previousBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset)
|
||||
} else {
|
||||
previousBackgroundOffset = self.validLayout?.height
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
self.updateItemsLayout(width: size.width)
|
||||
self.updateScrollNode()
|
||||
}
|
||||
|
||||
self.updateVisibleItems(synchronous: true)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring)
|
||||
|
||||
if let previousBackgroundOffset = previousBackgroundOffset, let topInset = self.topInset {
|
||||
let currentBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset)
|
||||
if abs(currentBackgroundOffset - previousBackgroundOffset) > .ulpOfOne {
|
||||
transition.animateOffsetAdditive(node: self.scrollNode, offset: currentBackgroundOffset - previousBackgroundOffset)
|
||||
self.updateBackground(transition: transition)
|
||||
}
|
||||
} else {
|
||||
self.animateInOnLayout = true
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
var previousBackgroundOffset: CGFloat?
|
||||
if let topInset = self.topInset {
|
||||
previousBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset)
|
||||
} else {
|
||||
previousBackgroundOffset = self.validLayout?.height
|
||||
}
|
||||
|
||||
let previousLayout = self.validLayout
|
||||
self.validLayout = size
|
||||
|
||||
if self.animateInOnLayout {
|
||||
self.updateBackgroundOffset?(size.height, .immediate)
|
||||
}
|
||||
|
||||
var synchronous = false
|
||||
if previousLayout?.width != size.width {
|
||||
synchronous = true
|
||||
self.updateItemsLayout(width: size.width)
|
||||
}
|
||||
|
||||
self.ignoreScrolling = true
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.updateScrollNode()
|
||||
self.ignoreScrolling = false
|
||||
|
||||
self.updateVisibleItems(synchronous: synchronous)
|
||||
|
||||
var backgroundTransition = transition
|
||||
|
||||
if self.animateInOnLayout {
|
||||
self.animateInOnLayout = false
|
||||
backgroundTransition = .animated(duration: 0.3, curve: .spring)
|
||||
if let topInset = self.topInset {
|
||||
let currentBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset)
|
||||
backgroundTransition.animateOffsetAdditive(node: self.scrollNode, offset: currentBackgroundOffset - size.height)
|
||||
}
|
||||
} else {
|
||||
if let previousBackgroundOffset = previousBackgroundOffset, let topInset = self.topInset {
|
||||
let currentBackgroundOffset = max(0.0, -self.scrollNode.view.contentOffset.y + topInset)
|
||||
if abs(currentBackgroundOffset - previousBackgroundOffset) > .ulpOfOne {
|
||||
transition.animateOffsetAdditive(node: self.scrollNode, offset: currentBackgroundOffset - previousBackgroundOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.updateBackground(transition: backgroundTransition)
|
||||
}
|
||||
|
||||
private func updateItemsLayout(width: CGFloat) {
|
||||
self.displayItems.removeAll()
|
||||
|
||||
let itemsPerRow = min(8, max(4, Int(width / 80)))
|
||||
let sideInset: CGFloat = 4.0
|
||||
let itemSpacing: CGFloat = 4.0
|
||||
let itemSize = floor((width - sideInset * 2.0 - itemSpacing * (CGFloat(itemsPerRow) - 1.0)) / CGFloat(itemsPerRow))
|
||||
|
||||
var columnIndex = 0
|
||||
var topOffset: CGFloat = 7.0
|
||||
for i in 0 ..< self.items.count {
|
||||
self.displayItems.append(DisplayItem(file: self.items[i], frame: CGRect(origin: CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: topOffset), size: CGSize(width: itemSize, height: itemSize))))
|
||||
|
||||
columnIndex += 1
|
||||
if columnIndex == itemsPerRow {
|
||||
columnIndex = 0
|
||||
topOffset += itemSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVisibleItems(synchronous: Bool) {
|
||||
guard let _ = self.validLayout, let topInset = self.topInset else {
|
||||
return
|
||||
}
|
||||
|
||||
var minVisibleY = self.scrollNode.view.bounds.minY
|
||||
var maxVisibleY = self.scrollNode.view.bounds.maxY
|
||||
|
||||
let minActivatedY = minVisibleY
|
||||
let maxActivatedY = maxVisibleY
|
||||
|
||||
minVisibleY -= 200.0
|
||||
maxVisibleY += 200.0
|
||||
|
||||
var validIds = Set<MediaId>()
|
||||
for i in 0 ..< self.displayItems.count {
|
||||
let item = self.displayItems[i]
|
||||
|
||||
let itemFrame = item.frame.offsetBy(dx: 0.0, dy: topInset)
|
||||
|
||||
if itemFrame.maxY >= minVisibleY {
|
||||
let isActivated = itemFrame.maxY >= minActivatedY && itemFrame.minY <= maxActivatedY
|
||||
|
||||
let itemNode: HorizontalStickerGridItemNode
|
||||
if let current = self.itemNodes[item.file.fileId] {
|
||||
itemNode = current
|
||||
} else {
|
||||
let item = HorizontalStickerGridItem(
|
||||
account: self.context.account,
|
||||
file: item.file,
|
||||
isPreviewed: { _ in
|
||||
return false
|
||||
}, sendSticker: { [weak self] file, node, rect in
|
||||
self?.sendSticker?(file, node, rect)
|
||||
}
|
||||
)
|
||||
itemNode = item.node(layout: GridNodeLayout(
|
||||
size: CGSize(),
|
||||
insets: UIEdgeInsets(),
|
||||
scrollIndicatorInsets: nil,
|
||||
preloadSize: 0.0,
|
||||
type: .fixed(itemSize: CGSize(), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)
|
||||
), synchronousLoad: synchronous) as! HorizontalStickerGridItemNode
|
||||
itemNode.subnodeTransform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
self.itemNodes[item.file.fileId] = itemNode
|
||||
self.scrollNode.addSubnode(itemNode)
|
||||
}
|
||||
itemNode.frame = itemFrame
|
||||
itemNode.isVisibleInGrid = isActivated
|
||||
validIds.insert(item.file.fileId)
|
||||
}
|
||||
if itemFrame.minY > maxVisibleY {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [MediaId] = []
|
||||
for (id, itemNode) in self.itemNodes {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
itemNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.itemNodes.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let backroundDiameter: CGFloat = 20.0
|
||||
private let shadowBlur: CGFloat = 6.0
|
||||
|
||||
final class InlineReactionSearchPanel: ChatInputContextPanelNode {
|
||||
private let containerNode: ASDisplayNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let backgroundTopLeftNode: ASImageNode
|
||||
private let backgroundTopLeftContainerNode: ASDisplayNode
|
||||
private let backgroundTopRightNode: ASImageNode
|
||||
private let backgroundTopRightContainerNode: ASDisplayNode
|
||||
private let backgroundContainerNode: ASDisplayNode
|
||||
private let stickersNode: InlineReactionSearchStickersNode
|
||||
|
||||
var controllerInteraction: ChatControllerInteraction?
|
||||
|
||||
private var validLayout: (CGSize, CGFloat)?
|
||||
|
||||
override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) {
|
||||
self.containerNode = ASDisplayNode()
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
|
||||
let shadowImage = generateImage(CGSize(width: backroundDiameter + shadowBlur * 2.0, height: floor(backroundDiameter / 2.0 + shadowBlur)), rotatedContext: { size, context in
|
||||
let diameter = backroundDiameter
|
||||
let shadow = UIColor(white: 0.0, alpha: 0.5)
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.saveGState()
|
||||
context.setFillColor(shadow.cgColor)
|
||||
context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor)
|
||||
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
|
||||
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
|
||||
|
||||
context.restoreGState()
|
||||
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter)))
|
||||
})?.stretchableImage(withLeftCapWidth: Int(backroundDiameter / 2.0 + shadowBlur), topCapHeight: 0)
|
||||
|
||||
self.backgroundTopLeftNode = ASImageNode()
|
||||
self.backgroundTopLeftNode.image = shadowImage
|
||||
self.backgroundTopLeftContainerNode = ASDisplayNode()
|
||||
self.backgroundTopLeftContainerNode.clipsToBounds = true
|
||||
self.backgroundTopLeftContainerNode.addSubnode(self.backgroundTopLeftNode)
|
||||
|
||||
self.backgroundTopRightNode = ASImageNode()
|
||||
self.backgroundTopRightNode.image = shadowImage
|
||||
self.backgroundTopRightContainerNode = ASDisplayNode()
|
||||
self.backgroundTopRightContainerNode.clipsToBounds = true
|
||||
self.backgroundTopRightContainerNode.addSubnode(self.backgroundTopRightNode)
|
||||
|
||||
self.backgroundContainerNode = ASDisplayNode()
|
||||
|
||||
self.stickersNode = InlineReactionSearchStickersNode(context: context)
|
||||
|
||||
super.init(context: context, theme: theme, strings: strings, fontSize: fontSize)
|
||||
|
||||
self.placement = .overPanels
|
||||
self.isOpaque = false
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.backgroundContainerNode.addSubnode(self.backgroundNode)
|
||||
self.backgroundContainerNode.addSubnode(self.backgroundTopLeftContainerNode)
|
||||
self.backgroundContainerNode.addSubnode(self.backgroundTopRightContainerNode)
|
||||
self.containerNode.addSubnode(self.backgroundContainerNode)
|
||||
self.containerNode.addSubnode(self.stickersNode)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
||||
self.stickersNode.updateBackgroundOffset = { [weak self] offset, transition in
|
||||
guard let strongSelf = self, let (_, _) = strongSelf.validLayout else {
|
||||
return
|
||||
}
|
||||
transition.updateFrame(node: strongSelf.backgroundContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize()), beginWithCurrentState: false)
|
||||
|
||||
let cornersTransitionDistance: CGFloat = 20.0
|
||||
let cornersTransition: CGFloat = max(0.0, min(1.0, (cornersTransitionDistance - offset) / cornersTransitionDistance))
|
||||
transition.updateSublayerTransformScaleAndOffset(node: strongSelf.backgroundTopLeftContainerNode, scale: 1.0, offset: CGPoint(x: -cornersTransition * backroundDiameter, y: 0.0), beginWithCurrentState: true)
|
||||
transition.updateSublayerTransformScaleAndOffset(node: strongSelf.backgroundTopRightContainerNode, scale: 1.0, offset: CGPoint(x: cornersTransition * backroundDiameter, y: 0.0), beginWithCurrentState: true)
|
||||
}
|
||||
|
||||
self.stickersNode.sendSticker = { [weak self] file, node, rect in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.controllerInteraction?.sendSticker(file, true, node, rect)
|
||||
}
|
||||
}
|
||||
|
||||
func updateResults(results: [TelegramMediaFile]) {
|
||||
self.stickersNode.updateItems(items: results)
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) {
|
||||
self.validLayout = (size, leftInset)
|
||||
|
||||
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: backroundDiameter / 2.0), size: size))
|
||||
|
||||
transition.updateFrame(node: self.backgroundTopLeftContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -shadowBlur), size: CGSize(width: size.width / 2.0, height: backroundDiameter / 2.0 + shadowBlur)))
|
||||
transition.updateFrame(node: self.backgroundTopRightContainerNode, frame: CGRect(origin: CGPoint(x: size.width / 2.0, y: -shadowBlur), size: CGSize(width: size.width - size.width / 2.0, height: backroundDiameter / 2.0 + shadowBlur)))
|
||||
|
||||
transition.updateFrame(node: self.backgroundTopLeftNode, frame: CGRect(origin: CGPoint(x: -shadowBlur, y: 0.0), size: CGSize(width: size.width + shadowBlur * 2.0, height: backroundDiameter / 2.0 + shadowBlur)))
|
||||
transition.updateFrame(node: self.backgroundTopRightNode, frame: CGRect(origin: CGPoint(x: -shadowBlur - size.width / 2.0, y: 0.0), size: CGSize(width: size.width + shadowBlur * 2.0, height: backroundDiameter / 2.0 + shadowBlur)))
|
||||
|
||||
transition.updateFrame(node: self.stickersNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: size.width - leftInset * 2.0, height: size.height)))
|
||||
self.stickersNode.update(size: CGSize(width: size.width - leftInset * 2.0, height: size.height), transition: transition)
|
||||
}
|
||||
|
||||
override func animateOut(completion: @escaping () -> Void) {
|
||||
self.containerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.containerNode.bounds.height - self.backgroundContainerNode.frame.minY), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.backgroundNode.frame.contains(self.view.convert(point, to: self.backgroundNode.view)) {
|
||||
return nil
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import TelegramCore
|
||||
import SyncCore
|
||||
import AVFoundation
|
||||
import ContextUI
|
||||
import TelegramPresentationData
|
||||
|
||||
private final class MultiplexedVideoTrackingNode: ASDisplayNode {
|
||||
var inHierarchyUpdated: ((Bool) -> Void)?
|
||||
@ -26,20 +27,129 @@ private final class MultiplexedVideoTrackingNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
private final class VisibleVideoItem {
|
||||
enum Id: Equatable, Hashable {
|
||||
case saved(MediaId)
|
||||
case trending(MediaId)
|
||||
}
|
||||
let id: Id
|
||||
let fileReference: FileMediaReference
|
||||
let frame: CGRect
|
||||
|
||||
init(fileReference: FileMediaReference, frame: CGRect) {
|
||||
init(fileReference: FileMediaReference, frame: CGRect, isTrending: Bool) {
|
||||
self.fileReference = fileReference
|
||||
self.frame = frame
|
||||
if isTrending {
|
||||
self.id = .trending(fileReference.media.fileId)
|
||||
} else {
|
||||
self.id = .saved(fileReference.media.fileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class MultiplexedVideoNodeFiles {
|
||||
let saved: [FileMediaReference]
|
||||
let trending: [FileMediaReference]
|
||||
|
||||
init(saved: [FileMediaReference], trending: [FileMediaReference]) {
|
||||
self.saved = saved
|
||||
self.trending = trending
|
||||
}
|
||||
}
|
||||
|
||||
private final class TrendingHeaderNode: ASDisplayNode {
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let reactions: [String]
|
||||
private let reactionNodes: [ImmediateTextNode]
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
var reactionSelected: ((String) -> Void)?
|
||||
|
||||
override init() {
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.reactions = [
|
||||
"👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"
|
||||
]
|
||||
self.scrollNode = ASScrollNode()
|
||||
let scrollNode = self.scrollNode
|
||||
self.reactionNodes = reactions.map { reaction -> ImmediateTextNode in
|
||||
let textNode = ImmediateTextNode()
|
||||
textNode.attributedText = NSAttributedString(string: reaction, font: Font.regular(30.0), textColor: .black)
|
||||
scrollNode.addSubnode(textNode)
|
||||
return textNode
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.canCancelContentTouches = true
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
self.reactionNodes[i].view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
}
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let location = recognizer.location(in: self.scrollNode.view)
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
if self.reactionNodes[i].frame.contains(location) {
|
||||
let reaction = self.reactions[i]
|
||||
self.reactionSelected?(reaction)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme, strings: PresentationStrings, width: CGFloat, sideInset: CGFloat) -> CGFloat {
|
||||
let height: CGFloat = 72.0
|
||||
let leftInset: CGFloat = 10.0
|
||||
|
||||
//TODO:localize
|
||||
self.titleNode.attributedText = NSAttributedString(string: "TRENDING GIFS", font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset * 2.0 - sideInset * 2.0, height: 100.0))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: titleSize)
|
||||
|
||||
let reactionSizes = self.reactionNodes.map { reactionNode -> CGSize in
|
||||
return reactionNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
}
|
||||
|
||||
let reactionSpacing: CGFloat = 4.0
|
||||
var reactionsOffset: CGFloat = leftInset - 2.0
|
||||
|
||||
for i in 0 ..< self.reactionNodes.count {
|
||||
if i != 0 {
|
||||
reactionsOffset += reactionSpacing
|
||||
}
|
||||
reactionNodes[i].frame = CGRect(origin: CGPoint(x: reactionsOffset, y: 0.0), size: reactionSizes[i])
|
||||
reactionsOffset += reactionSizes[i].width
|
||||
}
|
||||
reactionsOffset += leftInset - 2.0
|
||||
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 28.0), size: CGSize(width: width, height: 44.0))
|
||||
self.scrollNode.view.contentSize = CGSize(width: reactionsOffset, height: 44.0)
|
||||
|
||||
return height
|
||||
}
|
||||
}
|
||||
|
||||
final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private let account: Account
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
private let trackingNode: MultiplexedVideoTrackingNode
|
||||
var didScroll: ((CGFloat, CGFloat) -> Void)?
|
||||
var didEndScrolling: (() -> Void)?
|
||||
var reactionSelected: ((String) -> Void)?
|
||||
|
||||
var topInset: CGFloat = 0.0 {
|
||||
didSet {
|
||||
@ -59,21 +169,24 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var files: [FileMediaReference] = [] {
|
||||
var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: []) {
|
||||
didSet {
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
self.updateVisibleItems()
|
||||
self.updateVisibleItems(extendSizeForTransition: 0.0, transition: .immediate, synchronous: true)
|
||||
print("MultiplexedVideoNode files updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
||||
}
|
||||
}
|
||||
private var displayItems: [VisibleVideoItem] = []
|
||||
private var visibleThumbnailLayers: [MediaId: SoftwareVideoThumbnailLayer] = [:]
|
||||
private var statusDisposable: [MediaId : MetaDisposable] = [:]
|
||||
private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailLayer] = [:]
|
||||
private var statusDisposable: [VisibleVideoItem.Id: MetaDisposable] = [:]
|
||||
|
||||
private let contextContainerNode: ContextControllerSourceNode
|
||||
let scrollNode: ASScrollNode
|
||||
|
||||
private var visibleLayers: [MediaId: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:]
|
||||
private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:]
|
||||
|
||||
private let savedTitleNode: ImmediateTextNode
|
||||
private let trendingHeaderNode: TrendingHeaderNode
|
||||
|
||||
private var displayLink: CADisplayLink!
|
||||
private var timeOffset = 0.0
|
||||
@ -85,8 +198,10 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
var fileContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void)?
|
||||
var enableVideoNodes = false
|
||||
|
||||
init(account: Account) {
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.trackingNode = MultiplexedVideoTrackingNode()
|
||||
self.trackingNode.isLayerBacked = true
|
||||
|
||||
@ -98,13 +213,26 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.contextContainerNode = ContextControllerSourceNode()
|
||||
self.scrollNode = ASScrollNode()
|
||||
|
||||
//TODO:localization
|
||||
self.savedTitleNode = ImmediateTextNode()
|
||||
self.savedTitleNode.attributedText = NSAttributedString(string: "MY GIFS", font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
|
||||
|
||||
self.trendingHeaderNode = TrendingHeaderNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.trendingHeaderNode.reactionSelected = { [weak self] reaction in
|
||||
self?.reactionSelected?(reaction)
|
||||
}
|
||||
|
||||
self.isOpaque = true
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.alwaysBounceVertical = true
|
||||
|
||||
self.scrollNode.addSubnode(self.savedTitleNode)
|
||||
self.scrollNode.addSubnode(self.trendingHeaderNode)
|
||||
|
||||
self.addSubnode(self.trackingNode)
|
||||
self.addSubnode(self.contextContainerNode)
|
||||
self.contextContainerNode.addSubnode(self.scrollNode)
|
||||
@ -216,13 +344,16 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
private var validSize: CGSize?
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
func updateLayout(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
if self.validSize == nil || !self.validSize!.equalTo(size) {
|
||||
let previousSize = self.validSize ?? CGSize()
|
||||
self.validSize = size
|
||||
self.contextContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
self.updateVisibleItems(transition: transition)
|
||||
self.updateVisibleItems(extendSizeForTransition: max(0.0, previousSize.height - size.height), transition: transition)
|
||||
print("MultiplexedVideoNode layout updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
|
||||
}
|
||||
}
|
||||
@ -242,9 +373,12 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private var currentExtendSizeForTransition: CGFloat = 0.0
|
||||
|
||||
private var validVisibleItemsOffset: CGFloat?
|
||||
private func updateImmediatelyVisibleItems(ensureFrames: Bool = false) {
|
||||
let visibleBounds = self.scrollNode.bounds
|
||||
private func updateImmediatelyVisibleItems(ensureFrames: Bool = false, synchronous: Bool = false) {
|
||||
var visibleBounds = self.scrollNode.bounds
|
||||
visibleBounds.size.height += max(0.0, self.currentExtendSizeForTransition)
|
||||
let visibleThumbnailBounds = visibleBounds.insetBy(dx: 0.0, dy: -350.0)
|
||||
|
||||
if let validVisibleItemsOffset = self.validVisibleItemsOffset, validVisibleItemsOffset.isEqual(to: visibleBounds.origin.y) {
|
||||
@ -257,8 +391,8 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
let minVisibleThumbnailY = visibleThumbnailBounds.minY
|
||||
let maxVisibleThumbnailY = visibleThumbnailBounds.maxY
|
||||
|
||||
var visibleThumbnailIds = Set<MediaId>()
|
||||
var visibleIds = Set<MediaId>()
|
||||
var visibleThumbnailIds = Set<VisibleVideoItem.Id>()
|
||||
var visibleIds = Set<VisibleVideoItem.Id>()
|
||||
|
||||
for item in self.displayItems {
|
||||
if item.frame.maxY < minVisibleThumbnailY {
|
||||
@ -268,17 +402,17 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
break;
|
||||
}
|
||||
|
||||
visibleThumbnailIds.insert(item.fileReference.media.fileId)
|
||||
visibleThumbnailIds.insert(item.id)
|
||||
|
||||
if let thumbnailLayer = self.visibleThumbnailLayers[item.fileReference.media.fileId] {
|
||||
if let thumbnailLayer = self.visibleThumbnailLayers[item.id] {
|
||||
if ensureFrames {
|
||||
thumbnailLayer.frame = item.frame
|
||||
}
|
||||
} else {
|
||||
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.fileReference)
|
||||
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.fileReference, synchronousLoad: synchronous)
|
||||
thumbnailLayer.frame = item.frame
|
||||
self.scrollNode.layer.addSublayer(thumbnailLayer)
|
||||
self.visibleThumbnailLayers[item.fileReference.media.fileId] = thumbnailLayer
|
||||
self.visibleThumbnailLayers[item.id] = thumbnailLayer
|
||||
}
|
||||
|
||||
let progressSize = CGSize(width: 24.0, height: 24.0)
|
||||
@ -291,9 +425,9 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
continue
|
||||
}
|
||||
|
||||
visibleIds.insert(item.fileReference.media.fileId)
|
||||
visibleIds.insert(item.id)
|
||||
|
||||
if let (_, layerHolder) = self.visibleLayers[item.fileReference.media.fileId] {
|
||||
if let (_, layerHolder) = self.visibleLayers[item.id] {
|
||||
if ensureFrames {
|
||||
layerHolder.layer.frame = item.frame
|
||||
}
|
||||
@ -303,23 +437,23 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
layerHolder.layer.frame = item.frame
|
||||
self.scrollNode.layer.addSublayer(layerHolder.layer)
|
||||
let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.fileReference, layerHolder: layerHolder)
|
||||
self.visibleLayers[item.fileReference.media.fileId] = (manager, layerHolder)
|
||||
self.visibleThumbnailLayers[item.fileReference.media.fileId]?.ready = { [weak self] in
|
||||
self.visibleLayers[item.id] = (manager, layerHolder)
|
||||
self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.visibleLayers[item.fileReference.media.fileId]?.0.start()
|
||||
strongSelf.visibleLayers[item.id]?.0.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [MediaId] = []
|
||||
var removeIds: [VisibleVideoItem.Id] = []
|
||||
for id in self.visibleLayers.keys {
|
||||
if !visibleIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
}
|
||||
}
|
||||
|
||||
var removeThumbnailIds: [MediaId] = []
|
||||
var removeThumbnailIds: [VisibleVideoItem.Id] = []
|
||||
for id in self.visibleThumbnailLayers.keys {
|
||||
if !visibleThumbnailIds.contains(id) {
|
||||
removeThumbnailIds.append(id)
|
||||
@ -353,116 +487,255 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}*/
|
||||
}
|
||||
|
||||
private func updateVisibleItems(transition: ContainedViewLayoutTransition = .immediate) {
|
||||
private func updateVisibleItems(extendSizeForTransition: CGFloat, transition: ContainedViewLayoutTransition, synchronous: Bool = false) {
|
||||
let drawableSize = self.scrollNode.bounds.size
|
||||
if !drawableSize.width.isZero {
|
||||
var displayItems: [VisibleVideoItem] = []
|
||||
|
||||
let idealHeight = self.idealHeight
|
||||
|
||||
var weights: [Int] = []
|
||||
var totalItemSize: CGFloat = 0.0
|
||||
for item in self.files {
|
||||
let aspectRatio: CGFloat
|
||||
if let dimensions = item.media.dimensions {
|
||||
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
|
||||
} else {
|
||||
aspectRatio = 1.0
|
||||
var verticalOffset: CGFloat = self.topInset
|
||||
|
||||
func commitFilesSpans(files: [FileMediaReference], isTrending: Bool) {
|
||||
var rowsCount = 0
|
||||
var firstRowMax = 0;
|
||||
|
||||
let viewPortAvailableSize = drawableSize.width
|
||||
|
||||
let preferredRowSize: CGFloat = 100.0
|
||||
let itemsCount = files.count
|
||||
let spanCount: CGFloat = 100.0
|
||||
var spanLeft = spanCount
|
||||
var currentItemsInRow = 0
|
||||
var currentItemsSpanAmount: CGFloat = 0.0
|
||||
|
||||
var itemSpans: [Int: CGFloat] = [:]
|
||||
var itemsToRow: [Int: Int] = [:]
|
||||
|
||||
for a in 0 ..< itemsCount {
|
||||
var size: CGSize
|
||||
if let dimensions = files[a].media.dimensions {
|
||||
size = dimensions.cgSize
|
||||
} else {
|
||||
size = CGSize(width: 100.0, height: 100.0)
|
||||
}
|
||||
if size.width <= 0.0 {
|
||||
size.width = 100.0
|
||||
}
|
||||
if size.height <= 0.0 {
|
||||
size.height = 100.0
|
||||
}
|
||||
//size = CGSize(width: 100.0, height: 100.0)
|
||||
let aspect: CGFloat = size.width / size.height
|
||||
if aspect > 4.0 || aspect < 0.2 {
|
||||
size.width = max(size.width, size.height)
|
||||
size.height = size.width
|
||||
}
|
||||
|
||||
var requiredSpan = min(spanCount, floor(spanCount * (size.width / size.height * preferredRowSize / viewPortAvailableSize)))
|
||||
let moveToNewRow = spanLeft < requiredSpan || requiredSpan > 33.0 && spanLeft < requiredSpan - 15.0
|
||||
if moveToNewRow {
|
||||
if spanLeft > 0 {
|
||||
let spanPerItem = floor(spanLeft / CGFloat(currentItemsInRow))
|
||||
|
||||
let start = a - currentItemsInRow
|
||||
var b = start
|
||||
while b < start + currentItemsInRow {
|
||||
if (b == start + currentItemsInRow - 1) {
|
||||
itemSpans[b] = itemSpans[b]! + spanLeft
|
||||
} else {
|
||||
itemSpans[b] = itemSpans[b]! + spanPerItem
|
||||
}
|
||||
spanLeft -= spanPerItem;
|
||||
|
||||
b += 1
|
||||
}
|
||||
|
||||
itemsToRow[a - 1] = rowsCount
|
||||
}
|
||||
rowsCount += 1
|
||||
currentItemsSpanAmount = 0
|
||||
currentItemsInRow = 0
|
||||
spanLeft = spanCount
|
||||
} else {
|
||||
if spanLeft < requiredSpan {
|
||||
requiredSpan = spanLeft
|
||||
}
|
||||
}
|
||||
if rowsCount == 0 {
|
||||
firstRowMax = max(firstRowMax, a)
|
||||
}
|
||||
if a == itemsCount - 1 {
|
||||
itemsToRow[a] = rowsCount
|
||||
}
|
||||
currentItemsSpanAmount += requiredSpan
|
||||
currentItemsInRow += 1
|
||||
spanLeft -= requiredSpan
|
||||
spanLeft = max(0, spanLeft)
|
||||
|
||||
itemSpans[a] = requiredSpan
|
||||
}
|
||||
if itemsCount != 0 {
|
||||
rowsCount += 1
|
||||
}
|
||||
|
||||
var currentRowHorizontalOffset: CGFloat = 0.0
|
||||
for index in 0 ..< files.count {
|
||||
guard let width = itemSpans[index] else {
|
||||
continue
|
||||
}
|
||||
let itemWidth = floor(width * drawableSize.width / 100.0) - 1
|
||||
|
||||
var itemSize = CGSize(width: itemWidth, height: preferredRowSize)
|
||||
if itemsToRow[index] != nil {
|
||||
itemSize.width = max(itemSize.width, drawableSize.width - currentRowHorizontalOffset)
|
||||
}
|
||||
displayItems.append(VisibleVideoItem(fileReference: files[index], frame: CGRect(origin: CGPoint(x: currentRowHorizontalOffset, y: verticalOffset), size: itemSize), isTrending: isTrending))
|
||||
currentRowHorizontalOffset += itemSize.width + 1.0
|
||||
|
||||
if itemsToRow[index] != nil {
|
||||
verticalOffset += preferredRowSize + 1.0
|
||||
currentRowHorizontalOffset = 0.0
|
||||
}
|
||||
}
|
||||
weights.append(Int(aspectRatio * 100))
|
||||
totalItemSize += aspectRatio * idealHeight
|
||||
}
|
||||
|
||||
let numberOfRows = max(Int(round(totalItemSize / drawableSize.width)), 1)
|
||||
|
||||
let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows)
|
||||
|
||||
var i = 0
|
||||
var offset = CGPoint(x: 0.0, y: self.topInset)
|
||||
var previousItemSize: CGFloat = 0.0
|
||||
var contentMaxValueInScrollDirection: CGFloat = self.topInset
|
||||
let maxWidth = drawableSize.width
|
||||
|
||||
let minimumInteritemSpacing: CGFloat = 1.0
|
||||
let minimumLineSpacing: CGFloat = 1.0
|
||||
|
||||
let viewportWidth: CGFloat = drawableSize.width
|
||||
|
||||
let preferredRowSize = idealHeight
|
||||
|
||||
var rowIndex = -1
|
||||
for row in partition {
|
||||
rowIndex += 1
|
||||
|
||||
var summedRatios: CGFloat = 0.0
|
||||
|
||||
var j = i
|
||||
var n = i + row.count
|
||||
|
||||
while j < n {
|
||||
func commitFiles(files: [FileMediaReference], isTrending: Bool) {
|
||||
var weights: [Int] = []
|
||||
var totalItemSize: CGFloat = 0.0
|
||||
for item in files {
|
||||
let aspectRatio: CGFloat
|
||||
if let dimensions = self.files[j].media.dimensions {
|
||||
if let dimensions = item.media.dimensions {
|
||||
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
|
||||
} else {
|
||||
aspectRatio = 1.0
|
||||
}
|
||||
|
||||
summedRatios += aspectRatio
|
||||
|
||||
j += 1
|
||||
weights.append(Int(aspectRatio * 100))
|
||||
totalItemSize += aspectRatio * idealHeight
|
||||
}
|
||||
|
||||
var rowSize = drawableSize.width - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
let numberOfRows = max(Int(round(totalItemSize / drawableSize.width)), 1)
|
||||
|
||||
if rowIndex == partition.count - 1 {
|
||||
if row.count < 2 {
|
||||
rowSize = floor(viewportWidth / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
} else if row.count < 3 {
|
||||
rowSize = floor(viewportWidth * 2.0 / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
}
|
||||
}
|
||||
let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows)
|
||||
|
||||
j = i
|
||||
n = i + row.count
|
||||
var i = 0
|
||||
var offset = CGPoint(x: 0.0, y: verticalOffset)
|
||||
var previousItemSize: CGFloat = 0.0
|
||||
let maxWidth = drawableSize.width
|
||||
|
||||
while j < n {
|
||||
let aspectRatio: CGFloat
|
||||
if let dimensions = self.files[j].media.dimensions {
|
||||
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
|
||||
} else {
|
||||
aspectRatio = 1.0
|
||||
}
|
||||
let preferredAspectRatio = aspectRatio
|
||||
let minimumInteritemSpacing: CGFloat = 1.0
|
||||
let minimumLineSpacing: CGFloat = 1.0
|
||||
|
||||
let viewportWidth: CGFloat = drawableSize.width
|
||||
|
||||
let preferredRowSize = idealHeight
|
||||
|
||||
var rowIndex = -1
|
||||
for row in partition {
|
||||
rowIndex += 1
|
||||
|
||||
let actualSize = CGSize(width: round(rowSize / summedRatios * (preferredAspectRatio)), height: preferredRowSize)
|
||||
var summedRatios: CGFloat = 0.0
|
||||
|
||||
var frame = CGRect(x: offset.x, y: offset.y, width: actualSize.width, height: actualSize.height)
|
||||
if frame.origin.x + frame.size.width >= maxWidth - 2.0 {
|
||||
frame.size.width = max(1.0, maxWidth - frame.origin.x)
|
||||
var j = i
|
||||
var n = i + row.count
|
||||
|
||||
while j < n {
|
||||
let aspectRatio: CGFloat
|
||||
if let dimensions = files[j].media.dimensions {
|
||||
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
|
||||
} else {
|
||||
aspectRatio = 1.0
|
||||
}
|
||||
|
||||
summedRatios += aspectRatio
|
||||
|
||||
j += 1
|
||||
}
|
||||
|
||||
displayItems.append(VisibleVideoItem(fileReference: self.files[j], frame: frame))
|
||||
var rowSize = drawableSize.width - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
|
||||
offset.x += actualSize.width + minimumInteritemSpacing
|
||||
previousItemSize = actualSize.height
|
||||
contentMaxValueInScrollDirection = frame.maxY
|
||||
if rowIndex == partition.count - 1 {
|
||||
if row.count < 2 {
|
||||
rowSize = floor(viewportWidth / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
} else if row.count < 3 {
|
||||
rowSize = floor(viewportWidth * 2.0 / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
j += 1
|
||||
j = i
|
||||
n = i + row.count
|
||||
|
||||
while j < n {
|
||||
let aspectRatio: CGFloat
|
||||
if let dimensions = files[j].media.dimensions {
|
||||
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
|
||||
} else {
|
||||
aspectRatio = 1.0
|
||||
}
|
||||
let preferredAspectRatio = aspectRatio
|
||||
|
||||
let actualSize = CGSize(width: round(rowSize / summedRatios * (preferredAspectRatio)), height: preferredRowSize)
|
||||
|
||||
var frame = CGRect(x: offset.x, y: offset.y, width: actualSize.width, height: actualSize.height)
|
||||
if frame.origin.x + frame.size.width >= maxWidth - 2.0 {
|
||||
frame.size.width = max(1.0, maxWidth - frame.origin.x)
|
||||
}
|
||||
|
||||
displayItems.append(VisibleVideoItem(fileReference: files[j], frame: frame, isTrending: isTrending))
|
||||
|
||||
offset.x += actualSize.width + minimumInteritemSpacing
|
||||
previousItemSize = actualSize.height
|
||||
verticalOffset = frame.maxY
|
||||
|
||||
j += 1
|
||||
}
|
||||
|
||||
if row.count > 0 {
|
||||
offset = CGPoint(x: 0.0, y: offset.y + previousItemSize + minimumLineSpacing)
|
||||
}
|
||||
|
||||
i += row.count
|
||||
}
|
||||
|
||||
if row.count > 0 {
|
||||
offset = CGPoint(x: 0.0, y: offset.y + previousItemSize + minimumLineSpacing)
|
||||
}
|
||||
|
||||
i += row.count
|
||||
}
|
||||
let contentSize = CGSize(width: drawableSize.width, height: contentMaxValueInScrollDirection + self.bottomInset)
|
||||
|
||||
if !self.files.saved.isEmpty {
|
||||
self.savedTitleNode.isHidden = false
|
||||
let leftInset: CGFloat = 10.0
|
||||
let savedTitleSize = self.savedTitleNode.updateLayout(CGSize(width: drawableSize.width - leftInset * 2.0, height: 100.0))
|
||||
self.savedTitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset - 3.0), size: savedTitleSize)
|
||||
verticalOffset += savedTitleSize.height + 5.0
|
||||
commitFilesSpans(files: self.files.saved, isTrending: false)
|
||||
//commitFiles(files: self.files.saved, isTrending: false)
|
||||
} else {
|
||||
self.savedTitleNode.isHidden = true
|
||||
}
|
||||
if !self.files.trending.isEmpty {
|
||||
self.trendingHeaderNode.isHidden = false
|
||||
let trendingHeight = self.trendingHeaderNode.update(theme: self.theme, strings: self.strings, width: drawableSize.width, sideInset: 0.0)
|
||||
self.trendingHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: drawableSize.width, height: trendingHeight))
|
||||
verticalOffset += trendingHeight
|
||||
commitFilesSpans(files: self.files.trending, isTrending: true)
|
||||
//commitFiles(files: self.files.trending, isTrending: true)
|
||||
} else {
|
||||
self.trendingHeaderNode.isHidden = true
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: drawableSize.width, height: verticalOffset + self.bottomInset)
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
|
||||
self.displayItems = displayItems
|
||||
|
||||
self.validVisibleItemsOffset = nil
|
||||
self.updateImmediatelyVisibleItems(ensureFrames: true)
|
||||
self.currentExtendSizeForTransition = extendSizeForTransition
|
||||
self.updateImmediatelyVisibleItems(ensureFrames: true, synchronous: synchronous)
|
||||
|
||||
transition.updateAlpha(node: scrollNode, alpha: 1.0, force: true, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.currentExtendSizeForTransition = 0.0
|
||||
strongSelf.updateImmediatelyVisibleItems()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -435,7 +435,7 @@ class PaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
snapshot.frame = CGRect(origin: self.textField.placeholderLabel.frame.origin, size: node.labelNode.frame.size)
|
||||
self.textField.layer.addSublayer(snapshot)
|
||||
snapshot.animateAlpha(from: 0.0, to: 1.0, duration: duration * 2.0 / 3.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue)
|
||||
self.textField.placeholderLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 3.0 / 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false)
|
||||
//self.textField.placeholderLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 3.0 / 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false)
|
||||
|
||||
}
|
||||
|
||||
@ -491,4 +491,9 @@ class PaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
self.textFieldDidChange(self.textField)
|
||||
}
|
||||
}
|
||||
|
||||
func updateQuery(_ query: String) {
|
||||
self.textField.text = query
|
||||
self.textFieldDidChange(self.textField)
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,6 +83,12 @@ final class PaneSearchContainerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
|
||||
if let contentNode = self.contentNode as? GifPaneSearchContentNode {
|
||||
contentNode.requestUpdateQuery = { [weak self] query in
|
||||
self?.updateQuery(query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
@ -100,6 +106,10 @@ final class PaneSearchContainerNode: ASDisplayNode {
|
||||
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
|
||||
}
|
||||
|
||||
func updateQuery(_ query: String) {
|
||||
self.searchBar.updateQuery(query)
|
||||
}
|
||||
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
|
||||
return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight))
|
||||
}
|
||||
@ -121,15 +131,25 @@ final class PaneSearchContainerNode: ASDisplayNode {
|
||||
self.searchBar.deactivate(clear: true)
|
||||
}
|
||||
|
||||
func animateIn(from placeholder: PaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
let verticalOrigin = placeholderFrame.minY - 4.0
|
||||
self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition)
|
||||
func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y
|
||||
if let placeholder = placeholder {
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
verticalOrigin = placeholderFrame.minY - 4.0
|
||||
self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition)
|
||||
} else {
|
||||
self.contentNode.animateIn(additivePosition: 0.0, transition: transition)
|
||||
}
|
||||
|
||||
switch transition {
|
||||
case let .animated(duration, curve):
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0)
|
||||
self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction, completion: completion)
|
||||
if let placeholder = placeholder {
|
||||
self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction, completion: completion)
|
||||
} else {
|
||||
self.searchBar.alpha = 0.0
|
||||
transition.updateAlpha(node: self.searchBar, alpha: 1.0)
|
||||
}
|
||||
if let size = self.validLayout {
|
||||
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin)))
|
||||
self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction)
|
||||
|
||||
@ -56,6 +56,8 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
private var item: (VisualMediaItem, Media?, CGSize, CGSize?)?
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
private var hasVisibility: Bool = false
|
||||
|
||||
init(context: AccountContext, interaction: VisualMediaItemInteraction) {
|
||||
self.context = context
|
||||
self.interaction = interaction
|
||||
@ -192,7 +194,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
} else {
|
||||
sampleBufferLayer = takeSampleBufferLayer()
|
||||
self.sampleBufferLayer = sampleBufferLayer
|
||||
self.containerNode.layer.insertSublayer(sampleBufferLayer.layer, above: self.imageNode.layer)
|
||||
self.imageNode.layer.addSublayer(sampleBufferLayer.layer)
|
||||
}
|
||||
|
||||
self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer)
|
||||
@ -327,6 +329,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func updateIsVisible(_ isVisible: Bool) {
|
||||
self.hasVisibility = isVisible
|
||||
if let _ = self.videoLayerFrameManager {
|
||||
let displayLink: ConstantDisplayLinkAnimator
|
||||
if let current = self.displayLink {
|
||||
@ -342,8 +345,8 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
displayLink.frameInterval = 2
|
||||
self.displayLink = displayLink
|
||||
}
|
||||
displayLink.isPaused = !isVisible
|
||||
}
|
||||
self.displayLink?.isPaused = !self.hasVisibility || self.isHidden
|
||||
}
|
||||
|
||||
func updateSelectionState(animated: Bool) {
|
||||
@ -420,6 +423,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
} else {
|
||||
self.isHidden = false
|
||||
}
|
||||
self.displayLink?.isPaused = !self.hasVisibility || self.isHidden
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -63,7 +63,7 @@ final class SoftwareVideoLayerFrameManager {
|
||||
func start() {
|
||||
let secondarySignal: Signal<String?, NoError>
|
||||
if let secondaryResource = self.secondaryResource {
|
||||
secondarySignal = self.account.postbox.mediaBox.resourceData(self.resource, option: .complete(waitUntilFetchStatus: false))
|
||||
secondarySignal = self.account.postbox.mediaBox.resourceData(secondaryResource, option: .complete(waitUntilFetchStatus: false))
|
||||
|> map { data -> String? in
|
||||
if data.complete {
|
||||
return data.path
|
||||
|
||||
@ -23,7 +23,7 @@ final class SoftwareVideoThumbnailLayer: CALayer {
|
||||
}
|
||||
}
|
||||
|
||||
init(account: Account, fileReference: FileMediaReference) {
|
||||
init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool) {
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = UIColor.clear.cgColor
|
||||
@ -31,7 +31,7 @@ final class SoftwareVideoThumbnailLayer: CALayer {
|
||||
self.masksToBounds = true
|
||||
|
||||
if let dimensions = fileReference.media.dimensions {
|
||||
self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference)).start(next: { [weak self] transform in
|
||||
self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference, synchronousLoad: synchronousLoad)).start(next: { [weak self] transform in
|
||||
var boundingSize = dimensions.cgSize.aspectFilled(CGSize(width: 93.0, height: 93.0))
|
||||
let imageSize = boundingSize
|
||||
boundingSize.width = min(200.0, boundingSize.width)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user