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