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() {
super.didLoad()
let indicatorView = UIActivityIndicatorView(style: .whiteLarge)
let indicatorView: UIActivityIndicatorView
switch self.type {
case let .navigationAccent(color):
indicatorView = UIActivityIndicatorView(style: .whiteLarge)
indicatorView.color = color
case let .custom(color, _, _, forceCustom):
case let .custom(color, diameter, _, forceCustom):
indicatorView = UIActivityIndicatorView(style: diameter < 15.0 ? .white : .whiteLarge)
indicatorView.color = convertIndicatorColor(color)
if !forceCustom {
self.view.addSubview(indicatorView)

View File

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

View File

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

View File

@ -12,17 +12,21 @@ public enum RequestChatContextResultsError {
public final class CachedChatContextResult: PostboxCoding {
public let data: Data
public let timestamp: Int32
public init(data: Data) {
public init(data: Data, timestamp: Int32) {
self.data = data
self.timestamp = timestamp
}
public init(decoder: PostboxDecoder) {
self.data = decoder.decodeDataForKey("data") ?? Data()
self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeData(self.data, forKey: "data")
encoder.encodeInt32(self.timestamp, forKey: "timestamp")
}
}
@ -35,7 +39,7 @@ private struct RequestData: Codable {
let query: String
}
private let requestVersion = "1"
private let requestVersion = "3"
public func requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String) -> Signal<ChatContextResultCollection?, RequestChatContextResultsError> {
return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in
@ -69,7 +73,10 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P
let key = ValueBoxKey(MemoryBuffer(data: keyData))
if let cachedEntry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key)) as? CachedChatContextResult {
if let cachedResult = try? JSONDecoder().decode(ChatContextResultCollection.self, from: cachedEntry.data) {
return .single(cachedResult)
let timestamp = Int32(Date().timeIntervalSince1970)
if cachedEntry.timestamp + cachedResult.cacheTimeout > timestamp {
return .single(cachedResult)
}
}
}
}
@ -102,12 +109,12 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P
}
return account.postbox.transaction { transaction -> ChatContextResultCollection? in
if result.cacheTimeout > 10 {
if result.cacheTimeout > 10 && offset.isEmpty {
if let resultData = try? JSONEncoder().encode(result) {
let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query)
if let keyData = try? JSONEncoder().encode(requestData) {
let key = ValueBoxKey(MemoryBuffer(data: keyData))
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key), entry: CachedChatContextResult(data: resultData), collectionSpec: collectionSpec)
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedContextResults, key: key), entry: CachedChatContextResult(data: resultData, timestamp: Int32(Date().timeIntervalSince1970)), collectionSpec: collectionSpec)
}
}
}

View File

@ -253,7 +253,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
}
if let videoFileReference = videoFileReference {
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: videoFileReference)
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: videoFileReference, synchronousLoad: false)
self.layer.addSublayer(thumbnailLayer)
let layerHolder = takeSampleBufferLayer()
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill

View File

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

View File

