GIF fixes

This commit is contained in:
Ali 2020-05-26 18:51:27 +04:00
parent 5ecbcc0f19
commit 83952f399c
21 changed files with 564 additions and 158 deletions

View File

@ -1305,14 +1305,14 @@ public func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaRef
}
}
public func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, overlayColor: overlayColor)
public func mediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
return internalMediaGridMessageVideo(postbox: postbox, videoReference: videoReference, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: autoFetchFullSizeThumbnail, overlayColor: overlayColor, nilForEmptyResult: nilForEmptyResult)
|> map {
return $0.1
}
}
public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> {
public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: FileMediaReference, imageReference: ImageMediaReference? = nil, onlyFullSize: Bool = false, synchronousLoad: Bool = false, autoFetchFullSizeThumbnail: Bool = false, overlayColor: UIColor? = nil, nilForEmptyResult: Bool = false) -> Signal<(() -> CGSize?, (TransformImageArguments) -> DrawingContext?), NoError> {
let signal: Signal<Tuple3<Data?, Tuple2<Data, String>?, Bool>, NoError>
if let imageReference = imageReference {
signal = chatMessagePhotoDatas(postbox: postbox, photoReference: imageReference, tryAdditionalRepresentations: true, synchronousLoad: synchronousLoad)
@ -1346,6 +1346,12 @@ public func internalMediaGridMessageVideo(postbox: Postbox, videoReference: File
}
return nil
}, { arguments in
if nilForEmptyResult {
if thumbnailData == nil && fullSizeData == nil {
return nil
}
}
let context = DrawingContext(size: arguments.drawingSize, clear: true)
let drawingRect = arguments.drawingRect

View File

@ -131,7 +131,7 @@ private final class MediaBoxFileMap {
self.progress = nil
}
fileprivate func contains(_ range: Range<Int32>) -> Bool {
fileprivate func contains(_ range: Range<Int32>) -> Range<Int32>? {
let maxValue: Int
if let truncationSize = self.truncationSize {
maxValue = Int(truncationSize)
@ -139,7 +139,11 @@ private final class MediaBoxFileMap {
maxValue = Int.max
}
let intRange: Range<Int> = Int(range.lowerBound) ..< min(maxValue, Int(range.upperBound))
return self.ranges.contains(integersIn: intRange)
if self.ranges.contains(integersIn: intRange) {
return Int32(intRange.lowerBound) ..< Int32(intRange.upperBound)
} else {
return nil
}
}
}
@ -385,7 +389,7 @@ final class MediaBoxPartialFile {
}
var isCompleted = false
if let truncationSize = self.fileMap.truncationSize, self.fileMap.contains(0 ..< truncationSize) {
if let truncationSize = self.fileMap.truncationSize, let _ = self.fileMap.contains(0 ..< truncationSize) {
isCompleted = true
}
@ -432,9 +436,9 @@ final class MediaBoxPartialFile {
func read(range: Range<Int32>) -> Data? {
assert(self.queue.isCurrent())
if self.fileMap.contains(range) {
self.fd.seek(position: Int64(range.lowerBound))
var data = Data(count: range.count)
if let actualRange = self.fileMap.contains(range) {
self.fd.seek(position: Int64(actualRange.lowerBound))
var data = Data(count: actualRange.count)
let dataCount = data.count
let readBytes = data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<Int8>) -> Int in
return self.fd.read(bytes, dataCount)
@ -452,8 +456,8 @@ final class MediaBoxPartialFile {
func data(range: Range<Int32>, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable {
assert(self.queue.isCurrent())
if self.fileMap.contains(range) {
next(MediaResourceData(path: self.path, offset: Int(range.lowerBound), size: range.count, complete: true))
if let actualRange = self.fileMap.contains(range) {
next(MediaResourceData(path: self.path, offset: Int(actualRange.lowerBound), size: actualRange.count, complete: true))
return EmptyDisposable
}
@ -481,7 +485,7 @@ final class MediaBoxPartialFile {
func fetched(range: Range<Int32>, priority: MediaBoxFetchPriority, fetch: @escaping (Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void) -> Disposable {
assert(self.queue.isCurrent())
if self.fileMap.contains(range) {
if let _ = self.fileMap.contains(range) {
completed()
return EmptyDisposable
}
@ -559,7 +563,7 @@ final class MediaBoxPartialFile {
assert(self.queue.isCurrent())
next(self.fileMap.ranges)
if let truncationSize = self.fileMap.truncationSize, self.fileMap.contains(0 ..< truncationSize) {
if let truncationSize = self.fileMap.truncationSize, let _ = self.fileMap.contains(0 ..< truncationSize) {
completed()
return EmptyDisposable
}
@ -676,8 +680,8 @@ final class MediaBoxPartialFile {
if request.waitingUntilAfterInitialFetch {
request.waitingUntilAfterInitialFetch = false
if strongSelf.fileMap.contains(request.range) {
request.completion(MediaResourceData(path: strongSelf.path, offset: Int(request.range.lowerBound), size: request.range.count, complete: true))
if let actualRange = strongSelf.fileMap.contains(request.range) {
request.completion(MediaResourceData(path: strongSelf.path, offset: Int(actualRange.lowerBound), size: actualRange.count, complete: true))
} else {
request.completion(MediaResourceData(path: strongSelf.path, offset: Int(request.range.lowerBound), size: 0, complete: false))
}

View File

@ -123,6 +123,7 @@ public final class ShimmerEffectNode: ASDisplayNode {
case circle(CGRect)
case roundedRectLine(startPoint: CGPoint, width: CGFloat, diameter: CGFloat)
case roundedRect(rect: CGRect, cornerRadius: CGFloat)
case rect(rect: CGRect)
}
private let backgroundNode: ASDisplayNode
@ -189,6 +190,8 @@ public final class ShimmerEffectNode: ASDisplayNode {
UIGraphicsPushContext(context)
path.fill()
UIGraphicsPopContext()
case let .rect(rect):
context.fill(rect)
}
}
})

View File

@ -334,12 +334,18 @@ public func chatMessageSticker(account: Account, file: TelegramMediaFile, small:
return chatMessageSticker(postbox: account.postbox, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad)
}
public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaResource, animated: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaResource, animated: Bool = false, synchronousLoad: Bool = false, nilIfEmpty: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
let signal = chatMessageStickerPackThumbnailData(postbox: postbox, resource: resource, animated: animated, synchronousLoad: synchronousLoad)
return signal
|> map { fullSizeData in
return { arguments in
if nilIfEmpty {
if fullSizeData == nil {
return nil
}
}
let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil)
let drawingRect = arguments.drawingRect

View File

@ -111,7 +111,7 @@ public func requestChatContextResults(account: Account, botId: PeerId, peerId: P
}
return account.postbox.transaction { transaction -> ChatContextResultCollection? in
if result.cacheTimeout > 10 && offset.isEmpty {
if result.cacheTimeout > 10 {
if let resultData = try? JSONEncoder().encode(result) {
let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query)
if let keyData = try? JSONEncoder().encode(requestData) {

View File

@ -61,7 +61,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
private let imageNodeBackground: ASDisplayNode
private let imageNode: TransformImageNode
private var videoLayer: (SoftwareVideoThumbnailLayer, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
private var currentImageResource: TelegramMediaResource?
private var currentVideoFile: TelegramMediaFile?
@ -248,13 +248,13 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
if updatedVideoFile {
if let (thumbnailLayer, _, layer) = self.videoLayer {
self.videoLayer = nil
thumbnailLayer.removeFromSuperlayer()
thumbnailLayer.removeFromSupernode()
layer.layer.removeFromSuperlayer()
}
if let videoFileReference = videoFileReference {
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: videoFileReference, synchronousLoad: false)
self.layer.addSublayer(thumbnailLayer)
let thumbnailLayer = SoftwareVideoThumbnailNode(account: self.account, fileReference: videoFileReference, synchronousLoad: false)
self.addSubnode(thumbnailLayer)
let layerHolder = takeSampleBufferLayer()
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.layer.addSublayer(layerHolder.layer)

View File

@ -24,6 +24,16 @@ private func fixListScrolling(_ multiplexedNode: MultiplexedVideoNode) {
}
}
final class ChatMediaInputGifPaneTrendingState {
let files: [MultiplexedVideoNodeFile]
let nextOffset: String?
init(files: [MultiplexedVideoNodeFile], nextOffset: String?) {
self.files = files
self.nextOffset = nextOffset
}
}
final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
private let account: Account
private var theme: PresentationTheme
@ -49,7 +59,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
private let emptyNode: ImmediateTextNode
private let disposable = MetaDisposable()
let trendingPromise = Promise<[MultiplexedVideoNodeFile]?>(nil)
let trendingPromise = Promise<ChatMediaInputGifPaneTrendingState?>(nil)
private var validLayout: (CGSize, CGFloat, CGFloat, Bool, Bool, DeviceMetrics)?
private var didScrollPreviousOffset: CGFloat?
@ -57,6 +67,8 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
private var didScrollPreviousState: ChatMediaInputPaneScrollState?
private(set) var mode: ChatMediaInputGifMode = .recent
private var isLoadingMore: Bool = false
private var nextOffset: String?
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void, openGifContextMenu: @escaping (MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void) {
self.account = account
@ -132,7 +144,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
return
}
self.mode = mode
self.resetMode(synchronous: true)
self.resetMode(synchronous: true, searchOffset: nil)
}
override var isEmpty: Bool {
@ -160,7 +172,7 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
let displaySearch: Bool
switch self.mode {
case .recent:
case .recent, .trending:
displaySearch = true
default:
displaySearch = false
@ -194,9 +206,9 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
func initializeIfNeeded() {
if self.multiplexedNode == nil {
self.trendingPromise.set(paneGifSearchForQuery(account: account, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil)
|> map { items -> [MultiplexedVideoNodeFile]? in
if let (items, _) = items {
return items
|> map { items -> ChatMediaInputGifPaneTrendingState? in
if let items = items {
return ChatMediaInputGifPaneTrendingState(files: items.files, nextOffset: items.nextOffset)
} else {
return nil
}
@ -243,6 +255,10 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
let state = ChatMediaInputPaneScrollState(absoluteOffset: absoluteOffset, relativeChange: delta)
strongSelf.didScrollPreviousState = state
strongSelf.paneDidScroll(strongSelf, state, .immediate)
if offset >= height - multiplexedNode.bounds.height - 200.0 {
strongSelf.loadMore()
}
}
multiplexedNode.didEndScrolling = { [weak self] in
@ -253,23 +269,25 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
strongSelf.fixPaneScroll(strongSelf, didScrollPreviousState)
}
if let multiplexedNode = strongSelf.multiplexedNode {
if let _ = strongSelf.multiplexedNode {
//fixListScrolling(multiplexedNode)
}
}
self.updateMultiplexedNodeLayout(changedIsExpanded: false, transition: .immediate)
self.resetMode(synchronous: false)
self.resetMode(synchronous: false, searchOffset: nil)
}
}
private func resetMode(synchronous: Bool) {
let filesSignal: Signal<MultiplexedVideoNodeFiles, NoError>
private func resetMode(synchronous: Bool, searchOffset: String?) {
self.isLoadingMore = true
let filesSignal: Signal<(MultiplexedVideoNodeFiles, String?), NoError>
switch self.mode {
case .recent:
filesSignal = combineLatest(self.trendingPromise.get(), self.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]))
|> map { trending, view -> MultiplexedVideoNodeFiles in
|> map { trending, view -> (MultiplexedVideoNodeFiles, String?) in
var recentGifs: OrderedItemListView?
if let orderedView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] {
recentGifs = orderedView as? OrderedItemListView
@ -286,36 +304,58 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
saved = []
}
return MultiplexedVideoNodeFiles(saved: saved, trending: trending ?? [], isSearch: false)
return (MultiplexedVideoNodeFiles(saved: saved, trending: trending?.files ?? [], isSearch: false, canLoadMore: false), nil)
}
case .trending:
if let searchOffset = searchOffset {
filesSignal = paneGifSearchForQuery(account: self.account, query: "", offset: searchOffset, incompleteResults: true, delayRequest: false, updateActivity: nil)
|> map { result -> (MultiplexedVideoNodeFiles, String?) in
let canLoadMore: Bool
if let result = result {
canLoadMore = !result.isComplete
} else {
canLoadMore = true
}
return (MultiplexedVideoNodeFiles(saved: [], trending: result?.files ?? [], isSearch: true, canLoadMore: canLoadMore), result?.nextOffset)
}
} else {
filesSignal = self.trendingPromise.get()
|> map { trending -> MultiplexedVideoNodeFiles in
return MultiplexedVideoNodeFiles(saved: [], trending: trending ?? [], isSearch: true)
|> map { trending -> (MultiplexedVideoNodeFiles, String?) in
return (MultiplexedVideoNodeFiles(saved: [], trending: trending?.files ?? [], isSearch: true, canLoadMore: false), trending?.nextOffset)
}
}
case let .emojiSearch(emoji):
filesSignal = paneGifSearchForQuery(account: self.account, query: emoji, offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil)
|> map { trending -> MultiplexedVideoNodeFiles in
return MultiplexedVideoNodeFiles(saved: [], trending: trending?.0 ?? [], isSearch: true)
filesSignal = paneGifSearchForQuery(account: self.account, query: emoji, offset: searchOffset, incompleteResults: true, delayRequest: false, updateActivity: nil)
|> map { result -> (MultiplexedVideoNodeFiles, String?) in
let canLoadMore: Bool
if let result = result {
canLoadMore = !result.isComplete
} else {
canLoadMore = true
}
return (MultiplexedVideoNodeFiles(saved: [], trending: result?.files ?? [], isSearch: true, canLoadMore: canLoadMore), result?.nextOffset)
}
}
var firstTime = true
self.disposable.set((filesSignal
|> deliverOnMainQueue).start(next: { [weak self] files in
|> deliverOnMainQueue).start(next: { [weak self] addedFiles, nextOffset in
if let strongSelf = self {
//let previousFiles = strongSelf.multiplexedNode?.files
var resetScrollingToOffset: CGFloat?
if firstTime {
firstTime = false
if searchOffset == nil {
resetScrollingToOffset = 0.0
}
}
strongSelf.isLoadingMore = false
let displaySearch: Bool
switch strongSelf.mode {
case .recent:
case .recent, .trending:
displaySearch = true
default:
displaySearch = false
@ -327,17 +367,41 @@ final class ChatMediaInputGifPane: ChatMediaInputPane, UIScrollViewDelegate {
strongSelf.multiplexedNode?.topInset = topInset + (displaySearch ? 60.0 : 0.0)
}
strongSelf.multiplexedNode?.setFiles(files: files, synchronous: synchronous, resetScrollingToOffset: resetScrollingToOffset)
/*let wasEmpty: Bool
if let previousFiles = previousFiles {
wasEmpty = previousFiles.trending.isEmpty && previousFiles.saved.isEmpty
} else {
wasEmpty = true
var files = addedFiles
if let _ = searchOffset {
var resultFiles: [MultiplexedVideoNodeFile] = []
if let currentFiles = strongSelf.multiplexedNode?.files.trending {
resultFiles = currentFiles
}
let isEmpty = files.trending.isEmpty && files.saved.isEmpty
strongSelf.emptyNode.isHidden = !isEmpty*/
var existingFileIds = Set(resultFiles.map { $0.file.media.fileId })
for file in addedFiles.trending {
if existingFileIds.contains(file.file.media.fileId) {
continue
}
existingFileIds.insert(file.file.media.fileId)
resultFiles.append(file)
}
files = MultiplexedVideoNodeFiles(saved: [], trending: resultFiles, isSearch: true, canLoadMore: addedFiles.canLoadMore)
}
strongSelf.nextOffset = nextOffset
strongSelf.multiplexedNode?.setFiles(files: files, synchronous: synchronous, resetScrollingToOffset: resetScrollingToOffset)
}
}))
}
private func loadMore() {
if self.isLoadingMore {
return
}
guard let nextOffset = self.nextOffset else {
return
}
switch self.mode {
case .trending, .emojiSearch:
self.resetMode(synchronous: false, searchOffset: nextOffset)
default:
break
}
}
}

View File

@ -24,7 +24,7 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
let selectedItem: () -> Void
var selectable: Bool {
return false
return true
}
init(inputNodeInteraction: ChatMediaInputNodeInteraction, type: ChatMediaInputMetaSectionItemType, theme: PresentationTheme, selected: @escaping () -> Void) {
@ -37,14 +37,16 @@ final class ChatMediaInputMetaSectionItem: ListViewItem {
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatMediaInputMetaSectionItemNode()
node.contentSize = CGSize(width: 41.0, height: 41.0)
node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)
Queue.mainQueue().async {
node.inputNodeInteraction = self.inputNodeInteraction
node.setItem(item: self)
node.updateTheme(theme: self.theme)
node.updateIsHighlighted()
node.updateAppearanceTransition(transition: .immediate)
Queue.mainQueue().async {
node.contentSize = CGSize(width: 41.0, height: 41.0)
node.insets = ChatMediaInputNode.setupPanelIconInsets(item: self, previousItem: previousItem, nextItem: nextItem)
completion(node, {
return (nil, { _ in
@ -78,7 +80,6 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
private let textNodeContainer: ASDisplayNode
private let textNode: ImmediateTextNode
private let highlightNode: ASImageNode
private let buttonNode: HighlightTrackingButtonNode
var item: ChatMediaInputMetaSectionItem?
var currentCollectionId: ItemCollectionId?
@ -110,32 +111,22 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
self.textNodeContainer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
self.buttonNode = HighlightTrackingButtonNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.highlightNode)
self.addSubnode(self.imageNode)
self.addSubnode(self.textNodeContainer)
self.addSubnode(self.buttonNode)
let imageSize = CGSize(width: 26.0, height: 26.0)
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize)
self.textNodeContainer.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + UIScreenPixel), size: imageSize)
self.buttonNode.frame = CGRect(origin: CGPoint(), size: boundingSize)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.textNodeContainer.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0) + 1.0), size: imageSize)
}
override func didLoad() {
super.didLoad()
}
@objc private func buttonPressed() {
self.item?.selectedItem()
}
func setItem(item: ChatMediaInputMetaSectionItem) {
self.item = item
switch item.type {
@ -167,7 +158,7 @@ final class ChatMediaInputMetaSectionItemNode: ListViewItemNode {
self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelTrendingGifsIcon(theme)
case let .gifEmoji(emoji):
self.imageNode.image = nil
self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(28.0), textColor: .black)
self.textNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(27.0), textColor: .black)
let textSize = self.textNode.updateLayout(CGSize(width: 100.0, height: 100.0))
self.textNode.frame = CGRect(origin: CGPoint(x: floor((self.textNodeContainer.bounds.width - textSize.width) / 2.0), y: floor((self.textNodeContainer.bounds.height - textSize.height) / 2.0)), size: textSize)
}

View File

@ -501,9 +501,11 @@ final class ChatMediaInputNode: ChatInputNode {
self.listView = ListView()
self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0)
self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = false
self.gifListView = ListView()
self.gifListView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0)
self.gifListView.scroller.panGestureRecognizer.cancelsTouchesInView = false
var paneDidScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void)?
var fixPaneScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void)?

View File

@ -11,6 +11,7 @@ import StickerResources
import AccountContext
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ShimmerEffect
enum ChatMediaInputStickerGridSectionAccessory {
case none
@ -121,6 +122,7 @@ final class ChatMediaInputStickerGridItem: GridItem {
let selected: () -> Void
let interfaceInteraction: ChatControllerInteraction?
let inputNodeInteraction: ChatMediaInputNodeInteraction
let theme: PresentationTheme
let section: GridSection?
@ -130,6 +132,7 @@ final class ChatMediaInputStickerGridItem: GridItem {
self.stickerItem = stickerItem
self.interfaceInteraction = interfaceInteraction
self.inputNodeInteraction = inputNodeInteraction
self.theme = theme
self.selected = selected
if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue {
self.section = nil
@ -170,6 +173,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
private var currentSize: CGSize?
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var placeholderNode: ShimmerEffectNode?
private var didSetUpAnimationNode = false
private var item: ChatMediaInputStickerGridItem?
@ -196,16 +200,45 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
override init() {
self.imageNode = TransformImageNode()
self.placeholderNode = ShimmerEffectNode()
super.init()
self.addSubnode(self.imageNode)
if let placeholderNode = self.placeholderNode {
self.addSubnode(placeholderNode)
}
var firstTime = true
self.imageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil {
strongSelf.removePlaceholder(animated: !firstTime)
}
firstTime = false
}
}
deinit {
self.stickerFetchedDisposable.dispose()
}
private func removePlaceholder(animated: Bool) {
if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
if !animated {
placeholderNode.removeFromSupernode()
} else {
placeholderNode.alpha = 0.0
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in
placeholderNode?.removeFromSupernode()
})
}
}
}
override func didLoad() {
super.didLoad()
@ -216,7 +249,13 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
guard let item = item as? ChatMediaInputStickerGridItem else {
return
}
let sideSize: CGFloat = size.width - 10.0
let boundingSize = CGSize(width: sideSize, height: sideSize)
self.item = item
if self.currentState == nil || self.currentState!.0 !== item.account || self.currentState!.1 != item.stickerItem {
if let dimensions = item.stickerItem.file.dimensions {
if item.stickerItem.file.isAnimatedSticker {
@ -227,8 +266,12 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
animationNode.started = { [weak self] in
self?.imageNode.isHidden = true
}
if let placeholderNode = self.placeholderNode {
self.insertSubnode(animationNode, belowSubnode: placeholderNode)
} else {
self.addSubnode(animationNode)
}
}
let dimensions = item.stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512)
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: item.account.postbox, file: item.stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))))
self.updateVisibility()
@ -253,9 +296,6 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
if self.currentSize != size {
self.currentSize = size
let sideSize: CGFloat = size.width - 10.0 //min(75.0 - 10.0, size.width)
let boundingSize = CGSize(width: sideSize, height: sideSize)
if let (_, _, mediaDimensions) = self.currentState {
let imageSize = mediaDimensions.aspectFitted(boundingSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
@ -266,6 +306,20 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
}
}
}
if let placeholderNode = self.placeholderNode {
let placeholderFrame = CGRect(origin: CGPoint(x: floor((size.width - boundingSize.width) / 2.0), y: floor((size.height - boundingSize.height) / 2.0)), size: boundingSize)
placeholderNode.frame = CGRect(origin: CGPoint(), size: size)
let theme = item.theme
placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.mixedWith(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.9), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), shapes: [.roundedRect(rect: placeholderFrame, cornerRadius: 10.0)], size: bounds.size)
}
}
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
if let placeholderNode = self.placeholderNode {
placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize)
}
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {

View File

@ -11,6 +11,7 @@ import StickerResources
import ItemListStickerPackItem
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ShimmerEffect
final class ChatMediaInputStickerPackItem: ListViewItem {
let account: Account
@ -75,6 +76,8 @@ private let verticalOffset: CGFloat = 3.0
final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
private let imageNode: TransformImageNode
private var animatedStickerNode: AnimatedStickerNode?
private var placeholderNode: ShimmerEffectNode?
private var placeholderImageNode: ASImageNode?
private let highlightNode: ASImageNode
var inputNodeInteraction: ChatMediaInputNodeInteraction?
@ -107,7 +110,12 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
self.imageNode = TransformImageNode()
self.imageNode.isLayerBacked = !smartInvertColorsEnabled()
self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize)
self.placeholderImageNode = ASImageNode()
self.placeholderImageNode?.isUserInteractionEnabled = false
//self.placeholderNode = ShimmerEffectNode()
self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset - UIScreenPixel, y: floor((boundingSize.height - highlightSize.height) / 2.0) - UIScreenPixel), size: highlightSize)
self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
self.imageNode.contentAnimations = [.firstUpdate]
@ -116,12 +124,54 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
self.addSubnode(self.highlightNode)
self.addSubnode(self.imageNode)
if let placeholderImageNode = self.placeholderImageNode {
self.addSubnode(placeholderImageNode)
}
if let placeholderNode = self.placeholderNode {
self.addSubnode(placeholderNode)
}
var firstTime = true
self.imageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil {
strongSelf.removePlaceholder(animated: !firstTime)
}
firstTime = false
}
}
deinit {
self.stickerFetchedDisposable.dispose()
}
private func removePlaceholder(animated: Bool) {
if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
if !animated {
placeholderNode.removeFromSupernode()
} else {
placeholderNode.alpha = 0.0
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in
placeholderNode?.removeFromSupernode()
})
}
}
if let placeholderImageNode = self.placeholderImageNode {
self.placeholderImageNode = nil
if !animated {
placeholderImageNode.removeFromSupernode()
} else {
placeholderImageNode.alpha = 0.0
placeholderImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderImageNode] _ in
placeholderImageNode?.removeFromSupernode()
})
}
}
}
func updateStickerPackItem(account: Account, info: StickerPackCollectionInfo, item: StickerPackItem?, collectionId: ItemCollectionId, theme: PresentationTheme) {
self.currentCollectionId = collectionId
@ -159,13 +209,13 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
let imageSize = representation.dimensions.cgSize.aspectFitted(boundingImageSize)
let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))
imageApply()
self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource))
self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: representation.resource, nilIfEmpty: true))
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize)
case let .animated(resource):
let imageSize = boundingImageSize
let imageApply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))
imageApply()
self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true))
self.imageNode.setSignal(chatMessageStickerPackThumbnail(postbox: account.postbox, resource: resource, animated: true, nilIfEmpty: true))
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize)
let loopAnimatedStickers = self.inputNodeInteraction?.stickerSettings?.loopAnimatedStickers ?? false
@ -178,7 +228,11 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
animatedStickerNode = AnimatedStickerNode()
self.animatedStickerNode = animatedStickerNode
animatedStickerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
if let placeholderNode = self.placeholderNode {
self.insertSubnode(animatedStickerNode, belowSubnode: placeholderNode)
} else {
self.addSubnode(animatedStickerNode)
}
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource), width: 80, height: 80, mode: .cached)
}
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
@ -191,10 +245,35 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
}
}
if let placeholderImageNode = self.placeholderImageNode {
if placeholderImageNode.image == nil {
placeholderImageNode.image = generateStretchableFilledCircleImage(diameter: 10.0, color: theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.mixedWith(.clear, alpha: 0.6))
}
let size = boundingSize
let imageSize = boundingImageSize
let placeholderFrame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize)
placeholderImageNode.frame = placeholderFrame
}
if let placeholderNode = self.placeholderNode {
let size = boundingSize
let imageSize = boundingImageSize
let placeholderFrame = CGRect(origin: CGPoint(x: floor((boundingSize.width - imageSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - imageSize.height) / 2.0)), size: imageSize)
placeholderNode.frame = CGRect(origin: CGPoint(), size: size)
placeholderNode.update(backgroundColor: theme.chat.inputPanel.panelBackgroundColor, foregroundColor: theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor.mixedWith(theme.chat.inputPanel.panelBackgroundColor, alpha: 0.8), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), shapes: [.roundedRect(rect: placeholderFrame, cornerRadius: 5.0)], size: bounds.size)
}
self.updateIsHighlighted()
}
}
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let placeholderNode = self.placeholderNode {
//placeholderNode.updateAbsoluteRect(rect, within: containerSize)
}
}
func updateIsHighlighted() {
assert(Queue.mainQueue().isCurrent())
if let currentCollectionId = self.currentCollectionId, let inputNodeInteraction = self.inputNodeInteraction {

View File

@ -550,6 +550,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
replaceVideoNode = true
} else if currentFile.fileId.namespace == Namespaces.Media.CloudFile && file.fileId.namespace == Namespaces.Media.CloudFile && currentFile.fileId != file.fileId {
replaceVideoNode = true
} else if currentFile.fileId != file.fileId && file.fileId.namespace == Namespaces.Media.CloudSecretFile {
replaceVideoNode = true
}
}
} else if !(file.resource is LocalFileVideoMediaResource) {

View File

@ -1433,7 +1433,7 @@ 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))
let searchActivityIndicator = ActivityIndicator(type: .custom(currentState.theme.list.itemAccentColor, 11.0, 1.0, false))
searchActivityIndicator.isUserInteractionEnabled = false
self.searchActivityIndicator = searchActivityIndicator
let indicatorSize = searchActivityIndicator.measure(CGSize(width: 100.0, height: 100.0))
@ -1441,7 +1441,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
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)
//searchActivityIndicator.layer.sublayerTransform = CATransform3DMakeScale(0.5, 0.5, 1.0)
}
} else if let searchActivityIndicator = self.searchActivityIndicator {
self.searchActivityIndicator = nil

View File

@ -11,7 +11,19 @@ import AccountContext
import WebSearchUI
import AppBundle
func paneGifSearchForQuery(account: Account, query: String, offset: String?, incompleteResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<([MultiplexedVideoNodeFile], String?)?, NoError> {
class PaneGifSearchForQueryResult {
let files: [MultiplexedVideoNodeFile]
let nextOffset: String?
let isComplete: Bool
init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool) {
self.files = files
self.nextOffset = nextOffset
self.isComplete = isComplete
}
}
func paneGifSearchForQuery(account: Account, query: String, offset: String?, incompleteResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<PaneGifSearchForQueryResult?, NoError> {
let contextBot = account.postbox.transaction { transaction -> String in
let configuration = currentSearchBotsConfiguration(transaction: transaction)
return configuration.gifBotUsername ?? "gif"
@ -30,16 +42,14 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, inc
return .single(nil)
}
}
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool), NoError> in
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
let results = requestContextResults(account: account, botId: user.id, query: query, peerId: account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, limit: 1)
|> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
return { _ in
return .contextRequestResult(user, results)
}
|> map { results -> (ChatPresentationInputQueryResult?, Bool) in
return (.contextRequestResult(user, results), results != nil)
}
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool), NoError>
if delayRequest {
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
} else {
@ -48,12 +58,12 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, inc
return maybeDelayedContextResults
} else {
return .single({ _ in return nil })
return .single((nil, true))
}
}
return contextBot
|> mapToSignal { result -> Signal<([MultiplexedVideoNodeFile], String?)?, NoError> in
if let r = result(nil), case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection {
|> mapToSignal { result -> Signal<PaneGifSearchForQueryResult?, NoError> in
if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection {
let results = collection.results
var references: [MultiplexedVideoNodeFile] = []
for result in results {
@ -101,7 +111,7 @@ func paneGifSearchForQuery(account: Account, query: String, offset: String?, inc
}
}
}
return .single((references, collection.nextOffset))
return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1))
} else if incompleteResults {
return .single(nil)
} else {
@ -134,7 +144,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
private var validLayout: CGSize?
private let trendingPromise: Promise<[MultiplexedVideoNodeFile]?>
private let trendingPromise: Promise<ChatMediaInputGifPaneTrendingState?>
private let searchDisposable = MetaDisposable()
private let _ready = Promise<Void>()
@ -149,7 +159,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
private var hasInitialText = false
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<[MultiplexedVideoNodeFile]?>) {
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<ChatMediaInputGifPaneTrendingState?>) {
self.context = context
self.controllerInteraction = controllerInteraction
self.inputNodeInteraction = inputNodeInteraction
@ -190,12 +200,19 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError>
if !text.isEmpty {
signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: "", updateActivity: self.updateActivity)
|> map { result -> ([MultiplexedVideoNodeFile], String?)? in
if let result = result {
return (result.files, result.nextOffset)
} else {
return nil
}
}
self.updateActivity?(true)
} else {
signal = self.trendingPromise.get()
|> map { items -> ([MultiplexedVideoNodeFile], String?)? in
if let items = items {
return (items, nil)
return (items.files, nil)
} else {
return nil
}
@ -215,7 +232,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
} else {
strongSelf.nextOffset = nil
}
strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: result, isSearch: true), synchronous: true, resetScrollingToOffset: nil)
strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: result, isSearch: true, canLoadMore: false), synchronous: true, resetScrollingToOffset: nil)
strongSelf.updateActivity?(false)
strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty
}))
@ -232,6 +249,13 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError>
signal = paneGifSearchForQuery(account: self.context.account, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity)
|> map { result -> ([MultiplexedVideoNodeFile], String?)? in
if let result = result {
return (result.files, result.nextOffset)
} else {
return nil
}
}
self.searchDisposable.set((signal
|> deliverOnMainQueue).start(next: { [weak self] result in
@ -255,7 +279,7 @@ final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
} else {
strongSelf.nextOffset = nil
}
strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: files, isSearch: true), synchronous: true, resetScrollingToOffset: nil)
strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: files, isSearch: true, canLoadMore: false), synchronous: true, resetScrollingToOffset: nil)
strongSelf.notFoundNode.isHidden = text.isEmpty || !files.isEmpty
}))
}

