GIF-related improvements

This commit is contained in:
Ali 2020-05-22 19:13:47 +04:00
parent 29b23c767f
commit 2ab830e3a1
30 changed files with 1144 additions and 236 deletions

View File

@ -1 +1 @@
11.4.1 11.5

View File

@ -114,11 +114,13 @@ public final class ActivityIndicator: ASDisplayNode {
override public func didLoad() { override public func didLoad() {
super.didLoad() super.didLoad()
let indicatorView = UIActivityIndicatorView(style: .whiteLarge) let indicatorView: UIActivityIndicatorView
switch self.type { switch self.type {
case let .navigationAccent(color): case let .navigationAccent(color):
indicatorView = UIActivityIndicatorView(style: .whiteLarge)
indicatorView.color = color 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) indicatorView.color = convertIndicatorColor(color)
if !forceCustom { if !forceCustom {
self.view.addSubview(indicatorView) self.view.addSubview(indicatorView)

View File

@ -433,8 +433,8 @@ public extension ContainedViewLayoutTransition {
} }
} }
func updateAlpha(node: ASDisplayNode, alpha: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { func updateAlpha(node: ASDisplayNode, alpha: CGFloat, beginWithCurrentState: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
if node.alpha.isEqual(to: alpha) { if node.alpha.isEqual(to: alpha) && !force {
if let completion = completion { if let completion = completion {
completion(true) completion(true)
} }

View File

@ -489,7 +489,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.playOnContentOwnership = false strongSelf.playOnContentOwnership = false
strongSelf.initiallyActivated = true strongSelf.initiallyActivated = true
strongSelf.skipInitialPause = true strongSelf.skipInitialPause = true
strongSelf.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .stop) strongSelf.videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: isAnimated ? .loop : .stop)
} }
} }
} }

View File

@ -12,17 +12,21 @@ public enum RequestChatContextResultsError {
public final class CachedChatContextResult: PostboxCoding { public final class CachedChatContextResult: PostboxCoding {
public let data: Data public let data: Data
public let timestamp: Int32
public init(data: Data) { public init(data: Data, timestamp: Int32) {
self.data = data self.data = data
self.timestamp = timestamp
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
self.data = decoder.decodeDataForKey("data") ?? Data() self.data = decoder.decodeDataForKey("data") ?? Data()
self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0)
} }
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
encoder.encodeData(self.data, forKey: "data") encoder.encodeData(self.data, forKey: "data")
encoder.encodeInt32(self.timestamp, forKey: "timestamp")
} }
} }
@ -35,7 +39,7 @@ private struct RequestData: Codable {
let query: String 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> { 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 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)) let key = ValueBoxKey(MemoryBuffer(data: keyData))
if let cachedEntry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key)) as? CachedChatContextResult { 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) { 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 return account.postbox.transaction { transaction -> ChatContextResultCollection? in
if result.cacheTimeout > 10 { if result.cacheTimeout > 10 && offset.isEmpty {
if let resultData = try? JSONEncoder().encode(result) { if let resultData = try? JSONEncoder().encode(result) {
let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query) let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query)
if let keyData = try? JSONEncoder().encode(requestData) { if let keyData = try? JSONEncoder().encode(requestData) {
let key = ValueBoxKey(MemoryBuffer(data: keyData)) 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)
} }
} }
} }

View File

@ -253,7 +253,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
} }
if let videoFileReference = videoFileReference { 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) self.layer.addSublayer(thumbnailLayer)
let layerHolder = takeSampleBufferLayer() let layerHolder = takeSampleBufferLayer()
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill

View File