@ -1201,7 +1201,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
if transition.isAnimated, let derivedLayoutState = self.derivedLayoutState {
let offset = derivedLayoutState.inputContextPanelsOverMainPanelFrame.maxY - inputContextPanelsOverMainPanelFrame.maxY
transition.animateOffsetAdditive(node: self.inputContextPanelContainer, offset: -offset)
//transition.animateOffsetAdditive(node: self.inputContextPanelContainer, offset: -offset)
}
if let inputContextPanelNode = self.inputContextPanelNode {

View File

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

View File

@ -26,13 +26,25 @@ private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) {
final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
private let account: Account
private var theme: PresentationTheme
private var strings: PresentationStrings
private let controllerInteraction: ChatControllerInteraction
private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void
private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void
private let openGifContextMenu: (FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void
let searchPlaceholderNode: PaneSearchBarPlaceholderNode
private let searchPlaceholderNode: PaneSearchBarPlaceholderNode
var visibleSearchPlaceholderNode: PaneSearchBarPlaceholderNode? {
guard let scrollNode = multiplexedNode?.scrollNode else {
return nil
}
if scrollNode.bounds.contains(self.searchPlaceholderNode.frame) {
return self.searchPlaceholderNode
}
return nil
}
private var multiplexedNode: MultiplexedVideoNode?
private let emptyNode: ImmediateTextNode
@ -46,6 +58,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void, openGifContextMenu: @escaping (FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void) {
self.account = account
self.theme = theme
self.strings = strings
self.controllerInteraction = controllerInteraction
self.paneDidScroll = paneDidScroll
self.fixPaneScroll = fixPaneScroll
@ -64,7 +78,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
self.addSubnode(self.emptyNode)
self.searchPlaceholderNode.activate = { [weak self] in
self?.inputNodeInteraction?.toggleSearch(true, .gif)
self?.inputNodeInteraction?.toggleSearch(true, .gif, "")
}
self.updateThemeAndStrings(theme: theme, strings: strings)
@ -75,6 +89,9 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
}
override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
self.emptyNode.attributedText = NSAttributedString(string: strings.Gif_NoGifsPlaceholder, font: Font.regular(15.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
self.searchPlaceholderNode.setup(theme: theme, strings: strings, type: .gifs)
@ -108,7 +125,11 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
}
override var isEmpty: Bool {
return self.multiplexedNode?.files.isEmpty ?? true
if let files = self.multiplexedNode?.files {
return files.trending.isEmpty && files.saved.isEmpty
} else {
return true
}
}
override func willEnterHierarchy() {
@ -118,7 +139,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
}
private func updateMultiplexedNodeLayout(changedIsExpanded: Bool, transition: ContainedViewLayoutTransition) {
guard let (size, topInset, bottomInset, isExpanded, isVisible, deviceMetrics) = self.validLayout else {
guard let (size, topInset, bottomInset, isExpanded, _, deviceMetrics) = self.validLayout else {
return
}
@ -137,52 +158,79 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
var targetBounds = CGRect(origin: previousBounds.origin, size: nodeFrame.size)
if changedIsExpanded {
targetBounds.origin.y = isExpanded || multiplexedNode.files.isEmpty ? 0.0 : 60.0
let isEmpty = multiplexedNode.files.trending.isEmpty && multiplexedNode.files.saved.isEmpty
//targetBounds.origin.y = isExpanded || isEmpty ? 0.0 : 60.0
}
transition.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: targetBounds)
//transition.updateBounds(layer: multiplexedNode.scrollNode.layer, bounds: targetBounds)
transition.updateFrame(node: multiplexedNode, frame: nodeFrame)
multiplexedNode.updateLayout(size: nodeFrame.size, transition: transition)
multiplexedNode.updateLayout(theme: self.theme, strings: self.strings, size: nodeFrame.size, transition: transition)
self.searchPlaceholderNode.frame = CGRect(x: 0.0, y: 41.0, width: size.width, height: 56.0)
}
}
func initializeIfNeeded() {
if self.multiplexedNode == nil {
self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", updateActivity: nil))
self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, updateActivity: nil)
|> map { items -> [FileMediaReference]? in
if let (items, _) = items {
return items
} else {
return nil
}
})
let multiplexedNode = MultiplexedVideoNode(account: account)
let multiplexedNode = MultiplexedVideoNode(account: self.account, theme: self.theme, strings: self.strings)
self.multiplexedNode = multiplexedNode
if let layout = self.validLayout {
multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout.0)
}
multiplexedNode.reactionSelected = { [weak self] reaction in
guard let strongSelf = self else {
return
}
strongSelf.inputNodeInteraction?.toggleSearch(true, .gif, reaction)
}
self.addSubnode(multiplexedNode)
multiplexedNode.scrollNode.addSubnode(self.searchPlaceholderNode)
let gifs = self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)])
|> map { view -> [FileMediaReference] in
let gifs = combineLatest(self.trendingPromise.get(), self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]))
|> map { trending, view -> MultiplexedVideoNodeFiles in
var recentGifs: OrderedItemListView?
if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] {
recentGifs = orderedView as? OrderedItemListView
}
var saved: [FileMediaReference] = []
if let recentGifs = recentGifs {
return recentGifs.items.map { item in
saved = recentGifs.items.map { item in
let file = (item.contents as! RecentMediaItem).media as! TelegramMediaFile
return .savedGif(media: file)
}
} else {
return []
saved = []
}
return MultiplexedVideoNodeFiles(saved: saved, trending: trending ?? [])
}
self.disposable.set((gifs
|> deliverOnMainQueue).start(next: { [weak self] gifs in
|> deliverOnMainQueue).start(next: { [weak self] files in
if let strongSelf = self {
let previousFiles = strongSelf.multiplexedNode?.files
strongSelf.multiplexedNode?.files = gifs
strongSelf.emptyNode.isHidden = !gifs.isEmpty
if (previousFiles ?? []).isEmpty && !gifs.isEmpty {
strongSelf.multiplexedNode?.files = files
let wasEmpty: Bool
if let previousFiles = previousFiles {
wasEmpty = previousFiles.trending.isEmpty && previousFiles.saved.isEmpty
} else {
wasEmpty = true
}
let isEmpty = files.trending.isEmpty && files.saved.isEmpty
strongSelf.emptyNode.isHidden = !isEmpty
if wasEmpty && isEmpty {
strongSelf.multiplexedNode?.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 60.0)
}
}

View File

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

View File

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

View File

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

View File

@ -726,7 +726,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
let mediaManager = context.sharedContext.mediaManager
let streamVideo = isMediaStreamable(message: message, media: updatedVideoFile)
let videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor)
let loopVideo = updatedVideoFile.isAnimated
let videoContent = NativeVideoContent(id: .message(message.stableId, updatedVideoFile.fileId), fileReference: .message(message: MessageReference(message), media: updatedVideoFile), streamVideo: streamVideo ? .conservative : .none, loopVideo: loopVideo, enableSound: false, fetchAutomatically: false, onlyFullSizeThumbnail: (onlyFullSizeVideoThumbnail ?? false), continuePlayingWithoutSoundOnLostAudioSession: isInlinePlayableVideo, placeholderColor: emptyColor)
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.isUserInteractionEnabled = false
videoNode.ownsContentNodeUpdated = { [weak self] owns in

View File

@ -21,13 +21,15 @@ final class ChatPanelInterfaceInteractionStatuses {
let unblockingPeer: Signal<Bool, NoError>
let searching: Signal<Bool, NoError>
let loadingMessage: Signal<Bool, NoError>
let inlineSearch: Signal<Bool, NoError>
init(editingMessage: Signal<Float?, NoError>, startingBot: Signal<Bool, NoError>, unblockingPeer: Signal<Bool, NoError>, searching: Signal<Bool, NoError>, loadingMessage: Signal<Bool, NoError>) {
init(editingMessage: Signal<Float?, NoError>, startingBot: Signal<Bool, NoError>, unblockingPeer: Signal<Bool, NoError>, searching: Signal<Bool, NoError>, loadingMessage: Signal<Bool, NoError>, inlineSearch: Signal<Bool, NoError>) {
self.editingMessage = editingMessage
self.startingBot = startingBot
self.unblockingPeer = unblockingPeer
self.searching = searching
self.loadingMessage = loadingMessage
self.inlineSearch = inlineSearch
}
}

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
@ -11,18 +12,7 @@ import TextFormat
import AccountContext
import TouchDownGesture
import ImageTransparency
private let searchLayoutProgressImage = generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setStrokeColor(UIColor(rgb: 0x9099A2, alpha: 0.6).cgColor)
let lineWidth: CGFloat = 2.0
let cutoutWidth: CGFloat = 4.0
context.setLineWidth(lineWidth)
context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth)))
context.clear(CGRect(origin: CGPoint(x: (size.width - cutoutWidth) / 2.0, y: 0.0), size: CGSize(width: cutoutWidth, height: size.height / 2.0)))
})
import ActivityIndicator
private let accessoryButtonFont = Font.medium(14.0)
@ -217,7 +207,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let attachmentButton: HighlightableButtonNode
let attachmentButtonDisabledNode: HighlightableButtonNode
let searchLayoutClearButton: HighlightableButton
let searchLayoutProgressView: UIImageView
private let searchLayoutClearImageNode: ASImageNode
private var searchActivityIndicator: ActivityIndicator?
var audioRecordingInfoContainerNode: ASDisplayNode?
var audioRecordingDotNode: ASImageNode?
var audioRecordingTimeNode: ChatTextInputAudioRecordingTimeNode?
@ -281,6 +272,19 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
}
}
private let statusDisposable = MetaDisposable()
override var interfaceInteraction: ChatPanelInterfaceInteraction? {
didSet {
if let statuses = self.interfaceInteraction?.statuses {
self.statusDisposable.set((statuses.inlineSearch
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] value in
self?.updateIsProcessingInlineRequest(value)
}))
}
}
}
func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, extendedSearchLayout: Bool, accessoryItems: [ChatTextInputAccessoryItem], animated: Bool) {
if state.inputText.length != 0 && self.textInputNode == nil {
self.loadTextInputNode()
@ -390,8 +394,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.attachmentButton.isAccessibilityElement = true
self.attachmentButtonDisabledNode = HighlightableButtonNode()
self.searchLayoutClearButton = HighlightableButton()
self.searchLayoutProgressView = UIImageView(image: searchLayoutProgressImage)
self.searchLayoutProgressView.isHidden = true
self.searchLayoutClearImageNode = ASImageNode()
self.searchLayoutClearImageNode.isUserInteractionEnabled = false
self.searchLayoutClearButton.addSubnode(self.searchLayoutClearImageNode)
self.actionButtons = ChatTextInputActionButtonsNode(theme: presentationInterfaceState.theme, strings: presentationInterfaceState.strings, presentController: presentController)
@ -466,8 +471,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.searchLayoutClearButton.addTarget(self, action: #selector(self.searchLayoutClearButtonPressed), for: .touchUpInside)
self.searchLayoutClearButton.alpha = 0.0
self.searchLayoutClearButton.addSubview(self.searchLayoutProgressView)
self.addSubnode(self.textInputContainer)
self.addSubnode(self.textInputBackgroundNode)
@ -495,6 +498,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.statusDisposable.dispose()
}
func loadTextInputNodeIfNeeded() {
if self.textInputNode == nil {
self.loadTextInputNode()
@ -735,7 +742,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.textInputBackgroundNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight)
self.searchLayoutClearButton.setImage(PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme), for: [])
self.searchLayoutClearImageNode.image = PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme)
if let audioRecordingDotNode = self.audioRecordingDotNode {
audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme)
@ -1102,9 +1109,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight)
let textFieldInsets = self.textFieldInsets(metrics: metrics)
transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize))
let searchProgressSize = self.searchLayoutProgressView.bounds.size
transition.updateFrame(layer: self.searchLayoutProgressView.layer, frame: CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - searchProgressSize.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - searchProgressSize.height) / 2.0)), size: searchProgressSize))
if let image = self.searchLayoutClearImageNode.image {
self.searchLayoutClearImageNode.frame = CGRect(origin: CGPoint(x: floor((searchLayoutClearButtonSize.width - image.size.width) / 2.0), y: floor((searchLayoutClearButtonSize.height - image.size.height) / 2.0)), size: image.size)
}
let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top + audioRecordingItemsVerticalOffset, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
transition.updateFrame(node: self.textInputContainer, frame: textInputFrame)
@ -1423,6 +1430,26 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
}
}
func updateIsProcessingInlineRequest(_ value: Bool) {
if value {
if self.searchActivityIndicator == nil, let currentState = self.presentationInterfaceState {
let searchActivityIndicator = ActivityIndicator(type: .custom(currentState.theme.list.itemAccentColor, 22.0, 1.0, false))
searchActivityIndicator.isUserInteractionEnabled = false
self.searchActivityIndicator = searchActivityIndicator
let indicatorSize = searchActivityIndicator.measure(CGSize(width: 100.0, height: 100.0))
let size = self.searchLayoutClearButton.bounds.size
searchActivityIndicator.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height) / 2.0) + 1.0), size: indicatorSize)
self.searchLayoutClearImageNode.isHidden = true
self.searchLayoutClearButton.addSubnode(searchActivityIndicator)
searchActivityIndicator.layer.sublayerTransform = CATransform3DMakeScale(0.5, 0.5, 1.0)
}
} else if let searchActivityIndicator = self.searchActivityIndicator {
self.searchActivityIndicator = nil
self.searchLayoutClearImageNode.isHidden = false
searchActivityIndicator.removeFromSupernode()
}
}
@objc func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero {
self.sendButtonPressed()

View File

@ -210,7 +210,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode {
}
private func dequeueTransition() {
if let validLayout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first {
if let validLayout = self.validLayout, let (transition, _) = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()

View File

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

View File

@ -11,7 +11,7 @@ import AccountContext
import WebSearchUI
import AppBundle
func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bool) -> Void)?) -> Signal<[FileMediaReference]?, NoError> {
func paneGifSearchForQuery(account: Account, query: String, offset: String?, updateActivity: ((Bool) -> Void)?) -> Signal<([FileMediaReference], String?)?, NoError> {
let delayRequest = true
let contextBot = account.postbox.transaction { transaction -> String in
@ -34,7 +34,7 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo
}
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, limit: 15)
let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, offset: offset ?? "", limit: 50)
|> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
return { _ in
return .contextRequestResult(user, results)
@ -66,25 +66,46 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo
}
}
return contextBot
|> mapToSignal { result -> Signal<[FileMediaReference]?, NoError> in
|> mapToSignal { result -> Signal<([FileMediaReference], String?)?, NoError> in
if let r = result(nil), case let .contextRequestResult(_, collection) = r, let results = collection?.results {
var references: [FileMediaReference] = []
for result in results {
switch result {
case let .externalReference(externalReference):
var imageResource: TelegramMediaResource?
var thumbnailResource: TelegramMediaResource?
var thumbnailIsVideo: Bool = false
var uniqueId: Int64?
if let content = externalReference.content {
imageResource = content.resource
if let resource = content.resource as? WebFileReferenceMediaResource {
uniqueId = Int64(HashFunctions.murMurHash32(resource.url))
}
} else if let thumbnail = externalReference.thumbnail {
imageResource = thumbnail.resource
}
if let thumbnail = externalReference.thumbnail {
thumbnailResource = thumbnail.resource
if thumbnail.mimeType.hasPrefix("video/") {
thumbnailIsVideo = true
}
}
if externalReference.type == "gif", let thumbnailResource = imageResource, let content = externalReference.content, let dimensions = content.dimensions {
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions {
var previews: [TelegramMediaImageRepresentation] = []
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
if let thumbnailResource = thumbnailResource {
if thumbnailIsVideo {
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
dimensions: dimensions,
resource: thumbnailResource
))
} else {
previews.append(TelegramMediaImageRepresentation(
dimensions: dimensions,
resource: thumbnailResource
))
}
}
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
references.append(FileMediaReference.standalone(media: file))
}
case let .internalReference(internalReference):
@ -93,7 +114,7 @@ func paneGifSearchForQuery(account: Account, query: String, updateActivity: ((Bo
}
}
}
return .single(references)
return .single((references, collection?.nextOffset))
} else {
return .complete()
}
@ -119,6 +140,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
private let notFoundNode: ASImageNode
private let notFoundLabel: ImmediateTextNode
private var nextOffset: (String, String)?
private var isLoadingNextResults: Bool = false
private var validLayout: CGSize?
private let trendingPromise: Promise<[FileMediaReference]?>
@ -131,6 +155,9 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
var deactivateSearchBar: (() -> Void)?
var updateActivity: ((Bool) -> Void)?
var requestUpdateQuery: ((String) -> Void)?
private var hasInitialText = false
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<[FileMediaReference]?>) {
self.context = context
@ -167,27 +194,82 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
}
func updateText(_ text: String, languageCode: String?) {
let signal: Signal<[FileMediaReference]?, NoError>
self.hasInitialText = true
self.isLoadingNextResults = true
let signal: Signal<([FileMediaReference], String?)?, NoError>
if !text.isEmpty {
signal = paneGifSearchForQuery(account: self.context.account, query: text, updateActivity: self.updateActivity)
signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: "", updateActivity: self.updateActivity)
self.updateActivity?(true)
} else {
signal = self.trendingPromise.get()
|> map { items -> ([FileMediaReference], String?)? in
if let items = items {
return (items, nil)
} else {
return nil
}
}
self.updateActivity?(false)
}
self.searchDisposable.set((signal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self, let result = result else {
guard let strongSelf = self, let (result, nextOffset) = result else {
return
}
strongSelf.multiplexedNode?.files = result
strongSelf.isLoadingNextResults = false
if let nextOffset = nextOffset {
strongSelf.nextOffset = (text, nextOffset)
} else {
strongSelf.nextOffset = nil
}
strongSelf.multiplexedNode?.files = MultiplexedVideoNodeFiles(saved: [], trending: result)
strongSelf.updateActivity?(false)
strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty
}))
}
private func loadMore() {
if self.isLoadingNextResults {
return
}
guard let (text, nextOffsetValue) = self.nextOffset else {
return
}
self.isLoadingNextResults = true
let signal: Signal<([FileMediaReference], String?)?, NoError>
signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity)
self.searchDisposable.set((signal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self, let (result, nextOffset) = result else {
return
}
var files = strongSelf.multiplexedNode?.files.trending ?? []
var currentIds = Set(files.map { $0.media.fileId })
for item in result {
if currentIds.contains(item.media.fileId) {
continue
}
currentIds.insert(item.media.fileId)
files.append(item)
}
strongSelf.isLoadingNextResults = false
if let nextOffset = nextOffset {
strongSelf.nextOffset = (text, nextOffset)
} else {
strongSelf.nextOffset = nil
}
strongSelf.multiplexedNode?.files = MultiplexedVideoNodeFiles(saved: [], trending: files)
strongSelf.notFoundNode.isHidden = text.isEmpty || !files.isEmpty
}))
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.notFoundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GifsNotFoundIcon"), color: theme.list.freeMonoIconColor)
self.notFoundLabel.attributedText = NSAttributedString(string: strings.Gif_NoGifsFound, font: Font.medium(14.0), textColor: theme.list.freeTextColor)
@ -223,10 +305,10 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
transition.updateFrame(layer: multiplexedNode.layer, frame: nodeFrame)
multiplexedNode.updateLayout(size: nodeFrame.size, transition: transition)
multiplexedNode.updateLayout(theme: self.theme, strings: self.strings, size: nodeFrame.size, transition: transition)
}
if firstLayout {
if firstLayout && !self.hasInitialText {
self.updateText("", languageCode: nil)
}
}
@ -235,7 +317,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
super.willEnterHierarchy()
if self.multiplexedNode == nil {
let multiplexedNode = MultiplexedVideoNode(account: self.context.account)
let multiplexedNode = MultiplexedVideoNode(account: self.context.account, theme: self.theme, strings: self.strings)
self.multiplexedNode = multiplexedNode
if let layout = self.validLayout {
multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout)
@ -248,7 +330,19 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
}
multiplexedNode.didScroll = { [weak self] offset, height in
self?.deactivateSearchBar?()
guard let strongSelf = self, let multiplexedNode = strongSelf.multiplexedNode else {
return
}
strongSelf.deactivateSearchBar?()
if offset >= height - multiplexedNode.bounds.height - 200.0 {
strongSelf.loadMore()
}
}
multiplexedNode.reactionSelected = { [weak self] reaction in
self?.requestUpdateQuery?(reaction)
}
}
}