View File

@ -84,7 +84,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
private let imageNodeBackground: ASDisplayNode
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var videoLayer: (SoftwareVideoThumbnailLayer, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)?
private var currentImageResource: TelegramMediaResource?
private var currentVideoFile: TelegramMediaFile?
private var currentAnimatedStickerFile: TelegramMediaFile?
@ -346,14 +346,14 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
if updatedVideoFile {
if let (thumbnailLayer, _, layer) = strongSelf.videoLayer {
strongSelf.videoLayer = nil
thumbnailLayer.removeFromSuperlayer()
thumbnailLayer.removeFromSupernode()
layer.layer.removeFromSuperlayer()
}
if let videoFile = videoFile {
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: item.account, fileReference: .standalone(media: videoFile), synchronousLoad: synchronousLoads)
let thumbnailLayer = SoftwareVideoThumbnailNode(account: item.account, fileReference: .standalone(media: videoFile), synchronousLoad: synchronousLoads)
thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
strongSelf.layer.addSublayer(thumbnailLayer)
strongSelf.addSubnode(thumbnailLayer)
let layerHolder = takeSampleBufferLayer()
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)

View File

@ -9,6 +9,34 @@ import SyncCore
import AVFoundation
import ContextUI
import TelegramPresentationData
import ShimmerEffect
final class MultiplexedVideoPlaceholderNode: ASDisplayNode {
private let effectNode: ShimmerEffectNode
private var theme: PresentationTheme?
private var size: CGSize?
override init() {
self.effectNode = ShimmerEffectNode()
super.init()
self.addSubnode(self.effectNode)
}
func update(size: CGSize, theme: PresentationTheme) {
if self.theme === theme && self.size == size {
return
}
self.effectNode.frame = CGRect(origin: CGPoint(), size: size)
self.effectNode.update(backgroundColor: theme.chat.inputPanel.panelBackgroundColor, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.mixedWith(theme.chat.inputPanel.panelBackgroundColor, alpha: 0.72), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), shapes: [.rect(rect: CGRect(origin: CGPoint(), size: size))], size: bounds.size)
}
func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
self.effectNode.updateAbsoluteRect(absoluteRect, within: containerSize)
}
}
private final class MultiplexedVideoTrackingNode: ASDisplayNode {
var inHierarchyUpdated: ((Bool) -> Void)?
@ -60,11 +88,13 @@ final class MultiplexedVideoNodeFiles {
let saved: [MultiplexedVideoNodeFile]
let trending: [MultiplexedVideoNodeFile]
let isSearch: Bool
let canLoadMore: Bool
init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool) {
init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool, canLoadMore: Bool) {
self.saved = saved
self.trending = trending
self.isSearch = isSearch
self.canLoadMore = canLoadMore
}
}
@ -95,7 +125,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}
}
private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false)
private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false, canLoadMore: false)
func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) {
self.files = files
@ -109,15 +139,14 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}
private var displayItems: [VisibleVideoItem] = []
private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailLayer] = [:]
private var statusDisposable: [VisibleVideoItem.Id: MetaDisposable] = [:]
private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailNode] = [:]
private var visiblePlaceholderNodes: [Int: MultiplexedVideoPlaceholderNode] = [:]
private let contextContainerNode: ContextControllerSourceNode
let scrollNode: ASScrollNode
private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:]
private let savedTitleNode: ImmediateTextNode
private let trendingTitleNode: ImmediateTextNode
private var displayLink: CADisplayLink!
@ -145,9 +174,6 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
self.contextContainerNode = ContextControllerSourceNode()
self.scrollNode = ASScrollNode()
self.savedTitleNode = ImmediateTextNode()
self.savedTitleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_SavedSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
self.trendingTitleNode = ImmediateTextNode()
self.trendingTitleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_TrendingSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
@ -158,7 +184,6 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.alwaysBounceVertical = true
self.scrollNode.addSubnode(self.savedTitleNode)
self.scrollNode.addSubnode(self.trendingTitleNode)
self.addSubnode(self.trackingNode)
@ -255,9 +280,6 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
deinit {
self.displayLink.invalidate()
self.displayLink.isPaused = true
for(_, disposable) in self.statusDisposable {
disposable.dispose()
}
for (_, value) in self.visibleLayers {
value.1.isFreed = true
}
@ -310,9 +332,18 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
private var validVisibleItemsOffset: CGFloat?
private func updateImmediatelyVisibleItems(ensureFrames: Bool = false, synchronous: Bool = false) {
var visibleBounds = self.scrollNode.bounds
let containerSize = visibleBounds.size
visibleBounds.size.height += max(0.0, self.currentExtendSizeForTransition)
let visibleThumbnailBounds = visibleBounds.insetBy(dx: 0.0, dy: -350.0)
let containerWidth = containerSize.width
let itemSpacing: CGFloat = 1.0
let itemsInRow = max(3, min(6, Int(containerWidth / 140.0)))
let itemSize: CGFloat = floor(containerWidth / CGFloat(itemsInRow))
let absoluteContainerSize = CGSize(width: containerSize.width, height: containerSize.height)
let absoluteContainerOffset = -visibleBounds.origin.y
if let validVisibleItemsOffset = self.validVisibleItemsOffset, validVisibleItemsOffset.isEqual(to: visibleBounds.origin.y) {
return
}
@ -326,27 +357,45 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
var visibleThumbnailIds = Set<VisibleVideoItem.Id>()
var visibleIds = Set<VisibleVideoItem.Id>()
for item in self.displayItems {
var maxVisibleIndex = -1
for index in 0 ..< self.displayItems.count {
let item = self.displayItems[index]
if item.frame.maxY < minVisibleThumbnailY {
continue;
continue
}
if item.frame.minY > maxVisibleThumbnailY {
break;
break
}
maxVisibleIndex = max(maxVisibleIndex, index)
visibleThumbnailIds.insert(item.id)
if let thumbnailLayer = self.visibleThumbnailLayers[item.id] {
let thumbnailLayer: SoftwareVideoThumbnailNode
if let current = self.visibleThumbnailLayers[item.id] {
thumbnailLayer = current
if ensureFrames {
thumbnailLayer.frame = item.frame
}
} else {
let thumbnailLayer = SoftwareVideoThumbnailLayer(account: self.account, fileReference: item.file.file, synchronousLoad: synchronous)
var existingPlaceholderNode: MultiplexedVideoPlaceholderNode?
if let placeholderNode = self.visiblePlaceholderNodes[index] {
existingPlaceholderNode = placeholderNode
self.visiblePlaceholderNodes.removeValue(forKey: index)
placeholderNode.removeFromSupernode()
}
thumbnailLayer = SoftwareVideoThumbnailNode(account: self.account, fileReference: item.file.file, synchronousLoad: synchronous, usePlaceholder: true, existingPlaceholder: existingPlaceholderNode)
thumbnailLayer.frame = item.frame
self.scrollNode.layer.addSublayer(thumbnailLayer)
self.scrollNode.addSubnode(thumbnailLayer)
self.visibleThumbnailLayers[item.id] = thumbnailLayer
}
thumbnailLayer.update(theme: self.theme, size: item.frame.size)
thumbnailLayer.updateAbsoluteRect(item.frame.offsetBy(dx: 0.0, dy: absoluteContainerOffset), within: absoluteContainerSize)
if item.frame.maxY < minVisibleY {
continue
}
@ -375,6 +424,43 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}
}
var visiblePlaceholderIndices = Set<Int>()
if self.files.canLoadMore {
let verticalOffset: CGFloat = self.topInset
let sideInset: CGFloat = 0.0
var indexImpl = maxVisibleIndex + 1
while true {
let index = indexImpl
indexImpl += 1
let rowIndex = index / Int(itemsInRow)
let columnIndex = index % Int(itemsInRow)
let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: verticalOffset + itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing))
let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (containerWidth - itemOrigin.x) : itemSize, height: itemSize))
if itemFrame.maxY < minVisibleY {
continue
}
if itemFrame.minY > maxVisibleY {
break
}
visiblePlaceholderIndices.insert(index)
let placeholderNode: MultiplexedVideoPlaceholderNode
if let current = self.visiblePlaceholderNodes[index] {
placeholderNode = current
} else {
placeholderNode = MultiplexedVideoPlaceholderNode()
self.visiblePlaceholderNodes[index] = placeholderNode
self.scrollNode.addSubnode(placeholderNode)
}
placeholderNode.frame = itemFrame
placeholderNode.update(size: itemFrame.size, theme: self.theme)
placeholderNode.updateAbsoluteRect(itemFrame.offsetBy(dx: 0.0, dy: absoluteContainerOffset), within: absoluteContainerSize)
}
}
var removeIds: [VisibleVideoItem.Id] = []
for id in self.visibleLayers.keys {
if !visibleIds.contains(id) {
@ -389,12 +475,12 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
}
}
/*var removeProgressIds: [MediaId] = []
for id in self.visibleProgressNodes.keys {
if !visibleIds.contains(id) {
removeProgressIds.append(id)
var removePlaceholderIndices: [Int] = []
for index in self.visiblePlaceholderNodes.keys {
if !visiblePlaceholderIndices.contains(index) {
removePlaceholderIndices.append(index)
}
}
}*/
for id in removeIds {
let (_, layerHolder) = self.visibleLayers[id]!
@ -404,16 +490,16 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
for id in removeThumbnailIds {
let thumbnailLayer = self.visibleThumbnailLayers[id]!
thumbnailLayer.removeFromSuperlayer()
thumbnailLayer.removeFromSupernode()
self.visibleThumbnailLayers.removeValue(forKey: id)
}
/*for id in removeProgressIds {
let progressNode = self.visibleProgressNodes[id]!
progressNode.removeFromSupernode()
self.visibleProgressNodes.removeValue(forKey: id)
self.statusDisposable.removeValue(forKey: id)?.dispose()
}*/
for index in removePlaceholderIndices {
if let placeholderNode = self.visiblePlaceholderNodes[index] {
placeholderNode.removeFromSupernode()
self.visiblePlaceholderNodes.removeValue(forKey: index)
}
}
}
private func updateVisibleItems(extendSizeForTransition: CGFloat, transition: ContainedViewLayoutTransition, synchronous: Bool = false) {
@ -423,6 +509,29 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
var verticalOffset: CGFloat = self.topInset
func commitFileGrid(files: [MultiplexedVideoNodeFile], isTrending: Bool) {
let containerWidth = drawableSize.width
let itemCount = files.count
let itemSpacing: CGFloat = 1.0
let itemsInRow = max(3, min(6, Int(containerWidth / 140.0)))
let itemSize: CGFloat = floor(containerWidth / CGFloat(itemsInRow))
let rowCount = itemCount / itemsInRow + (itemCount % itemsInRow == 0 ? 0 : 1)
let sideInset: CGFloat = 0.0
for index in 0 ..< itemCount {
let rowIndex = index / Int(itemsInRow)
let columnIndex = index % Int(itemsInRow)
let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: verticalOffset + itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing))
let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (containerWidth - itemOrigin.x) : itemSize, height: itemSize))
displayItems.append(VisibleVideoItem(file: files[index], frame: itemFrame, isTrending: isTrending))
}
let contentHeight = CGFloat(rowCount + 1) * itemSpacing + CGFloat(rowCount) * itemSize
verticalOffset += contentHeight
}
func commitFilesSpans(files: [MultiplexedVideoNodeFile], isTrending: Bool) {
var rowsCount = 0
var firstRowMax = 0;
@ -529,15 +638,8 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
var hasContent = false
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)
commitFileGrid(files: self.files.saved, isTrending: false)
hasContent = true
} else {
self.savedTitleNode.isHidden = true
}
if !self.files.trending.isEmpty {
if self.files.isSearch {
@ -545,14 +647,14 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
} else {
self.trendingTitleNode.isHidden = false
if hasContent {
verticalOffset += 15.0
verticalOffset += 16.0
}
let leftInset: CGFloat = 10.0
let trendingTitleSize = self.trendingTitleNode.updateLayout(CGSize(width: drawableSize.width - leftInset * 2.0, height: 100.0))
self.trendingTitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset - 3.0), size: trendingTitleSize)
verticalOffset += trendingTitleSize.height + 5.0
}
commitFilesSpans(files: self.files.trending, isTrending: true)
commitFileGrid(files: self.files.trending, isTrending: true)
} else {
self.trendingTitleNode.isHidden = true
}