@ -203,6 +203,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private let searching = ValuePromise<Bool>(false, ignoreRepeated: true) private let searching = ValuePromise<Bool>(false, ignoreRepeated: true)
private let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>() private let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>()
private let loadingMessage = ValuePromise<Bool>(false, ignoreRepeated: true) private let loadingMessage = ValuePromise<Bool>(false, ignoreRepeated: true)
private let performingInlineSearch = ValuePromise<Bool>(false, ignoreRepeated: true)
private var preloadHistoryPeerId: PeerId? private var preloadHistoryPeerId: PeerId?
private let preloadHistoryPeerIdDisposable = MetaDisposable() private let preloadHistoryPeerIdDisposable = MetaDisposable()
@ -4543,7 +4544,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return nil 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 { switch self.chatLocation {
case let .peer(peerId): case let .peer(peerId):
@ -5127,13 +5128,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return nil return nil
}) })
} }
if case .contextRequest = kind {
self.performingInlineSearch.set(false)
}
case let .update(query, signal): case let .update(query, signal):
let currentQueryAndDisposable = self.contextQueryStates[kind] let currentQueryAndDisposable = self.contextQueryStates[kind]
currentQueryAndDisposable?.1.dispose() currentQueryAndDisposable?.1.dispose()
var inScope = true var inScope = true
var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? 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 let strongSelf = self {
if Thread.isMainThread && inScope { if Thread.isMainThread && inScope {
inScope = false inScope = false
@ -5148,13 +5153,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
}, error: { [weak self] error in }, error: { [weak self] error in
if let strongSelf = self { if let strongSelf = self {
if case .contextRequest = kind {
strongSelf.performingInlineSearch.set(false)
}
switch error { switch error {
case let .inlineBotLocationRequest(peerId): 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: { 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() 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: { }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).start() let _ = ApplicationSpecificNotice.setInlineBotLocationRequest(accountManager: strongSelf.context.sharedContext.accountManager, peerId: peerId, value: 0).start()
})]), in: .window(.root)) })]), 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 updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedInputQueryResult(queryKind: kind, { previousResult in
return inScopeResult(previousResult) return inScopeResult(previousResult)
}) })
} else {
if case .contextRequest = kind {
self.performingInlineSearch.set(true)
}
} }
if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {

View File

@ -1201,7 +1201,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
if transition.isAnimated, let derivedLayoutState = self.derivedLayoutState { if transition.isAnimated, let derivedLayoutState = self.derivedLayoutState {
let offset = derivedLayoutState.inputContextPanelsOverMainPanelFrame.maxY - inputContextPanelsOverMainPanelFrame.maxY 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 { if let inputContextPanelNode = self.inputContextPanelNode {

View File

@ -71,14 +71,14 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa
switch inputQueryResult { switch inputQueryResult {
case let .stickers(results): case let .stickers(results):
if !results.isEmpty { if !results.isEmpty {
if let currentPanel = currentPanel as? HorizontalStickersChatContextPanelNode { if let currentPanel = currentPanel as? InlineReactionSearchPanel {
currentPanel.updateResults(results.map({ $0.file })) currentPanel.updateResults(results: results.map({ $0.file }))
return currentPanel return currentPanel
} else { } 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.controllerInteraction = controllerInteraction
panel.interfaceInteraction = interfaceInteraction panel.interfaceInteraction = interfaceInteraction
panel.updateResults(results.map({ $0.file })) panel.updateResults(results: results.map({ $0.file }))
return panel return panel
} }
} }
@ -94,7 +94,7 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa
return panel return panel
} }
} }
case let .emojis(results, range): case let .emojis(results, _):
if !results.isEmpty { if !results.isEmpty {
if let currentPanel = currentPanel as? EmojisChatInputContextPanelNode { if let currentPanel = currentPanel as? EmojisChatInputContextPanelNode {
currentPanel.updateResults(results) currentPanel.updateResults(results)

View File

@ -26,13 +26,25 @@ private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) {
final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate { final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
private let account: Account private let account: Account
private var theme: PresentationTheme
private var strings: PresentationStrings
private let controllerInteraction: ChatControllerInteraction private let controllerInteraction: ChatControllerInteraction
private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void
private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void
private let openGifContextMenu: (FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> 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 var multiplexedNode: MultiplexedVideoNode?
private let emptyNode: ImmediateTextNode 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) { 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.account = account
self.theme = theme
self.strings = strings
self.controllerInteraction = controllerInteraction self.controllerInteraction = controllerInteraction
self.paneDidScroll = paneDidScroll self.paneDidScroll = paneDidScroll
self.fixPaneScroll = fixPaneScroll self.fixPaneScroll = fixPaneScroll
@ -64,7 +78,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
self.addSubnode(self.emptyNode) self.addSubnode(self.emptyNode)
self.searchPlaceholderNode.activate = { [weak self] in self.searchPlaceholderNode.activate = { [weak self] in
self?.inputNodeInteraction?.toggleSearch(true, .gif) self?.inputNodeInteraction?.toggleSearch(true, .gif, "")
} }
self.updateThemeAndStrings(theme: theme, strings: strings) self.updateThemeAndStrings(theme: theme, strings: strings)
@ -75,6 +89,9 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
} }
override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { 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.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) self.searchPlaceholderNode.setup(theme: theme, strings: strings, type: .gifs)
@ -108,7 +125,11 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
} }
override var isEmpty: Bool { 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() { override func willEnterHierarchy() {
@ -118,7 +139,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
} }
private func updateMultiplexedNodeLayout(changedIsExpanded: Bool, transition: ContainedViewLayoutTransition) { 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 return
} }
@ -137,52 +158,79 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size) var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size)
if changedIsExpanded { 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) 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) self.searchPlaceholderNode.frame = CGRect(x: 0.0, y: 41.0, width: size.width, height: 56.0)
} }
} }
func initializeIfNeeded() { func initializeIfNeeded() {
if self.multiplexedNode == nil { 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 self.multiplexedNode = multiplexedNode
if let layout = self.validLayout { if let layout = self.validLayout {
multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout.0) 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) self.addSubnode(multiplexedNode)
multiplexedNode.scrollNode.addSubnode(self.searchPlaceholderNode) multiplexedNode.scrollNode.addSubnode(self.searchPlaceholderNode)
let gifs = self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]) let gifs = combineLatest(self.trendingPromise.get(), self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]))
|> map { view -> [FileMediaReference] in |> map { trending, view -> MultiplexedVideoNodeFiles in
var recentGifs: OrderedItemListView? var recentGifs: OrderedItemListView?
if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] { if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] {
recentGifs = orderedView as? OrderedItemListView recentGifs = orderedView as? OrderedItemListView
} }
var saved: [FileMediaReference] = []
if let recentGifs = recentGifs { 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 let file = (item.contents as! RecentMediaItem).media as! TelegramMediaFile
return .savedGif(media: file) return .savedGif(media: file)
} }
} else { } else {
return [] saved = []
} }
return MultiplexedVideoNodeFiles(saved: saved, trending: trending ?? [])
} }
self.disposable.set((gifs self.disposable.set((gifs
|> deliverOnMainQueue).start(next: { [weak self] gifs in |> deliverOnMainQueue).start(next: { [weak self] files in
if let strongSelf = self { if let strongSelf = self {
let previousFiles = strongSelf.multiplexedNode?.files let previousFiles = strongSelf.multiplexedNode?.files
strongSelf.multiplexedNode?.files = gifs strongSelf.multiplexedNode?.files = files
strongSelf.emptyNode.isHidden = !gifs.isEmpty let wasEmpty: Bool
if (previousFiles ?? []).isEmpty && !gifs.isEmpty { 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) strongSelf.multiplexedNode?.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 60.0)
} }
} }

View File

@ -165,7 +165,7 @@ enum ChatMediaInputGridEntry: Equatable, Comparable, Identifiable {
switch self { switch self {
case let .search(theme, strings): case let .search(theme, strings):
return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: { return PaneSearchBarPlaceholderItem(theme: theme, strings: strings, type: .stickers, activate: {
inputNodeInteraction.toggleSearch(true, .sticker) inputNodeInteraction.toggleSearch(true, .sticker, "")
}) })
case let .peerSpecificSetup(theme, strings, dismissed): case let .peerSpecificSetup(theme, strings, dismissed):
return StickerPanePeerSpecificSetupGridItem(theme: theme, strings: strings, setup: { return StickerPanePeerSpecificSetupGridItem(theme: theme, strings: strings, setup: {

View File

@ -332,7 +332,7 @@ private enum StickerPacksCollectionUpdate {
final class ChatMediaInputNodeInteraction { final class ChatMediaInputNodeInteraction {
let navigateToCollectionId: (ItemCollectionId) -> Void let navigateToCollectionId: (ItemCollectionId) -> Void
let openSettings: () -> Void let openSettings: () -> Void
let toggleSearch: (Bool, ChatMediaInputSearchMode?) -> Void let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void
let openPeerSpecificSettings: () -> Void let openPeerSpecificSettings: () -> Void
let dismissPeerSpecificSettings: () -> Void let dismissPeerSpecificSettings: () -> Void
let clearRecentlyUsedStickers: () -> Void let clearRecentlyUsedStickers: () -> Void
@ -343,7 +343,7 @@ final class ChatMediaInputNodeInteraction {
var previewedStickerPackItem: StickerPreviewPeekItem? var previewedStickerPackItem: StickerPreviewPeekItem?
var appearanceTransition: CGFloat = 1.0 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.navigateToCollectionId = navigateToCollectionId
self.openSettings = openSettings self.openSettings = openSettings
self.toggleSearch = toggleSearch self.toggleSearch = toggleSearch
@ -551,7 +551,7 @@ final class ChatMediaInputNode: ChatInputNode {
controller.navigationPresentation = .modal controller.navigationPresentation = .modal
strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) strongSelf.controllerInteraction.navigationController()?.pushViewController(controller)
} }
}, toggleSearch: { [weak self] value, searchMode in }, toggleSearch: { [weak self] value, searchMode, query in
if let strongSelf = self { if let strongSelf = self {
if let searchMode = searchMode, value { if let searchMode = searchMode, value {
var searchContainerNode: PaneSearchContainerNode? var searchContainerNode: PaneSearchContainerNode?
@ -560,9 +560,14 @@ final class ChatMediaInputNode: ChatInputNode {
} else { } 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: { 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?.searchContainerNode?.deactivate()
self?.inputNodeInteraction.toggleSearch(false, nil) self?.inputNodeInteraction.toggleSearch(false, nil, "")
}) })
strongSelf.searchContainerNode = searchContainerNode strongSelf.searchContainerNode = searchContainerNode
if !query.isEmpty {
DispatchQueue.main.async {
searchContainerNode?.updateQuery(query)
}
}
} }
if let searchContainerNode = searchContainerNode { if let searchContainerNode = searchContainerNode {
strongSelf.searchContainerNodeLoadedDisposable.set((searchContainerNode.ready strongSelf.searchContainerNodeLoadedDisposable.set((searchContainerNode.ready
@ -1407,10 +1412,12 @@ final class ChatMediaInputNode: ChatInputNode {
searchContainerNode.frame = containerFrame searchContainerNode.frame = containerFrame
searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: .immediate) searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: .immediate)
var placeholderNode: PaneSearchBarPlaceholderNode? var placeholderNode: PaneSearchBarPlaceholderNode?
var anchorTop = CGPoint(x: 0.0, y: 0.0)
var anchorTopView: UIView = self.view
if let searchMode = searchMode { if let searchMode = searchMode {
switch searchMode { switch searchMode {
case .gif: case .gif:
placeholderNode = self.gifPane.searchPlaceholderNode placeholderNode = self.gifPane.visibleSearchPlaceholderNode
case .sticker: case .sticker:
self.stickerPane.gridNode.forEachItemNode { itemNode in self.stickerPane.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { if let itemNode = itemNode as? PaneSearchBarPlaceholderNode {
@ -1427,11 +1434,9 @@ final class ChatMediaInputNode: ChatInputNode {
} }
} }
if let placeholderNode = placeholderNode { searchContainerNode.animateIn(from: placeholderNode, anchorTop: anchorTop, anhorTopView: anchorTopView, transition: transition, completion: { [weak self] in
searchContainerNode.animateIn(from: placeholderNode, transition: transition, completion: { [weak self] in self?.gifPane.removeFromSupernode()
self?.gifPane.removeFromSupernode() })
})
}
} }
} }
} }
@ -1589,8 +1594,8 @@ final class ChatMediaInputNode: ChatInputNode {
if let searchMode = searchMode { if let searchMode = searchMode {
switch searchMode { switch searchMode {
case .gif: case .gif:
placeholderNode = self.gifPane.searchPlaceholderNode placeholderNode = self.gifPane.visibleSearchPlaceholderNode
paneIsEmpty = self.gifPane.isEmpty paneIsEmpty = placeholderNode != nil
case .sticker: case .sticker:
paneIsEmpty = true paneIsEmpty = true
self.stickerPane.gridNode.forEachItemNode { itemNode in self.stickerPane.gridNode.forEachItemNode { itemNode in
@ -1612,11 +1617,14 @@ final class ChatMediaInputNode: ChatInputNode {
} }
} }
if let placeholderNode = placeholderNode { if let placeholderNode = placeholderNode {
placeholderNode.isHidden = false
searchContainerNode.animateOut(to: placeholderNode, animateOutSearchBar: !paneIsEmpty, transition: transition, completion: { [weak searchContainerNode] in searchContainerNode.animateOut(to: placeholderNode, animateOutSearchBar: !paneIsEmpty, transition: transition, completion: { [weak searchContainerNode] in
searchContainerNode?.removeFromSupernode() searchContainerNode?.removeFromSupernode()
}) })
} else { } else {
searchContainerNode.removeFromSupernode() transition.updateAlpha(node: searchContainerNode, alpha: 0.0, completion: { [weak searchContainerNode] _ in
searchContainerNode?.removeFromSupernode()
})
} }
} }

View File

@ -338,7 +338,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
} }
}, getItemIsPreviewed: self.getItemIsPreviewed, }, getItemIsPreviewed: self.getItemIsPreviewed,
openSearch: { [weak self] in openSearch: { [weak self] in
self?.inputNodeInteraction?.toggleSearch(true, .trending) self?.inputNodeInteraction?.toggleSearch(true, .trending, "")
}) })
let isPane = self.isPane let isPane = self.isPane

View File

@ -726,7 +726,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
let mediaManager = context.sharedContext.mediaManager let mediaManager = context.sharedContext.mediaManager
let streamVideo = isMediaStreamable(message: message, media: updatedVideoFile) 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) let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.isUserInteractionEnabled = false videoNode.isUserInteractionEnabled = false
videoNode.ownsContentNodeUpdated = { [weak self] owns in videoNode.ownsContentNodeUpdated = { [weak self] owns in

View File

@ -21,13 +21,15 @@ final class ChatPanelInterfaceInteractionStatuses {
let unblockingPeer: Signal<Bool, NoError> let unblockingPeer: Signal<Bool, NoError>
let searching: Signal<Bool, NoError> let searching: Signal<Bool, NoError>
let loadingMessage: 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.editingMessage = editingMessage
self.startingBot = startingBot self.startingBot = startingBot
self.unblockingPeer = unblockingPeer self.unblockingPeer = unblockingPeer
self.searching = searching self.searching = searching
self.loadingMessage = loadingMessage self.loadingMessage = loadingMessage
self.inlineSearch = inlineSearch
} }
} }

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import SwiftSignalKit
import Postbox import Postbox
import TelegramCore import TelegramCore
import SyncCore import SyncCore
@ -11,18 +12,7 @@ import TextFormat
import AccountContext import AccountContext
import TouchDownGesture import TouchDownGesture
import ImageTransparency import ImageTransparency
import ActivityIndicator
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)))
})
private let accessoryButtonFont = Font.medium(14.0) private let accessoryButtonFont = Font.medium(14.0)
@ -217,7 +207,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let attachmentButton: HighlightableButtonNode let attachmentButton: HighlightableButtonNode
let attachmentButtonDisabledNode: HighlightableButtonNode let attachmentButtonDisabledNode: HighlightableButtonNode
let searchLayoutClearButton: HighlightableButton let searchLayoutClearButton: HighlightableButton
let searchLayoutProgressView: UIImageView private let searchLayoutClearImageNode: ASImageNode
private var searchActivityIndicator: ActivityIndicator?
var audioRecordingInfoContainerNode: ASDisplayNode? var audioRecordingInfoContainerNode: ASDisplayNode?
var audioRecordingDotNode: ASImageNode? var audioRecordingDotNode: ASImageNode?
var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode? 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) { func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) {
if state.inputText.length != 0 && self.textInputNode == nil { if state.inputText.length != 0 && self.textInputNode == nil {
self.loadTextInputNode() self.loadTextInputNode()
@ -390,8 +394,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.attachmentButton.isAccessibilityElement = true self.attachmentButton.isAccessibilityElement = true
self.attachmentButtonDisabledNode = HighlightableButtonNode() self.attachmentButtonDisabledNode = HighlightableButtonNode()
self.searchLayoutClearButton = HighlightableButton() self.searchLayoutClearButton = HighlightableButton()
self.searchLayoutProgressView = UIImageView(image: searchLayoutProgressImage) self.searchLayoutClearImageNode = ASImageNode()
self.searchLayoutProgressView.isHidden = true self.searchLayoutClearImageNode.isUserInteractionEnabled = false
self.searchLayoutClearButton.addSubnode(self.searchLayoutClearImageNode)
self.actionButtons = ChatTextInputActionButtonsNode(theme: presentationInterfaceState.theme, strings: presentationInterfaceState.strings, presentController: presentController) 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.addTarget(self, action: #selector(self.searchLayoutClearButtonPressed), for: .touchUpInside)
self.searchLayoutClearButton.alpha = 0.0 self.searchLayoutClearButton.alpha = 0.0
self.searchLayoutClearButton.addSubview(self.searchLayoutProgressView)
self.addSubnode(self.textInputContainer) self.addSubnode(self.textInputContainer)
self.addSubnode(self.textInputBackgroundNode) self.addSubnode(self.textInputBackgroundNode)
@ -495,6 +498,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
self.statusDisposable.dispose()
}
func loadTextInputNodeIfNeeded() { func loadTextInputNodeIfNeeded() {
if self.textInputNode == nil { if self.textInputNode == nil {
self.loadTextInputNode() 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.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 { if let audioRecordingDotNode = self.audioRecordingDotNode {
audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme) audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme)
@ -1102,9 +1109,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight) let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight)
let textFieldInsets = self.textFieldInsets(metrics: metrics) 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)) 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))
if let image = self.searchLayoutClearImageNode.image {
let searchProgressSize = self.searchLayoutProgressView.bounds.size 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)
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)) }
let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top + audioRecordingItemsVerticalOffset, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) 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) 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 { @objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero {
self.sendButtonPressed() self.sendButtonPressed()

View File

@ -210,7 +210,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
} }
private func dequeueTransition() { 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) self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions() var options = ListViewDeleteAndInsertOptions()