View File

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

View File

@ -40,7 +40,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply(.None) })
return (nil, { _ in apply(synchronousLoads, .None) })
})
}
}
@ -64,7 +64,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem {
let (layout, apply) = nodeLayout(self, params, top, bottom)
Queue.mainQueue().async {
completion(layout, { _ in
apply(animation)
apply(false, animation)
})
}
}
@ -188,11 +188,11 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
let (layout, apply) = doLayout(item, params, merged.top, merged.bottom)
self.contentSize = layout.contentSize
self.insets = layout.insets
apply(.None)
apply(false, .None)
}
}
func asyncLayout() -> (_ item: HorizontalListContextResultsChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
func asyncLayout() -> (_ item: HorizontalListContextResultsChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (Bool, ListViewItemUpdateAnimation) -> Void) {
let imageLayout = self.imageNode.asyncLayout()
let currentImageResource = self.currentImageResource
let currentVideoFile = self.currentVideoFile
@ -315,7 +315,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
} else {
let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0)), resource: imageResource)
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage))
updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage), synchronousLoad: true)
}
} else {
updateImageSignal = .complete()
@ -324,7 +324,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: height, height: croppedImageDimensions.width + sideInset), insets: UIEdgeInsets())
return (nodeLayout, { _ in
return (nodeLayout, { synchronousLoads, _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.currentImageResource = imageResource
@ -333,7 +333,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
if let imageApply = imageApply {
if let updateImageSignal = updateImageSignal {
strongSelf.imageNode.setSignal(updateImageSignal)
strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: true)
}
strongSelf.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height))
@ -351,7 +351,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
}
if let videoFile = videoFile {
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, fileReference: .standalone(media: videoFile))
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, fileReference: .standalone(media: videoFile), synchronousLoad: synchronousLoads)
thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
strongSelf.layer.addSublayer(thumbnailLayer)
let layerHolder = takeSampleBufferLayer()