View File

@ -45,7 +45,7 @@ final class PaneSearchContainerNode: ASDisplayNode {
return self.contentNode.ready
}
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<[MultiplexedVideoNodeFile]?>, cancel: @escaping () -> Void) {
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<ChatMediaInputGifPaneTrendingState?>, cancel: @escaping () -> Void) {
self.context = context
self.mode = mode
self.controllerInteraction = controllerInteraction

View File

@ -201,6 +201,10 @@ private final class VisualMediaItemNode: ASDisplayNode {
self.videoLayerFrameManager?.start()
}
} else {
if let sampleBufferLayer = self.sampleBufferLayer {
sampleBufferLayer.layer.removeFromSuperlayer()
self.sampleBufferLayer = nil
}
self.videoLayerFrameManager = nil
}
@ -571,9 +575,9 @@ private enum ItemsLayout {
return (i, j - 1)
}
}
}
return (i, self.frames.count - 1)
}
}
return (0, -1)
}
@ -873,10 +877,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
itemsLayout = current
} else {
switch self.contentType {
case .photoOrVideo:
case .photoOrVideo, .gifs:
itemsLayout = .grid(ItemsLayout.Grid(containerWidth: availableWidth, itemCount: self.mediaItems.count, bottomInset: bottomInset))
case .gifs:
itemsLayout = .balanced(ItemsLayout.Balanced(containerWidth: availableWidth, items: self.mediaItems, bottomInset: bottomInset))
/*case .gifs:
itemsLayout = .balanced(ItemsLayout.Balanced(containerWidth: availableWidth, items: self.mediaItems, bottomInset: bottomInset))*/
}
self.itemsLayout = itemsLayout
}