View File

@ -274,7 +274,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
}, },
openSettings: { openSettings: {
}, },
toggleSearch: { _, _ in toggleSearch: { _, _, _ in
}, },
openPeerSpecificSettings: { openPeerSpecificSettings: {
}, },

View File

@ -11,7 +11,7 @@ import AccountContext
import WebSearchUI import WebSearchUI
import AppBundle 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 delayRequest = true
let contextBot = account.postbox.transaction { transaction -> String in 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 |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { 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 |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
return { _ in return { _ in
return .contextRequestResult(user, results) return .contextRequestResult(user, results)
@ -66,25 +66,46 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo
} }
} }
return contextBot 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 { if let r = result(nil), case let .contextRequestResult(_, collection) = r, let results = collection?.results {
var references: [FileMediaReference] = [] var references: [FileMediaReference] = []
for result in results { for result in results {
switch result { switch result {
case let .externalReference(externalReference): case let .externalReference(externalReference):
var imageResource: TelegramMediaResource? var imageResource: TelegramMediaResource?
var thumbnailResource: TelegramMediaResource?
var thumbnailIsVideo: Bool = false
var uniqueId: Int64? var uniqueId: Int64?
if let content = externalReference.content { if let content = externalReference.content {
imageResource = content.resource imageResource = content.resource
if let resource = content.resource as? WebFileReferenceMediaResource { if let resource = content.resource as? WebFileReferenceMediaResource {
uniqueId = Int64(HashFunctions.murMurHash32(resource.url)) 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 { if externalReference.type == "gif", let resource = 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: [])]) 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)) references.append(FileMediaReference.standalone(media: file))
} }
case let .internalReference(internalReference): 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 { } else {
return .complete() return .complete()
} }
@ -119,6 +140,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
private let notFoundNode: ASImageNode private let notFoundNode: ASImageNode
private let notFoundLabel: ImmediateTextNode private let notFoundLabel: ImmediateTextNode
private var nextOffset: (String, String)?
private var isLoadingNextResults: Bool = false
private var validLayout: CGSize? private var validLayout: CGSize?
private let trendingPromise: Promise<[FileMediaReference]?> private let trendingPromise: Promise<[FileMediaReference]?>
@ -131,6 +155,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
var deactivateSearchBar: (() -> Void)? var deactivateSearchBar: (() -> Void)?
var updateActivity: ((Bool) -> 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]?>) { init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<[FileMediaReference]?>) {
self.context = context self.context = context
@ -167,27 +194,82 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
} }
func updateText(_ text: String, languageCode: String?) { 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 { 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) self.updateActivity?(true)
} else { } else {
signal = self.trendingPromise.get() signal = self.trendingPromise.get()
|> map { items -> ([FileMediaReference], String?)? in
if let items = items {
return (items, nil)
} else {
return nil
}
}
self.updateActivity?(false) self.updateActivity?(false)
} }
self.searchDisposable.set((signal self.searchDisposable.set((signal
|> deliverOnMainQueue).start(next: { [weak self] result in |> 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 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.updateActivity?(false)
strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty 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) { func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.notFoundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GifsNotFoundIcon"), color: theme.list.freeMonoIconColor) 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) 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)) 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) 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) self.updateText("", languageCode: nil)
} }
} }
@ -235,7 +317,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
super.willEnterHierarchy() super.willEnterHierarchy()
if self.multiplexedNode == nil { 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 self.multiplexedNode = multiplexedNode
if let layout = self.validLayout { if let layout = self.validLayout {
multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout) multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout)
@ -248,7 +330,19 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
} }
multiplexedNode.didScroll = { [weak self] offset, height in 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)
} }
} }
} }