View File

@ -14,22 +14,22 @@ import TelegramAnimatedStickerNode
final class HorizontalStickerGridItem: GridItem {
let account: Account
let file: TelegramMediaFile
let stickersInteraction: HorizontalStickersChatContextPanelInteraction
let interfaceInteraction: ChatPanelInterfaceInteraction
let isPreviewed: (HorizontalStickerGridItem) -> Bool
let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Void
let section: GridSection? = nil
init(account: Account, file: TelegramMediaFile, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) {
init(account: Account, file: TelegramMediaFile, isPreviewed: @escaping (HorizontalStickerGridItem) -> Bool, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Void) {
self.account = account
self.file = file
self.stickersInteraction = stickersInteraction
self.interfaceInteraction = interfaceInteraction
self.isPreviewed = isPreviewed
self.sendSticker = sendSticker
}
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
let node = HorizontalStickerGridItemNode()
node.setup(account: self.account, item: self)
node.interfaceInteraction = self.interfaceInteraction
node.sendSticker = self.sendSticker
return node
}
@ -39,7 +39,7 @@ final class HorizontalStickerGridItem: GridItem {
return
}
node.setup(account: self.account, item: self)
node.interfaceInteraction = self.interfaceInteraction
node.sendSticker = self.sendSticker
}
}
@ -50,7 +50,7 @@ final class HorizontalStickerGridItemNode: GridItemNode {
private let stickerFetchedDisposable = MetaDisposable()
var interfaceInteraction: ChatPanelInterfaceInteraction?
var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)?
private var currentIsPreviewing: Bool = false
@ -108,11 +108,14 @@ final class HorizontalStickerGridItemNode: GridItemNode {
self.addSubnode(animationNode)
self.animationNode = animationNode
}
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: item.file, small: true, size: fittedDimensions, synchronousLoad: false))
animationNode.started = { [weak self] in
self?.imageNode.alpha = 0.0
}
let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached)
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start())
@ -158,8 +161,8 @@ final class HorizontalStickerGridItemNode: GridItemNode {
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state {
let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), self, self.bounds)
if let (_, item, _) = self.currentState, case .ended = recognizer.state {
self.sendSticker?(.standalone(media: item.file), self, self.bounds)
}
}
@ -170,7 +173,7 @@ final class HorizontalStickerGridItemNode: GridItemNode {
func updatePreviewing(animated: Bool) {
var isPreviewing = false
if let (_, item, _) = self.currentState {
isPreviewing = item.stickersInteraction.previewedStickerItem == self.stickerItem
//isPreviewing = item.isPreviewed(self.stickerItem)
}
if self.currentIsPreviewing != isPreviewing {
self.currentIsPreviewing = isPreviewing

View File

@ -66,7 +66,11 @@ private struct StickerEntry: Identifiable, Comparable {
}
func item(account: Account, stickersInteraction: HorizontalStickersChatContextPanelInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) -> GridItem {
return HorizontalStickerGridItem(account: account, file: self.file, stickersInteraction: stickersInteraction, interfaceInteraction: interfaceInteraction)
return HorizontalStickerGridItem(account: account, file: self.file, isPreviewed: { item in
return false//stickersInteraction.previewedStickerItem == item
}, sendSticker: { file, node, rect in
let _ = interfaceInteraction.sendSticker(file, node, rect)
})
}
}

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 AVFoundation
import ContextUI
import TelegramPresentationData
private final class MultiplexedVideoTrackingNode: ASDisplayNode {
var inHierarchyUpdated: ((Bool) -> Void)?
@ -26,20 +27,129 @@ private final class MultiplexedVideoTrackingNode: ASDisplayNode {
}
private final class VisibleVideoItem {
enum Id: Equatable, Hashable {
case saved(MediaId)
case trending(MediaId)
}
let id: Id
let fileReference: FileMediaReference
let frame: CGRect
init(fileReference: FileMediaReference, frame: CGRect) {
init(fileReference: FileMediaReference, frame: CGRect, isTrending: Bool) {
self.fileReference = fileReference
self.frame = frame
if isTrending {
self.id = .trending(fileReference.media.fileId)
} else {
self.id = .saved(fileReference.media.fileId)
}
}
}
final class MultiplexedVideoNodeFiles {
let saved: [FileMediaReference]
let trending: [FileMediaReference]
init(saved: [FileMediaReference], trending: [FileMediaReference]) {
self.saved = saved
self.trending = trending
}
}
private final class TrendingHeaderNode: ASDisplayNode {
private let titleNode: ImmediateTextNode
private let reactions: [String]
private let reactionNodes: [ImmediateTextNode]
private let scrollNode: ASScrollNode
var reactionSelected: ((String) -> Void)?
override init() {
self.titleNode = ImmediateTextNode()
self.reactions = [
"👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"
]
self.scrollNode = ASScrollNode()
let scrollNode = self.scrollNode
self.reactionNodes = reactions.map { reaction -> ImmediateTextNode in
let textNode = ImmediateTextNode()
textNode.attributedText = NSAttributedString(string: reaction, font: Font.regular(30.0), textColor: .black)
scrollNode.addSubnode(textNode)
return textNode
}
super.init()
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.canCancelContentTouches = true
if #available(iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.addSubnode(self.titleNode)
self.addSubnode(self.scrollNode)
for i in 0 ..< self.reactionNodes.count {
self.reactionNodes[i].view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let location = recognizer.location(in: self.scrollNode.view)
for i in 0 ..< self.reactionNodes.count {
if self.reactionNodes[i].frame.contains(location) {
let reaction = self.reactions[i]
self.reactionSelected?(reaction)
break
}
}
}
}
func update(theme: PresentationTheme, strings: PresentationStrings, width: CGFloat, sideInset: CGFloat) -> CGFloat {
let height: CGFloat = 72.0
let leftInset: CGFloat = 10.0
//TODO:localize
self.titleNode.attributedText = NSAttributedString(string: "TRENDING GIFS", font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - leftInset * 2.0 - sideInset * 2.0, height: 100.0))
self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: titleSize)
let reactionSizes = self.reactionNodes.map { reactionNode -> CGSize in
return reactionNode.updateLayout(CGSize(width: 100.0, height: 100.0))
}
let reactionSpacing: CGFloat = 4.0
var reactionsOffset: CGFloat = leftInset - 2.0
for i in 0 ..< self.reactionNodes.count {
if i != 0 {
reactionsOffset += reactionSpacing
}
reactionNodes[i].frame = CGRect(origin: CGPoint(x: reactionsOffset, y: 0.0), size: reactionSizes[i])
reactionsOffset += reactionSizes[i].width
}
reactionsOffset += leftInset - 2.0
self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 28.0), size: CGSize(width: width, height: 44.0))
self.scrollNode.view.contentSize = CGSize(width: reactionsOffset, height: 44.0)
return height
}
}
final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
private let account: Account
private var theme: PresentationTheme
private var strings: PresentationStrings
private let trackingNode: MultiplexedVideoTrackingNode
var didScroll: ((CGFloat, CGFloat) -> Void)?
var didEndScrolling: (() -> Void)?
var reactionSelected: ((String) -> Void)?
var topInset: CGFloat = 0.0 {
didSet {
@ -59,21 +169,24 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}
}
var files: [FileMediaReference] = [] {
var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: []) {
didSet {
let startTime = CFAbsoluteTimeGetCurrent()
self.updateVisibleItems()
self.updateVisibleItems(extendSizeForTransition: 0.0, transition: .immediate, synchronous: true)
print("MultiplexedVideoNode files updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
}
}
private var displayItems: [VisibleVideoItem] = []
private var visibleThumbnailLayers: [MediaId: SoftwareVideoThumbnailLayer] = [:]
private var statusDisposable: [MediaId : MetaDisposable] = [:]
private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailLayer] = [:]
private var statusDisposable: [VisibleVideoItem.Id: MetaDisposable] = [:]
private let contextContainerNode: ContextControllerSourceNode
let scrollNode: ASScrollNode
private var visibleLayers: [MediaId: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:]
private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:]
private let savedTitleNode: ImmediateTextNode
private let trendingHeaderNode: TrendingHeaderNode
private var displayLink: CADisplayLink!
private var timeOffset = 0.0
@ -85,8 +198,10 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
var fileContextMenu: ((FileMediaReference, ASDisplayNode, CGRect, ContextGesture) -> Void)?
var enableVideoNodes = false
init(account: Account) {
init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
self.account = account
self.theme = theme
self.strings = strings
self.trackingNode = MultiplexedVideoTrackingNode()
self.trackingNode.isLayerBacked = true
@ -98,13 +213,26 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
self.contextContainerNode = ContextControllerSourceNode()
self.scrollNode = ASScrollNode()
//TODO:localization
self.savedTitleNode = ImmediateTextNode()
self.savedTitleNode.attributedText = NSAttributedString(string: "MY GIFS", font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
self.trendingHeaderNode = TrendingHeaderNode()
super.init()
self.trendingHeaderNode.reactionSelected = { [weak self] reaction in
self?.reactionSelected?(reaction)
}
self.isOpaque = true
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.alwaysBounceVertical = true
self.scrollNode.addSubnode(self.savedTitleNode)
self.scrollNode.addSubnode(self.trendingHeaderNode)
self.addSubnode(self.trackingNode)
self.addSubnode(self.contextContainerNode)
self.contextContainerNode.addSubnode(self.scrollNode)
@ -216,13 +344,16 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}
private var validSize: CGSize?
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
func updateLayout(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ContainedViewLayoutTransition) {
self.theme = theme
self.strings = strings
if self.validSize == nil || !self.validSize!.equalTo(size) {
let previousSize = self.validSize ?? CGSize()
self.validSize = size
self.contextContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
let startTime = CFAbsoluteTimeGetCurrent()
self.updateVisibleItems(transition: transition)
self.updateVisibleItems(extendSizeForTransition: max(0.0, previousSize.height - size.height), transition: transition)
print("MultiplexedVideoNode layout updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
}
}
@ -242,9 +373,12 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}
}
private var currentExtendSizeForTransition: CGFloat = 0.0
private var validVisibleItemsOffset: CGFloat?
private func updateImmediatelyVisibleItems(ensureFrames: Bool = false) {
let visibleBounds = self.scrollNode.bounds
private func updateImmediatelyVisibleItems(ensureFrames: Bool = false, synchronous: Bool = false) {
var visibleBounds = self.scrollNode.bounds
visibleBounds.size.height += max(0.0, self.currentExtendSizeForTransition)
let visibleThumbnailBounds = visibleBounds.insetBy(dx: 0.0, dy: -350.0)
if let validVisibleItemsOffset = self.validVisibleItemsOffset, validVisibleItemsOffset.isEqual(to: visibleBounds.origin.y) {
@ -257,8 +391,8 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
let minVisibleThumbnailY = visibleThumbnailBounds.minY
let maxVisibleThumbnailY = visibleThumbnailBounds.maxY
var visibleThumbnailIds = Set<MediaId>()
var visibleIds = Set<MediaId>()
var visibleThumbnailIds = Set<VisibleVideoItem.Id>()
var visibleIds = Set<VisibleVideoItem.Id>()
for item in self.displayItems {
if item.frame.maxY < minVisibleThumbnailY {
@ -268,17 +402,17 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
break;
}
visibleThumbnailIds.insert(item.fileReference.media.fileId)
visibleThumbnailIds.insert(item.id)
if let thumbnailLayer = self.visibleThumbnailLayers[item.fileReference.media.fileId] {
if let thumbnailLayer = self.visibleThumbnailLayers[item.id] {
if ensureFrames {
thumbnailLayer.frame = item.frame
}
} else {
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.fileReference)
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.fileReference, synchronousLoad: synchronous)
thumbnailLayer.frame = item.frame
self.scrollNode.layer.addSublayer(thumbnailLayer)
self.visibleThumbnailLayers[item.fileReference.media.fileId] = thumbnailLayer
self.visibleThumbnailLayers[item.id] = thumbnailLayer
}
let progressSize = CGSize(width: 24.0, height: 24.0)
@ -291,9 +425,9 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
continue
}
visibleIds.insert(item.fileReference.media.fileId)
visibleIds.insert(item.id)
if let (_, layerHolder) = self.visibleLayers[item.fileReference.media.fileId] {
if let (_, layerHolder) = self.visibleLayers[item.id] {
if ensureFrames {
layerHolder.layer.frame = item.frame
}
@ -303,23 +437,23 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
layerHolder.layer.frame = item.frame
self.scrollNode.layer.addSublayer(layerHolder.layer)
let manager = SoftwareVideoLayerFrameManager(account: self.account, fileReference: item.fileReference, layerHolder: layerHolder)
self.visibleLayers[item.fileReference.media.fileId] = (manager, layerHolder)
self.visibleThumbnailLayers[item.fileReference.media.fileId]?.ready = { [weak self] in
self.visibleLayers[item.id] = (manager, layerHolder)
self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in
if let strongSelf = self {
strongSelf.visibleLayers[item.fileReference.media.fileId]?.0.start()
strongSelf.visibleLayers[item.id]?.0.start()
}
}
}
}
var removeIds: [MediaId] = []
var removeIds: [VisibleVideoItem.Id] = []
for id in self.visibleLayers.keys {
if !visibleIds.contains(id) {
removeIds.append(id)
}
}
var removeThumbnailIds: [MediaId] = []
var removeThumbnailIds: [VisibleVideoItem.Id] = []
for id in self.visibleThumbnailLayers.keys {
if !visibleThumbnailIds.contains(id) {
removeThumbnailIds.append(id)
@ -353,116 +487,255 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}*/
}
private func updateVisibleItems(transition: ContainedViewLayoutTransition = .immediate) {
private func updateVisibleItems(extendSizeForTransition: CGFloat, transition: ContainedViewLayoutTransition, synchronous: Bool = false) {
let drawableSize = self.scrollNode.bounds.size
if !drawableSize.width.isZero {
var displayItems: [VisibleVideoItem] = []
let idealHeight = self.idealHeight
var weights: [Int] = []
var totalItemSize: CGFloat = 0.0
for item in self.files {
let aspectRatio: CGFloat
if let dimensions = item.media.dimensions {
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
} else {
aspectRatio = 1.0
var verticalOffset: CGFloat = self.topInset
func commitFilesSpans(files: [FileMediaReference], isTrending: Bool) {
var rowsCount = 0
var firstRowMax = 0;
let viewPortAvailableSize = drawableSize.width
let preferredRowSize: CGFloat = 100.0
let itemsCount = files.count
let spanCount: CGFloat = 100.0
var spanLeft = spanCount
var currentItemsInRow = 0
var currentItemsSpanAmount: CGFloat = 0.0
var itemSpans: [Int: CGFloat] = [:]
var itemsToRow: [Int: Int] = [:]
for a in 0 ..< itemsCount {
var size: CGSize
if let dimensions = files[a].media.dimensions {
size = dimensions.cgSize
} else {
size = CGSize(width: 100.0, height: 100.0)
}
if size.width <= 0.0 {
size.width = 100.0
}
if size.height <= 0.0 {
size.height = 100.0
}
//size = CGSize(width: 100.0, height: 100.0)
let aspect: CGFloat = size.width / size.height
if aspect > 4.0 || aspect < 0.2 {
size.width = max(size.width, size.height)
size.height = size.width
}
var requiredSpan = min(spanCount, floor(spanCount * (size.width / size.height * preferredRowSize / viewPortAvailableSize)))
let moveToNewRow = spanLeft < requiredSpan || requiredSpan > 33.0 && spanLeft < requiredSpan - 15.0
if moveToNewRow {
if spanLeft > 0 {
let spanPerItem = floor(spanLeft / CGFloat(currentItemsInRow))
let start = a - currentItemsInRow
var b = start
while b < start + currentItemsInRow {
if (b == start + currentItemsInRow - 1) {
itemSpans[b] = itemSpans[b]! + spanLeft
} else {
itemSpans[b] = itemSpans[b]! + spanPerItem
}
spanLeft -= spanPerItem;
b += 1
}
itemsToRow[a - 1] = rowsCount
}
rowsCount += 1
currentItemsSpanAmount = 0
currentItemsInRow = 0
spanLeft = spanCount
} else {
if spanLeft < requiredSpan {
requiredSpan = spanLeft
}
}
if rowsCount == 0 {
firstRowMax = max(firstRowMax, a)
}
if a == itemsCount - 1 {
itemsToRow[a] = rowsCount
}
currentItemsSpanAmount += requiredSpan
currentItemsInRow += 1
spanLeft -= requiredSpan
spanLeft = max(0, spanLeft)
itemSpans[a] = requiredSpan
}
if itemsCount != 0 {
rowsCount += 1
}
var currentRowHorizontalOffset: CGFloat = 0.0
for index in 0 ..< files.count {
guard let width = itemSpans[index] else {
continue
}
let itemWidth = floor(width * drawableSize.width / 100.0) - 1
var itemSize = CGSize(width: itemWidth, height: preferredRowSize)
if itemsToRow[index] != nil {
itemSize.width = max(itemSize.width, drawableSize.width - currentRowHorizontalOffset)
}
displayItems.append(VisibleVideoItem(fileReference: files[index], frame: CGRect(origin: CGPoint(x: currentRowHorizontalOffset, y: verticalOffset), size: itemSize), isTrending: isTrending))
currentRowHorizontalOffset += itemSize.width + 1.0
if itemsToRow[index] != nil {
verticalOffset += preferredRowSize + 1.0
currentRowHorizontalOffset = 0.0
}
}
weights.append(Int(aspectRatio * 100))
totalItemSize += aspectRatio * idealHeight
}
let numberOfRows = max(Int(round(totalItemSize / drawableSize.width)), 1)
let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows)
var i = 0
var offset = CGPoint(x: 0.0, y: self.topInset)
var previousItemSize: CGFloat = 0.0
var contentMaxValueInScrollDirection: CGFloat = self.topInset
let maxWidth = drawableSize.width
let minimumInteritemSpacing: CGFloat = 1.0
let minimumLineSpacing: CGFloat = 1.0
let viewportWidth: CGFloat = drawableSize.width
let preferredRowSize = idealHeight
var rowIndex = -1
for row in partition {
rowIndex += 1
var summedRatios: CGFloat = 0.0
var j = i
var n = i + row.count
while j < n {
func commitFiles(files: [FileMediaReference], isTrending: Bool) {
var weights: [Int] = []
var totalItemSize: CGFloat = 0.0
for item in files {
let aspectRatio: CGFloat
if let dimensions = self.files[j].media.dimensions {
if let dimensions = item.media.dimensions {
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
} else {
aspectRatio = 1.0
}
summedRatios += aspectRatio
j += 1
weights.append(Int(aspectRatio * 100))
totalItemSize += aspectRatio * idealHeight
}
var rowSize = drawableSize.width - (CGFloat(row.count - 1) * minimumInteritemSpacing)
let numberOfRows = max(Int(round(totalItemSize / drawableSize.width)), 1)
if rowIndex == partition.count - 1 {
if row.count < 2 {
rowSize = floor(viewportWidth / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
} else if row.count < 3 {
rowSize = floor(viewportWidth * 2.0 / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
}
}
let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows)
j = i
n = i + row.count
var i = 0
var offset = CGPoint(x: 0.0, y: verticalOffset)
var previousItemSize: CGFloat = 0.0
let maxWidth = drawableSize.width
while j < n {
let aspectRatio: CGFloat
if let dimensions = self.files[j].media.dimensions {
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
} else {
aspectRatio = 1.0
}
let preferredAspectRatio = aspectRatio
let minimumInteritemSpacing: CGFloat = 1.0
let minimumLineSpacing: CGFloat = 1.0
let viewportWidth: CGFloat = drawableSize.width
let preferredRowSize = idealHeight
var rowIndex = -1
for row in partition {
rowIndex += 1
let actualSize = CGSize(width: round(rowSize / summedRatios * (preferredAspectRatio)), height: preferredRowSize)
var summedRatios: CGFloat = 0.0
var frame = CGRect(x: offset.x, y: offset.y, width: actualSize.width, height: actualSize.height)
if frame.origin.x + frame.size.width >= maxWidth - 2.0 {
frame.size.width = max(1.0, maxWidth - frame.origin.x)
var j = i
var n = i + row.count
while j < n {
let aspectRatio: CGFloat
if let dimensions = files[j].media.dimensions {
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
} else {
aspectRatio = 1.0
}
summedRatios += aspectRatio
j += 1
}
displayItems.append(VisibleVideoItem(fileReference: self.files[j], frame: frame))
var rowSize = drawableSize.width - (CGFloat(row.count - 1) * minimumInteritemSpacing)
offset.x += actualSize.width + minimumInteritemSpacing
previousItemSize = actualSize.height
contentMaxValueInScrollDirection = frame.maxY
if rowIndex == partition.count - 1 {
if row.count < 2 {
rowSize = floor(viewportWidth / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
} else if row.count < 3 {
rowSize = floor(viewportWidth * 2.0 / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
}
}
j += 1
j = i
n = i + row.count
while j < n {
let aspectRatio: CGFloat
if let dimensions = files[j].media.dimensions {
aspectRatio = dimensions.cgSize.width / dimensions.cgSize.height
} else {
aspectRatio = 1.0
}
let preferredAspectRatio = aspectRatio
let actualSize = CGSize(width: round(rowSize / summedRatios * (preferredAspectRatio)), height: preferredRowSize)
var frame = CGRect(x: offset.x, y: offset.y, width: actualSize.width, height: actualSize.height)
if frame.origin.x + frame.size.width >= maxWidth - 2.0 {
frame.size.width = max(1.0, maxWidth - frame.origin.x)
}
displayItems.append(VisibleVideoItem(fileReference: files[j], frame: frame, isTrending: isTrending))
offset.x += actualSize.width + minimumInteritemSpacing
previousItemSize = actualSize.height
verticalOffset = frame.maxY
j += 1
}
if row.count > 0 {
offset = CGPoint(x: 0.0, y: offset.y + previousItemSize + minimumLineSpacing)
}
i += row.count
}
if row.count > 0 {
offset = CGPoint(x: 0.0, y: offset.y + previousItemSize + minimumLineSpacing)
}
i += row.count
}
let contentSize = CGSize(width: drawableSize.width, height: contentMaxValueInScrollDirection + self.bottomInset)
if !self.files.saved.isEmpty {
self.savedTitleNode.isHidden = false
let leftInset: CGFloat = 10.0
let savedTitleSize = self.savedTitleNode.updateLayout(CGSize(width: drawableSize.width - leftInset * 2.0, height: 100.0))
self.savedTitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset - 3.0), size: savedTitleSize)
verticalOffset += savedTitleSize.height + 5.0
commitFilesSpans(files: self.files.saved, isTrending: false)
//commitFiles(files: self.files.saved, isTrending: false)
} else {
self.savedTitleNode.isHidden = true
}
if !self.files.trending.isEmpty {
self.trendingHeaderNode.isHidden = false
let trendingHeight = self.trendingHeaderNode.update(theme: self.theme, strings: self.strings, width: drawableSize.width, sideInset: 0.0)
self.trendingHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: drawableSize.width, height: trendingHeight))
verticalOffset += trendingHeight
commitFilesSpans(files: self.files.trending, isTrending: true)
//commitFiles(files: self.files.trending, isTrending: true)
} else {
self.trendingHeaderNode.isHidden = true
}
let contentSize = CGSize(width: drawableSize.width, height: verticalOffset + self.bottomInset)
self.scrollNode.view.contentSize = contentSize
self.displayItems = displayItems
self.validVisibleItemsOffset = nil
self.updateImmediatelyVisibleItems(ensureFrames: true)
self.currentExtendSizeForTransition = extendSizeForTransition
self.updateImmediatelyVisibleItems(ensureFrames: true, synchronous: synchronous)
transition.updateAlpha(node: scrollNode, alpha: 1.0, force: true, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.currentExtendSizeForTransition = 0.0
strongSelf.updateImmediatelyVisibleItems()
})
}
}

View File

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

View File

@ -83,6 +83,12 @@ final class PaneSearchContainerNode: ASDisplayNode {
}
self.updateThemeAndStrings(theme: theme, strings: strings)
if let contentNode = self.contentNode as? GifPaneSearchContentNode {
contentNode.requestUpdateQuery = { [weak self] query in
self?.updateQuery(query)
}
}
}
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
@ -100,6 +106,10 @@ final class PaneSearchContainerNode: ASDisplayNode {
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
}
func updateQuery(_ query: String) {
self.searchBar.updateQuery(query)
}
func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight))
}
@ -121,15 +131,25 @@ final class PaneSearchContainerNode: ASDisplayNode {
self.searchBar.deactivate(clear: true)
}
func animateIn(from placeholder: PaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
let verticalOrigin = placeholderFrame.minY - 4.0
self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition)
func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y
if let placeholder = placeholder {
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
verticalOrigin = placeholderFrame.minY - 4.0
self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition)
} else {
self.contentNode.animateIn(additivePosition: 0.0, transition: transition)
}
switch transition {
case let .animated(duration, curve):
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0)
self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction, completion: completion)
if let placeholder = placeholder {
self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction, completion: completion)
} else {
self.searchBar.alpha = 0.0
transition.updateAlpha(node: self.searchBar, alpha: 1.0)
}
if let size = self.validLayout {
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin)))
self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction)