View File

@ -216,7 +216,7 @@ final class PeerInfoPaneTabsContainerNode: ASDisplayNode {
totalRawTabSize += paneNodeSize.width
}
let minSpacing: CGFloat = 10.0
let minSpacing: CGFloat = 26.0
if tabSizes.count <= 1 {
for i in 0 ..< tabSizes.count {
let (paneNodeSize, paneNode, wasAdded) = tabSizes[i]

View File

@ -54,11 +54,14 @@ func takeSampleBufferLayer() -> SampleBufferLayer {
Queue.mainQueue().async {
layer.flushAndRemoveImage()
layer.setAffineTransform(CGAffineTransform.identity)
#if targetEnvironment(simulator)
#else
let _ = pool.modify { list in
var list = list
list.append(layer)
return list
}
#endif
}
})
}

View File

@ -6,32 +6,55 @@ import Postbox
import SwiftSignalKit
import Display
import PhotoResources
import TelegramPresentationData
import AsyncDisplayKit
private final class SoftwareVideoThumbnailLayerNullAction: NSObject, CAAction {
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
}
}
final class SoftwareVideoThumbnailLayer: CALayer {
final class SoftwareVideoThumbnailNode: ASDisplayNode {
private let usePlaceholder: Bool
private var placeholder: MultiplexedVideoPlaceholderNode?
private var theme: PresentationTheme?
private var asolutePosition: (CGRect, CGSize)?
var disposable = MetaDisposable()
var ready: (() -> Void)? {
didSet {
if self.contents != nil {
if self.layer.contents != nil {
self.ready?()
}
}
}
init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool) {
init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool, usePlaceholder: Bool = false, existingPlaceholder: MultiplexedVideoPlaceholderNode? = nil) {
self.usePlaceholder = usePlaceholder
if usePlaceholder {
self.placeholder = existingPlaceholder
} else {
self.placeholder = nil
}
super.init()
self.backgroundColor = UIColor.clear.cgColor
self.contentsGravity = .resizeAspectFill
self.masksToBounds = true
if !usePlaceholder {
self.isLayerBacked = true
}
if let placeholder = self.placeholder {
self.addSubnode(placeholder)
}
self.backgroundColor = UIColor.clear
self.layer.contentsGravity = .resizeAspectFill
self.layer.masksToBounds = true
if let dimensions = fileReference.media.dimensions {
self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference, synchronousLoad: synchronousLoad)).start(next: { [weak self] transform in
self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, videoReference: fileReference, synchronousLoad: synchronousLoad, nilForEmptyResult: true)
|> deliverOnMainQueue).start(next: { [weak self] transform in
var boundingSize = dimensions.cgSize.aspectFilled(CGSize(width: 93.0, height: 93.0))
let imageSize = boundingSize
boundingSize.width = min(200.0, boundingSize.width)
@ -40,9 +63,31 @@ final class SoftwareVideoThumbnailLayer: CALayer {
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.contents = image.cgImage
if let placeholder = strongSelf.placeholder {
strongSelf.placeholder = placeholder
placeholder.removeFromSupernode()
}
strongSelf.ready?()
}
}
} else {
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if strongSelf.usePlaceholder && strongSelf.placeholder == nil {
let placeholder = MultiplexedVideoPlaceholderNode()
strongSelf.placeholder = placeholder
strongSelf.addSubnode(placeholder)
placeholder.frame = strongSelf.bounds
if let theme = strongSelf.theme {
placeholder.update(size: strongSelf.bounds.size, theme: theme)
}
if let (absoluteRect, containerSize) = strongSelf.asolutePosition {
placeholder.updateAbsoluteRect(absoluteRect, within: containerSize)
}
}
}
}
}))
}
@ -56,7 +101,24 @@ final class SoftwareVideoThumbnailLayer: CALayer {
self.disposable.dispose()
}
override func action(forKey event: String) -> CAAction? {
func update(theme: PresentationTheme, size: CGSize) {
if self.usePlaceholder {
self.theme = theme
}
if let placeholder = self.placeholder {
placeholder.frame = CGRect(origin: CGPoint(), size: size)
placeholder.update(size: size, theme: theme)
}
}
func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
self.asolutePosition = (absoluteRect, containerSize)
if let placeholder = self.placeholder {
placeholder.updateAbsoluteRect(absoluteRect, within: containerSize)
}
}
/*override func action(forKey event: String) -> CAAction? {
return SoftwareVideoThumbnailLayerNullAction()
}
}*/
}