View File

@ -281,7 +281,10 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont
if let (transition, firstTime) = self.enqueuedTransitions.first { if let (transition, firstTime) = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0) self.enqueuedTransitions.remove(at: 0)
let options = ListViewDeleteAndInsertOptions() var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
options.insert(.LowLatency)
options.insert(.PreferSynchronousResourceLoading)
if firstTime { if firstTime {
//options.insert(.Synchronous) //options.insert(.Synchronous)
//options.insert(.LowLatency) //options.insert(.LowLatency)

View File

@ -40,7 +40,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
Queue.mainQueue().async { Queue.mainQueue().async {
completion(node, { 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) let (layout, apply) = nodeLayout(self, params, top, bottom)
Queue.mainQueue().async { Queue.mainQueue().async {
completion(layout, { _ in 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) let (layout, apply) = doLayout(item, params, merged.top, merged.bottom)
self.contentSize = layout.contentSize self.contentSize = layout.contentSize
self.insets = layout.insets 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 imageLayout = self.imageNode.asyncLayout()
let currentImageResource = self.currentImageResource let currentImageResource = self.currentImageResource
let currentVideoFile = self.currentVideoFile let currentVideoFile = self.currentVideoFile
@ -315,7 +315,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
} else { } else {
let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0)), resource: imageResource) 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: []) 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 { } else {
updateImageSignal = .complete() updateImageSignal = .complete()
@ -324,7 +324,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: height, height: croppedImageDimensions.width + sideInset), insets: UIEdgeInsets()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: height, height: croppedImageDimensions.width + sideInset), insets: UIEdgeInsets())
return (nodeLayout, { _ in return (nodeLayout, { synchronousLoads, _ in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
strongSelf.currentImageResource = imageResource strongSelf.currentImageResource = imageResource
@ -333,7 +333,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
if let imageApply = imageApply { if let imageApply = imageApply {
if let updateImageSignal = updateImageSignal { 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)) 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 { 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) thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
strongSelf.layer.addSublayer(thumbnailLayer) strongSelf.layer.addSublayer(thumbnailLayer)
let layerHolder = takeSampleBufferLayer() let layerHolder = takeSampleBufferLayer()

View File

@ -14,22 +14,22 @@ import TelegramAnimatedStickerNode
final class HorizontalStickerGridItem: GridItem { final class HorizontalStickerGridItem: GridItem {
let account: Account let account: Account
let file: TelegramMediaFile let file: TelegramMediaFile
let stickersInteraction: HorizontalStickersChatContextPanelInteraction let isPreviewed: (HorizontalStickerGridItem) -> Bool
let interfaceInteraction: ChatPanelInterfaceInteraction let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Void
let section: GridSection? = nil 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.account = account
self.file = file self.file = file
self.stickersInteraction = stickersInteraction self.isPreviewed = isPreviewed
self.interfaceInteraction = interfaceInteraction self.sendSticker = sendSticker
} }
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = HorizontalStickerGridItemNode() let node = HorizontalStickerGridItemNode()
node.setup(account: self.account, item: self) node.setup(account: self.account, item: self)
node.interfaceInteraction = self.interfaceInteraction node.sendSticker = self.sendSticker
return node return node
} }
@ -39,7 +39,7 @@ final class HorizontalStickerGridItem: GridItem {
return return
} }
node.setup(account: self.account, item: self) 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() private let stickerFetchedDisposable = MetaDisposable()
var interfaceInteraction: ChatPanelInterfaceInteraction? var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)?
private var currentIsPreviewing: Bool = false private var currentIsPreviewing: Bool = false
@ -108,11 +108,14 @@ final class HorizontalStickerGridItemNode: GridItemNode {
self.addSubnode(animationNode) self.addSubnode(animationNode)
self.animationNode = 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 animationNode.started = { [weak self] in
self?.imageNode.alpha = 0.0 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) 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()) 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) { @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { if let (_, item, _) = self.currentState, case .ended = recognizer.state {
let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), self, self.bounds) self.sendSticker?(.standalone(media: item.file), self, self.bounds)
} }
} }
@ -170,7 +173,7 @@ final class HorizontalStickerGridItemNode: GridItemNode {
func updatePreviewing(animated: Bool) { func updatePreviewing(animated: Bool) {
var isPreviewing = false var isPreviewing = false
if let (_, item, _) = self.currentState { if let (_, item, _) = self.currentState {
isPreviewing = item.stickersInteraction.previewedStickerItem == self.stickerItem //isPreviewing = item.isPreviewed(self.stickerItem)
} }
if self.currentIsPreviewing != isPreviewing { if self.currentIsPreviewing != isPreviewing {
self.currentIsPreviewing = isPreviewing self.currentIsPreviewing = isPreviewing

View File

@ -66,7 +66,11 @@ private struct StickerEntry: Identifiable, Comparable {
} }
func item(account: Account, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> GridItem { 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)
})
} }
} }

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