View File

@ -56,6 +56,8 @@ private final class VisualMediaItemNode: ASDisplayNode {
private var item: (VisualMediaItem, Media?, CGSize, CGSize?)?
private var theme: PresentationTheme?
private var hasVisibility: Bool = false
init(context: AccountContext, interaction: VisualMediaItemInteraction) {
self.context = context
self.interaction = interaction
@ -192,7 +194,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
} else {
sampleBufferLayer = takeSampleBufferLayer()
self.sampleBufferLayer = sampleBufferLayer
self.containerNode.layer.insertSublayer(sampleBufferLayer.layer, above: self.imageNode.layer)
self.imageNode.layer.addSublayer(sampleBufferLayer.layer)
}
self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer)
@ -327,6 +329,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
}
func updateIsVisible(_ isVisible: Bool) {
self.hasVisibility = isVisible
if let _ = self.videoLayerFrameManager {
let displayLink: ConstantDisplayLinkAnimator
if let current = self.displayLink {
@ -342,8 +345,8 @@ private final class VisualMediaItemNode: ASDisplayNode {
displayLink.frameInterval = 2
self.displayLink = displayLink
}
displayLink.isPaused = !isVisible
}
self.displayLink?.isPaused = !self.hasVisibility || self.isHidden
}
func updateSelectionState(animated: Bool) {
@ -420,6 +423,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
} else {
self.isHidden = false
}
self.displayLink?.isPaused = !self.hasVisibility || self.isHidden
}
}

View File

@ -63,7 +63,7 @@ final class SoftwareVideoLayerFrameManager {
func start() {
let secondarySignal: Signal<String?, NoError>
if let secondaryResource = self.secondaryResource {
secondarySignal = self.account.postbox.mediaBox.resourceData(self.resource, option: .complete(waitUntilFetchStatus: false))
secondarySignal = self.account.postbox.mediaBox.resourceData(secondaryResource, option: .complete(waitUntilFetchStatus: false))
|> map { data -> String? in
if data.complete {
return data.path

View File

@ -23,7 +23,7 @@ final class SoftwareVideoThumbnailLayer: CALayer {
}
}
init(account: Account, fileReference: FileMediaReference) {
init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool) {
super.init()
self.backgroundColor = UIColor.clear.cgColor
@ -31,7 +31,7 @@ final class SoftwareVideoThumbnailLayer: CALayer {
self.masksToBounds = true
if let dimensions = fileReference.media.dimensions {
self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference)).start(next: { [weak self] transform in
self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference, synchronousLoad: synchronousLoad)).start(next: { [weak self] transform in
var boundingSize = dimensions.cgSize.aspectFilled(CGSize(width: 93.0, height: 93.0))
let imageSize = boundingSize
boundingSize.width = min(200.0, boundingSize.width)