View File

@ -8,6 +8,7 @@ import TelegramCore
import SyncCore import SyncCore
import AVFoundation import AVFoundation
import ContextUI import ContextUI
import TelegramPresentationData
private final class MultiplexedVideoTrackingNode: ASDisplayNode { private final class MultiplexedVideoTrackingNode: ASDisplayNode {
var inHierarchyUpdated: ((Bool) -> Void)? var inHierarchyUpdated: ((Bool) -> Void)?
@ -26,20 +27,129 @@ private final class MultiplexedVideoTrackingNode: ASDisplayNode {
} }
private final class VisibleVideoItem { private final class VisibleVideoItem {
enum Id: Equatable, Hashable {
case saved(MediaId)
case trending(MediaId)
}
let id: Id
let fileReference: FileMediaReference let fileReference: FileMediaReference
let frame: CGRect let frame: CGRect
init(fileReference: FileMediaReference, frame: CGRect) { init(fileReference: FileMediaReference, frame: CGRect, isTrending: Bool) {
self.fileReference = fileReference self.fileReference = fileReference
self.frame = frame 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 { final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
private let account: Account private let account: Account
private var theme: PresentationTheme
private var strings: PresentationStrings
private let trackingNode: MultiplexedVideoTrackingNode private let trackingNode: MultiplexedVideoTrackingNode
var didScroll: ((CGFloat, CGFloat) -> Void)? var didScroll: ((CGFloat, CGFloat) -> Void)?
var didEndScrolling: (() -> Void)? var didEndScrolling: (() -> Void)?
var reactionSelected: ((String) -> Void)?
var topInset: CGFloat = 0.0 { var topInset: CGFloat = 0.0 {
didSet { didSet {
@ -59,21 +169,24 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
} }
} }
var files: [FileMediaReference] = [] { var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: []) {
didSet { didSet {
let startTime = CFAbsoluteTimeGetCurrent() let startTime = CFAbsoluteTimeGetCurrent()
self.updateVisibleItems() self.updateVisibleItems(extendSizeForTransition: 0.0, transition: .immediate, synchronous: true)
print("MultiplexedVideoNode files updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") print("MultiplexedVideoNode files updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
} }
} }
private var displayItems: [VisibleVideoItem] = [] private var displayItems: [VisibleVideoItem] = []
private var visibleThumbnailLayers: [MediaId: SoftwareVideoThumbnailLayer] = [:] private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailLayer] = [:]
private var statusDisposable: [MediaId : MetaDisposable] = [:] private var statusDisposable: [VisibleVideoItem.Id: MetaDisposable] = [:]
private let contextContainerNode: ContextControllerSourceNode private let contextContainerNode: ContextControllerSourceNode
let scrollNode: ASScrollNode 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 displayLink: CADisplayLink!
private var timeOffset = 0.0 private var timeOffset = 0.0
@ -85,8 +198,10 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
var fileContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void)? var fileContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void)?
var enableVideoNodes = false var enableVideoNodes = false
init(account: Account) { init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
self.account = account self.account = account
self.theme = theme
self.strings = strings
self.trackingNode = MultiplexedVideoTrackingNode() self.trackingNode = MultiplexedVideoTrackingNode()
self.trackingNode.isLayerBacked = true self.trackingNode.isLayerBacked = true
@ -98,13 +213,26 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
self.contextContainerNode = ContextControllerSourceNode() self.contextContainerNode = ContextControllerSourceNode()
self.scrollNode = ASScrollNode() 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() super.init()
self.trendingHeaderNode.reactionSelected = { [weak self] reaction in
self?.reactionSelected?(reaction)
}
self.isOpaque = true self.isOpaque = true
self.scrollNode.view.showsVerticalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.alwaysBounceVertical = true self.scrollNode.view.alwaysBounceVertical = true
self.scrollNode.addSubnode(self.savedTitleNode)
self.scrollNode.addSubnode(self.trendingHeaderNode)
self.addSubnode(self.trackingNode) self.addSubnode(self.trackingNode)
self.addSubnode(self.contextContainerNode) self.addSubnode(self.contextContainerNode)
self.contextContainerNode.addSubnode(self.scrollNode) self.contextContainerNode.addSubnode(self.scrollNode)
@ -216,13 +344,16 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
} }
private var validSize: CGSize? 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) { if self.validSize == nil || !self.validSize!.equalTo(size) {
let previousSize = self.validSize ?? CGSize()
self.validSize = size self.validSize = size
self.contextContainerNode.frame = CGRect(origin: CGPoint(), size: 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() 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") 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 var validVisibleItemsOffset: CGFloat?
private func updateImmediatelyVisibleItems(ensureFrames: Bool = false) { private func updateImmediatelyVisibleItems(ensureFrames: Bool = false, synchronous: Bool = false) {
let visibleBounds = self.scrollNode.bounds var visibleBounds = self.scrollNode.bounds
visibleBounds.size.height += max(0.0, self.currentExtendSizeForTransition)
let visibleThumbnailBounds = visibleBounds.insetBy(dx: 0.0, dy: -350.0) let visibleThumbnailBounds = visibleBounds.insetBy(dx: 0.0, dy: -350.0)
if let validVisibleItemsOffset = self.validVisibleItemsOffset, validVisibleItemsOffset.isEqual(to: visibleBounds.origin.y) { 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 minVisibleThumbnailY = visibleThumbnailBounds.minY
let maxVisibleThumbnailY = visibleThumbnailBounds.maxY let maxVisibleThumbnailY = visibleThumbnailBounds.maxY
var visibleThumbnailIds = Set<MediaId>() var visibleThumbnailIds = Set<VisibleVideoItem.Id>()
var visibleIds = Set<MediaId>() var visibleIds = Set<VisibleVideoItem.Id>()
for item in self.displayItems { for item in self.displayItems {
if item.frame.maxY < minVisibleThumbnailY { if item.frame.maxY < minVisibleThumbnailY {
@ -268,17 +402,17 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
break; 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 { if ensureFrames {
thumbnailLayer.frame = item.frame thumbnailLayer.frame = item.frame
} }
} else { } 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 thumbnailLayer.frame = item.frame
self.scrollNode.layer.addSublayer(thumbnailLayer) 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) let progressSize = CGSize(width: 24.0, height: 24.0)
@ -291,9 +425,9 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
continue 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 { if ensureFrames {
layerHolder.layer.frame = item.frame layerHolder.layer.frame = item.frame
} }
@ -303,23 +437,23 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
layerHolder.layer.frame = item.frame layerHolder.layer.frame = item.frame
self.scrollNode.layer.addSublayer(layerHolder.layer) self.scrollNode.layer.addSublayer(layerHolder.layer)
let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.fileReference, layerHolder: layerHolder) let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.fileReference, layerHolder: layerHolder)
self.visibleLayers[item.fileReference.media.fileId] = (manager, layerHolder) self.visibleLayers[item.id] = (manager, layerHolder)
self.visibleThumbnailLayers[item.fileReference.media.fileId]?.ready = { [weak self] in self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in
if let strongSelf = self { 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 { for id in self.visibleLayers.keys {
if !visibleIds.contains(id) { if !visibleIds.contains(id) {
removeIds.append(id) removeIds.append(id)
} }
} }
var removeThumbnailIds: [MediaId] = [] var removeThumbnailIds: [VisibleVideoItem.Id] = []
for id in self.visibleThumbnailLayers.keys { for id in self.visibleThumbnailLayers.keys {
if !visibleThumbnailIds.contains(id) { if !visibleThumbnailIds.contains(id) {
removeThumbnailIds.append(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 let drawableSize = self.scrollNode.bounds.size
if !drawableSize.width.isZero { if !drawableSize.width.isZero {
var displayItems: [VisibleVideoItem] = [] var displayItems: [VisibleVideoItem] = []
let idealHeight = self.idealHeight let idealHeight = self.idealHeight
var weights: [Int] = [] var verticalOffset: CGFloat = self.topInset
var totalItemSize: CGFloat = 0.0
for item in self.files { func commitFilesSpans(files: [FileMediaReference], isTrending: Bool) {
let aspectRatio: CGFloat var rowsCount = 0
if let dimensions = item.media.dimensions { var firstRowMax = 0;
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
} else { let viewPortAvailableSize = drawableSize.width
aspectRatio = 1.0
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) func commitFiles(files: [FileMediaReference], isTrending: Bool) {
var weights: [Int] = []
let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows) var totalItemSize: CGFloat = 0.0
for item in files {
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 {
let aspectRatio: CGFloat let aspectRatio: CGFloat
if let dimensions = self.files[j].media.dimensions { if let dimensions = item.media.dimensions {
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
} else { } else {
aspectRatio = 1.0 aspectRatio = 1.0
} }
weights.append(Int(aspectRatio * 100))
summedRatios += aspectRatio totalItemSize += aspectRatio * idealHeight
j += 1
} }
var rowSize = drawableSize.width - (CGFloat(row.count - 1) * minimumInteritemSpacing) let numberOfRows = max(Int(round(totalItemSize / drawableSize.width)), 1)
if rowIndex == partition.count - 1 { let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows)
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 = i var i = 0
n = i + row.count var offset = CGPoint(x: 0.0, y: verticalOffset)
var previousItemSize: CGFloat = 0.0
let maxWidth = drawableSize.width
while j < n { let minimumInteritemSpacing: CGFloat = 1.0
let aspectRatio: CGFloat let minimumLineSpacing: CGFloat = 1.0
if let dimensions = self.files[j].media.dimensions {
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height let viewportWidth: CGFloat = drawableSize.width
} else {
aspectRatio = 1.0 let preferredRowSize = idealHeight
}
let preferredAspectRatio = aspectRatio 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) var j = i
if frame.origin.x + frame.size.width >= maxWidth - 2.0 { var n = i + row.count
frame.size.width = max(1.0, maxWidth - frame.origin.x)
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 if rowIndex == partition.count - 1 {
previousItemSize = actualSize.height if row.count < 2 {
contentMaxValueInScrollDirection = frame.maxY 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.scrollNode.view.contentSize = contentSize
self.displayItems = displayItems self.displayItems = displayItems
self.validVisibleItemsOffset = nil 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()
})
} }
} }

View File

@ -435,7 +435,7 @@ class PaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
snapshot.frame = CGRect(origin: self.textField.placeholderLabel.frame.origin, size: node.labelNode.frame.size) snapshot.frame = CGRect(origin: self.textField.placeholderLabel.frame.origin, size: node.labelNode.frame.size)
self.textField.layer.addSublayer(snapshot) self.textField.layer.addSublayer(snapshot)
snapshot.animateAlpha(from: 0.0, to: 1.0, duration: duration * 2.0 / 3.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue) 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) self.textFieldDidChange(self.textField)
} }
} }
func updateQuery(_ query: String) {
self.textField.text = query
self.textFieldDidChange(self.textField)
}
} }

View File

@ -83,6 +83,12 @@ final class PaneSearchContainerNode: ASDisplayNode {
} }
self.updateThemeAndStrings(theme: theme, strings: strings) 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) { 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) 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)? { func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight)) 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) self.searchBar.deactivate(clear: true)
} }
func animateIn(from placeholder: PaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y
let verticalOrigin = placeholderFrame.minY - 4.0 if let placeholder = placeholder {
self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition) 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 { switch transition {
case let .animated(duration, curve): case let .animated(duration, curve):
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0) 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 { 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))) 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) self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction)

View File

@ -56,6 +56,8 @@ private final class VisualMediaItemNode: ASDisplayNode {
private var item: (VisualMediaItem, Media?, CGSize, CGSize?)? private var item: (VisualMediaItem, Media?, CGSize, CGSize?)?
private var theme: PresentationTheme? private var theme: PresentationTheme?
private var hasVisibility: Bool = false
init(context: AccountContext, interaction: VisualMediaItemInteraction) { init(context: AccountContext, interaction: VisualMediaItemInteraction) {
self.context = context self.context = context
self.interaction = interaction self.interaction = interaction
@ -192,7 +194,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
} else { } else {
sampleBufferLayer = takeSampleBufferLayer() sampleBufferLayer = takeSampleBufferLayer()
self.sampleBufferLayer = sampleBufferLayer 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) 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) { func updateIsVisible(_ isVisible: Bool) {
self.hasVisibility = isVisible
if let _ = self.videoLayerFrameManager { if let _ = self.videoLayerFrameManager {
let displayLink: ConstantDisplayLinkAnimator let displayLink: ConstantDisplayLinkAnimator
if let current = self.displayLink { if let current = self.displayLink {
@ -342,8 +345,8 @@ private final class VisualMediaItemNode: ASDisplayNode {
displayLink.frameInterval = 2 displayLink.frameInterval = 2
self.displayLink = displayLink self.displayLink = displayLink
} }
displayLink.isPaused = !isVisible
} }
self.displayLink?.isPaused = !self.hasVisibility || self.isHidden
} }
func updateSelectionState(animated: Bool) { func updateSelectionState(animated: Bool) {
@ -420,6 +423,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
} else { } else {
self.isHidden = false self.isHidden = false
} }
self.displayLink?.isPaused = !self.hasVisibility || self.isHidden
} }
} }

View File

@ -63,7 +63,7 @@ final class SoftwareVideoLayerFrameManager {
func start() { func start() {
let secondarySignal: Signal<String?, NoError> let secondarySignal: Signal<String?, NoError>
if let secondaryResource = self.secondaryResource { 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 |> map { data -> String? in
if data.complete { if data.complete {
return data.path return data.path

View File

@ -23,7 +23,7 @@ final class SoftwareVideoThumbnailLayer: CALayer {
} }
} }
init(account: Account, fileReference: FileMediaReference) { init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool) {
super.init() super.init()
self.backgroundColor = UIColor.clear.cgColor self.backgroundColor = UIColor.clear.cgColor
@ -31,7 +31,7 @@ final class SoftwareVideoThumbnailLayer: CALayer {
self.masksToBounds = true self.masksToBounds = true
if let dimensions = fileReference.media.dimensions { 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)) var boundingSize = dimensions.cgSize.aspectFilled(CGSize(width: 93.0, height: 93.0))
let imageSize = boundingSize let imageSize = boundingSize
boundingSize.width = min(200.0, boundingSize.width) boundingSize.width = min(200.0, boundingSize.width)