diff --git a/submodules/AvatarVideoNode/BUILD b/submodules/AvatarVideoNode/BUILD new file mode 100644 index 0000000000..1464b865b3 --- /dev/null +++ b/submodules/AvatarVideoNode/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AvatarVideoNode", + module_name = "AvatarVideoNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/AnimationUI:AnimationUI", + "//submodules/AppBundle:AppBundle", + "//submodules/AccountContext:AccountContext", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/GradientBackground:GradientBackground", + "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift new file mode 100644 index 0000000000..e76786ed9e --- /dev/null +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -0,0 +1,395 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import UniversalMediaPlayer +import TelegramUniversalVideoContent +import Postbox +import TelegramCore +import AccountContext +import ComponentFlow +import GradientBackground +import AnimationCache +import MultiAnimationRenderer +import EntityKeyboard + +private let maxVideoLoopCount = 3 + +public final class AvatarVideoNode: ASDisplayNode { + private let context: AccountContext + + private var backgroundNode: ASImageNode + + private var emojiMarkup: TelegramMediaImage.EmojiMarkup? + + private var fileDisposable: Disposable? + private var animationFile: TelegramMediaFile? + private var itemLayer: EmojiPagerContentComponent.View.ItemLayer? + + private var videoNode: UniversalVideoNode? + private var videoContent: NativeVideoContent? + private let playbackStartDisposable = MetaDisposable() + private var videoLoopCount = 0 + + private var size = CGSize(width: 60.0, height: 60.0) + + public init(context: AccountContext) { + self.context = context + + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + + super.init() + + self.clipsToBounds = true + + self.addSubnode(self.backgroundNode) + } + + deinit { + self.fileDisposable?.dispose() + self.playbackStartDisposable.dispose() + } + + public override func didLoad() { + super.didLoad() + + if #available(iOS 13.0, *) { + self.layer.cornerCurve = .circular + } + } + + private func setupAnimation() { + guard let animationFile = self.animationFile else { + return + } + + let itemNativeFitSize = CGSize(width: 128.0, height: 128.0) + + let size = CGSize(width: self.size.width * 0.67, height: self.size.height * 0.67) + let itemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.size.width - size.width) / 2.0), y: floorToScreenPixels((self.size.height - size.height) / 2.0)), size: size) + + let animationData = EntityKeyboardAnimationData(file: animationFile) + let itemLayer = EmojiPagerContentComponent.View.ItemLayer( + item: EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: animationFile, + subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ), + context: context, + attemptSynchronousLoad: false, + content: .animation(animationData), + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: .clear, + blurredBadgeColor: .clear, + accentIconColor: .white, + pointSize: itemNativeFitSize, + onUpdateDisplayPlaceholder: { displayPlaceholder, _ in + if !displayPlaceholder { + print() + } +// guard let strongSelf = self else { +// return +// } +// if displayPlaceholder { +// if let itemLayer = strongSelf.visibleItemLayers[itemId] { +// let placeholderView: EmojiPagerContentComponent.View.ItemPlaceholderView +// if let current = strongSelf.visibleItemPlaceholderViews[itemId] { +// placeholderView = current +// } else { +// placeholderView = EmojiPagerContentComponent.View.ItemPlaceholderView( +// context: context, +// dimensions: item.file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), +// immediateThumbnailData: item.file.immediateThumbnailData, +// shimmerView: nil,//strongSelf.shimmerHostView, +// color: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08), +// size: itemNativeFitSize +// ) +// strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView +// strongSelf.view.insertSubview(placeholderView, at: 0) +// } +// placeholderView.frame = itemLayer.frame +// placeholderView.update(size: placeholderView.bounds.size) +// +// strongSelf.updateShimmerIfNeeded() +// } +// } else { +// if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] { +// strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId) +// +// if duration > 0.0 { +// placeholderView.layer.opacity = 0.0 +// placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak self, weak placeholderView] _ in +// guard let strongSelf = self else { +// return +// } +// placeholderView?.removeFromSuperview() +// strongSelf.updateShimmerIfNeeded() +// }) +// } else { +// placeholderView.removeFromSuperview() +// strongSelf.updateShimmerIfNeeded() +// } +// } +// } + } + ) + itemLayer.onContentsUpdate = { [weak self] in + self?.backgroundNode.isHidden = false + } + itemLayer.layerTintColor = UIColor.white.cgColor + itemLayer.frame = itemFrame + itemLayer.isVisibleForAnimations = true + self.itemLayer = itemLayer + self.backgroundNode.layer.addSublayer(itemLayer) + } + + public func update(peer: EnginePeer, photo: TelegramMediaImage, size: CGSize) { + self.size = size + if let markup = photo.emojiMarkup { + if markup != self.emojiMarkup { + self.emojiMarkup = markup + + let colors = markup.backgroundColors.map { UInt32(bitPattern: $0) } + let backgroundImage: UIImage + if colors.count == 1 { + backgroundImage = generateSingleColorImage(size: size, color: UIColor(rgb: colors.first!))! + } else if colors.count == 2 { + backgroundImage = generateGradientImage(size: size, colors: colors.map { UIColor(rgb: $0) }, locations: [0.0, 1.0])! + } else { + backgroundImage = GradientBackgroundNode.generatePreview(size: size, colors: colors.map { UIColor(rgb: $0) }) + } + self.backgroundNode.image = backgroundImage + self.backgroundNode.isHidden = true + + switch markup.content { + case let .emoji(fileId): + self.fileDisposable = (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]) + |> deliverOnMainQueue).start(next: { [weak self] files in + if let strongSelf = self, let file = files.values.first { + strongSelf.animationFile = file + strongSelf.setupAnimation() + } + }) + case let .sticker(packReference, fileId): + self.fileDisposable = (self.context.engine.stickers.loadedStickerPack(reference: packReference, forceActualized: false) + |> map { pack -> TelegramMediaFile? in + if case let .result(_, items, _) = pack, let item = items.first(where: { $0.file.fileId.id == fileId }) { + return item.file + } + return nil + } + |> deliverOnMainQueue).start(next: { [weak self] file in + if let strongSelf = self, let file { + strongSelf.animationFile = file + strongSelf.setupAnimation() + } + }) + } + } + } else if let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { + self.backgroundNode.image = nil + + let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil) + if videoContent.id != self.videoContent?.id { + self.videoNode?.removeFromSupernode() + self.videoContent = videoContent + } + } + } + + public func updateVisibility(_ isVisible: Bool) { + if isVisible, let videoContent = self.videoContent, self.videoLoopCount != maxVideoLoopCount { + if self.videoNode == nil { + let context = self.context + let mediaManager = context.sharedContext.mediaManager + let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded) + videoNode.clipsToBounds = true + videoNode.isUserInteractionEnabled = false + videoNode.isHidden = true + videoNode.playbackCompleted = { [weak self] in + if let strongSelf = self { + strongSelf.videoLoopCount += 1 + if strongSelf.videoLoopCount == maxVideoLoopCount { + if let videoNode = strongSelf.videoNode { + strongSelf.videoNode = nil + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in + videoNode?.removeFromSupernode() + }) + } + } + } + } + + if let _ = videoContent.startTimestamp { + self.playbackStartDisposable.set((videoNode.status + |> map { status -> Bool in + if let status = status, case .playing = status.status { + return true + } else { + return false + } + } + |> filter { playing in + return playing + } + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + if let strongSelf = self { + Queue.mainQueue().after(0.15) { + strongSelf.videoNode?.isHidden = false + } + } + })) + } else { + self.playbackStartDisposable.set(nil) + videoNode.isHidden = false + } + videoNode.canAttachContent = true + videoNode.play() + + self.addSubnode(videoNode) + self.videoNode = videoNode + } + } else if let videoNode = self.videoNode { + self.videoNode = nil + videoNode.removeFromSupernode() + } + } + + public func updateLayout(size: CGSize, cornerRadius: CGFloat, transition: ContainedViewLayoutTransition) { + self.layer.cornerRadius = cornerRadius + + self.backgroundNode.frame = CGRect(origin: .zero, size: size) + + if let videoNode = self.videoNode { + videoNode.frame = CGRect(origin: .zero, size: size) + videoNode.updateLayout(size: size, transition: transition) + } + } + + public func resetPlayback() { + self.videoLoopCount = 0 + } +} + +private final class VideoDecoration: UniversalVideoDecoration { + public let backgroundNode: ASDisplayNode? = nil + public let contentContainerNode: ASDisplayNode + public let foregroundNode: ASDisplayNode? = nil + + private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? + + private var validLayoutSize: CGSize? + + public init() { + self.contentContainerNode = ASDisplayNode() + } + + public func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) { + if self.contentNode !== contentNode { + let previous = self.contentNode + self.contentNode = contentNode + + if let previous = previous { + if previous.supernode === self.contentContainerNode { + previous.removeFromSupernode() + } + } + + if let contentNode = contentNode { + if contentNode.supernode !== self.contentContainerNode { + self.contentContainerNode.addSubnode(contentNode) + if let validLayoutSize = self.validLayoutSize { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) + contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + } + } + } + } + } + + public func updateCorners(_ corners: ImageCorners) { + self.contentContainerNode.clipsToBounds = true + if isRoundEqualCorners(corners) { + self.contentContainerNode.cornerRadius = corners.topLeft.radius + } else { + let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius)) + let size: CGSize = CGSize(width: boundingSize.width + corners.extendedEdges.left + corners.extendedEdges.right, height: boundingSize.height + corners.extendedEdges.top + corners.extendedEdges.bottom) + let arguments = TransformImageArguments(corners: corners, imageSize: size, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) + guard let context = DrawingContext(size: size, clear: true) else { + return + } + context.withContext { ctx in + ctx.setFillColor(UIColor.black.cgColor) + ctx.fill(arguments.drawingRect) + } + addCorners(context, arguments: arguments) + + if let maskImage = context.generateImage() { + let mask = CALayer() + mask.contents = maskImage.cgImage + mask.contentsScale = maskImage.scale + mask.contentsCenter = CGRect(x: max(corners.topLeft.radius, corners.bottomLeft.radius) / maskImage.size.width, y: max(corners.topLeft.radius, corners.topRight.radius) / maskImage.size.height, width: (maskImage.size.width - max(corners.topLeft.radius, corners.bottomLeft.radius) - max(corners.topRight.radius, corners.bottomRight.radius)) / maskImage.size.width, height: (maskImage.size.height - max(corners.topLeft.radius, corners.topRight.radius) - max(corners.bottomLeft.radius, corners.bottomRight.radius)) / maskImage.size.height) + + self.contentContainerNode.layer.mask = mask + self.contentContainerNode.layer.mask?.frame = self.contentContainerNode.bounds + } + } + } + + public func updateClippingFrame(_ frame: CGRect, completion: (() -> Void)?) { + self.contentContainerNode.layer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + }) + + if let maskLayer = self.contentContainerNode.layer.mask { + maskLayer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + }) + + maskLayer.animate(from: NSValue(cgPoint: maskLayer.position), to: NSValue(cgPoint: frame.center), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + }) + } + + if let contentNode = self.contentNode { + contentNode.layer.animate(from: NSValue(cgPoint: contentNode.layer.position), to: NSValue(cgPoint: frame.center), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + completion?() + }) + } + } + + public func updateContentNodeSnapshot(_ snapshot: UIView?) { + } + + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayoutSize = size + + let bounds = CGRect(origin: CGPoint(), size: size) + if let backgroundNode = self.backgroundNode { + transition.updateFrame(node: backgroundNode, frame: bounds) + } + if let foregroundNode = self.foregroundNode { + transition.updateFrame(node: foregroundNode, frame: bounds) + } + transition.updateFrame(node: self.contentContainerNode, frame: bounds) + if let maskLayer = self.contentContainerNode.layer.mask { + transition.updateFrame(layer: maskLayer, frame: bounds) + } + if let contentNode = self.contentNode { + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) + contentNode.updateLayout(size: size, transition: transition) + } + } + + public func setStatus(_ status: Signal) { + } + + public func tap() { + } +} diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 89931e38b5..7552d56592 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -86,6 +86,8 @@ swift_library( "//submodules/PeerInfoUI", "//submodules/TelegramUI/Components/ChatListHeaderComponent", "//submodules/TelegramUI/Components/ChatListTitleView", + "//submodules/AvatarNode:AvatarNode", + "//submodules/AvatarVideoNode:AvatarVideoNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 4839ce2461..05d12017c4 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -20,13 +20,12 @@ import ContextUI import ChatInterfaceState import TextFormat import InvisibleInkDustNode -import TelegramUniversalVideoContent -import UniversalMediaPlayer import GalleryUI import HierarchyTrackingLayer import TextNodeWithEntities import ComponentFlow import EmojiStatusComponent +import AvatarVideoNode public enum ChatListItemContent { public struct ThreadInfo: Equatable { @@ -612,8 +611,6 @@ private final class ChatListMediaPreviewNode: ASDisplayNode { } } -private let maxVideoLoopCount = 3 - class ChatListItemNode: ItemListRevealOptionsItemNode { final class TopicItemNode: ASDisplayNode { let topicTitleNode: TextNode @@ -882,10 +879,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let avatarNode: AvatarNode var avatarIconView: ComponentHostView? var avatarIconComponent: EmojiStatusComponent? - var videoNode: UniversalVideoNode? - private var videoContent: NativeVideoContent? - private let playbackStartDisposable = MetaDisposable() - private var videoLoopCount = 0 + var avatarVideoNode: AvatarVideoNode? private var inlineNavigationMarkLayer: SimpleLayer? @@ -1084,7 +1078,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { didSet { if self.visibilityStatus != oldValue { if self.visibilityStatus { - self.videoLoopCount = 0 + self.avatarVideoNode?.resetPlayback() } self.updateVideoVisibility() @@ -1116,7 +1110,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if self.trackingIsInHierarchy != oldValue { Queue.mainQueue().justDispatch { if self.trackingIsInHierarchy { - self.videoLoopCount = 0 + self.avatarVideoNode?.resetPlayback() } self.updateVideoVisibility() } @@ -1249,7 +1243,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { deinit { self.cachedDataDisposable.dispose() - self.playbackStartDisposable.dispose() } override func secondaryAction(at point: CGPoint) { @@ -1300,7 +1293,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } self.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: isForum ? .roundedRect : .round, synchronousLoad: synchronousLoads, displayDimensions: CGSize(width: 60.0, height: 60.0)) - if peer.isPremium && peer.id != item.context.account.peerId { + if (peer.isPremium || "".isEmpty) && peer.id != item.context.account.peerId { let context = item.context self.cachedDataDisposable.set((context.account.postbox.peerView(id: peer.id) |> deliverOnMainQueue).start(next: { [weak self] peerView in @@ -1325,14 +1318,16 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if isKnown { let photo = personalPhoto ?? profilePhoto - if let photo = photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { - let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil) - if videoContent.id != strongSelf.videoContent?.id { - strongSelf.videoNode?.removeFromSupernode() - strongSelf.videoContent = videoContent + if let photo = photo, !photo.videoRepresentations.isEmpty || photo.emojiMarkup != nil { + let videoNode: AvatarVideoNode + if let current = strongSelf.avatarVideoNode { + videoNode = current + } else { + videoNode = AvatarVideoNode(context: item.context) + strongSelf.avatarNode.addSubnode(videoNode) + strongSelf.avatarVideoNode = videoNode } + videoNode.update(peer: peer, photo: photo, size: CGSize(width: 60.0, height: 60.0)) if strongSelf.hierarchyTrackingLayer == nil { let hierarchyTrackingLayer = HierarchyTrackingLayer() @@ -1353,12 +1348,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.layer.addSublayer(hierarchyTrackingLayer) } } else { - strongSelf.videoContent = nil - + if let avatarVideoNode = strongSelf.avatarVideoNode { + avatarVideoNode.removeFromSupernode() + strongSelf.avatarVideoNode = nil + } strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer() strongSelf.hierarchyTrackingLayer = nil - } - + } strongSelf.updateVideoVisibility() } else { let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() @@ -1366,10 +1362,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { })) } else { self.cachedDataDisposable.set(nil) - self.videoContent = nil - self.videoNode?.removeFromSupernode() - self.videoNode = nil + self.avatarVideoNode?.removeFromSupernode() + self.avatarVideoNode = nil self.hierarchyTrackingLayer?.removeFromSuperlayer() self.hierarchyTrackingLayer = nil @@ -3435,76 +3430,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } private func updateVideoVisibility() { - guard let item = self.item else { - return - } - let isVisible = self.visibilityStatus && self.trackingIsInHierarchy - if isVisible, let videoContent = self.videoContent, self.videoLoopCount != maxVideoLoopCount { - if self.videoNode == nil { - let context = item.context - let mediaManager = context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) - videoNode.clipsToBounds = true - videoNode.isUserInteractionEnabled = false - videoNode.isHidden = true - videoNode.playbackCompleted = { [weak self] in - if let strongSelf = self { - strongSelf.videoLoopCount += 1 - if strongSelf.videoLoopCount == maxVideoLoopCount { - if let videoNode = strongSelf.videoNode { - strongSelf.videoNode = nil - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in - videoNode?.removeFromSupernode() - }) - } - } - } - } - - if let _ = videoContent.startTimestamp { - self.playbackStartDisposable.set((videoNode.status - |> map { status -> Bool in - if let status = status, case .playing = status.status { - return true - } else { - return false - } - } - |> filter { playing in - return playing - } - |> take(1) - |> deliverOnMainQueue).start(completed: { [weak self] in - if let strongSelf = self { - Queue.mainQueue().after(0.15) { - strongSelf.videoNode?.isHidden = false - } - } - })) - } else { - self.playbackStartDisposable.set(nil) - videoNode.isHidden = false - } - videoNode.layer.cornerRadius = self.avatarNode.bounds.size.width / 2.0 - if #available(iOS 13.0, *) { - videoNode.layer.cornerCurve = .circular - } - - videoNode.canAttachContent = true - videoNode.play() - -// self.mainContentContainerNode.insertSubnode(videoNode, aboveSubnode: self.avatarNode) - self.avatarNode.addSubnode(videoNode) - self.videoNode = videoNode - } - } else if let videoNode = self.videoNode { - self.videoNode = nil - videoNode.removeFromSupernode() - } - - if let videoNode = self.videoNode { - videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) + self.avatarVideoNode?.updateVisibility(isVisible) + + if let videoNode = self.avatarVideoNode { + videoNode.updateLayout(size: self.avatarNode.frame.size, cornerRadius: self.avatarNode.frame.size.width / 2.0, transition: .immediate) videoNode.frame = self.avatarNode.bounds } } @@ -3513,159 +3443,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { super.updateRevealOffset(offset: offset, transition: transition) transition.updateBounds(node: self.contextContainer, bounds: self.contextContainer.frame.offsetBy(dx: -offset, dy: 0.0)) - - /*if let item = self.item, let params = self.layoutParams?.5, let currentItemHeight = self.currentItemHeight, let countersSize = self.layoutParams?.6 { - let editingOffset: CGFloat - if let selectableControlNode = self.selectableControlNode { - editingOffset = selectableControlNode.bounds.size.width - var selectableControlFrame = selectableControlNode.frame - selectableControlFrame.origin.x = params.leftInset + offset - transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame) - } else { - editingOffset = 0.0 - } - - let layoutOffset: CGFloat = 0.0 - - if let reorderControlNode = self.reorderControlNode { - var reorderControlFrame = reorderControlNode.frame - reorderControlFrame.origin.x = params.width - params.rightInset - reorderControlFrame.size.width + offset - transition.updateFrame(node: reorderControlNode, frame: reorderControlFrame) - } - - let avatarDiameter = min(60.0, floor(item.presentationData.fontSize.baseDisplaySize * 60.0 / 17.0)) - - let avatarLeftInset: CGFloat - if case .forum = item.index { - avatarLeftInset = 50.0 - } else { - avatarLeftInset = 18.0 + avatarDiameter - } - - let leftInset: CGFloat = params.leftInset + avatarLeftInset - - let rawContentWidth = params.width - leftInset - params.rightInset - 10.0 - editingOffset - let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + 8.0), size: CGSize(width: rawContentWidth, height: currentItemHeight - 12.0 - 9.0)) - - let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + offset, dy: 0.0) - - var avatarFrame = self.avatarNode.frame - avatarFrame.origin.x = leftInset - avatarLeftInset + editingOffset + 10.0 + offset - transition.updateFrame(node: self.avatarNode, frame: avatarFrame) - if let videoNode = self.videoNode { - transition.updateFrame(node: videoNode, frame: CGRect(origin: .zero, size: avatarFrame.size)) - } - - if let avatarIconView = self.avatarIconView { - var avatarIconFrame = avatarIconView.frame - avatarIconFrame.origin.x = params.leftInset + floor((leftInset - params.leftInset - avatarIconFrame.width) / 2.0) + offset - transition.updateFrame(view: avatarIconView, frame: avatarIconFrame) - } - - var onlineFrame = self.onlineNode.frame - if self.onlineIsVoiceChat { - onlineFrame.origin.x = avatarFrame.maxX - onlineFrame.width + 1.0 - UIScreenPixel - } else { - onlineFrame.origin.x = avatarFrame.maxX - onlineFrame.width - 2.0 - } - transition.updateFrame(node: self.onlineNode, frame: onlineFrame) - - var titleOffset: CGFloat = 0.0 - if let secretIconNode = self.secretIconNode, let image = secretIconNode.image { - transition.updateFrame(node: secretIconNode, frame: CGRect(origin: CGPoint(x: contentRect.minX, y: secretIconNode.frame.minY), size: image.size)) - titleOffset += image.size.width + 3.0 - } - - let titleFrame = self.titleNode.frame - transition.updateFrameAdditive(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: titleFrame.origin.y), size: titleFrame.size)) - - var authorFrame = self.authorNode.frame - authorFrame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: authorFrame.origin.y), size: authorFrame.size) - transition.updateFrame(node: self.authorNode, frame: authorFrame) - - if let compoundHighlightingNode = self.compoundHighlightingNode { - let compoundHighlightingFrame = compoundHighlightingNode.frame - transition.updateFrame(node: compoundHighlightingNode, frame: CGRect(origin: CGPoint(x: authorFrame.minX, y: compoundHighlightingFrame.origin.y), size: compoundHighlightingFrame.size)) - } - - transition.updateFrame(node: self.inputActivitiesNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: self.inputActivitiesNode.frame.minY), size: self.inputActivitiesNode.bounds.size)) - - var textFrame = self.textNode.textNode.frame - textFrame.origin.x = contentRect.origin.x - transition.updateFrameAdditive(node: self.textNode.textNode, frame: textFrame) - - if let dustNode = self.dustNode { - transition.updateFrameAdditive(node: dustNode, frame: textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0)) - } - - var mediaPreviewOffsetX = textFrame.origin.x - let contentImageSpacing: CGFloat = 2.0 - for (_, media, mediaSize) in self.currentMediaPreviewSpecs { - guard let mediaId = media.id else { - continue - } - if let previewNode = self.mediaPreviewNodes[mediaId] { - transition.updateFrameAdditive(node: previewNode, frame: CGRect(origin: CGPoint(x: mediaPreviewOffsetX, y: previewNode.frame.minY), size: mediaSize)) - } - mediaPreviewOffsetX += mediaSize.width + contentImageSpacing - } - - let dateFrame = self.dateNode.frame - transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width, y: dateFrame.minY), size: dateFrame.size)) - - let statusFrame = self.statusNode.frame - - var statusOffset: CGFloat = 0.0 - if let dateStatusIconNode = self.dateStatusIconNode, let dateIconImage = dateStatusIconNode.image { - statusOffset += 2.0 + dateIconImage.size.width + 4.0 - var dateStatusX: CGFloat = contentRect.origin.x - dateStatusX += contentRect.size.width - dateStatusX += -dateFrame.size.width - 4.0 - dateIconImage.size.width - - let dateStatusY: CGFloat = dateStatusIconNode.frame.minY - - transition.updateFrame(node: dateStatusIconNode, frame: CGRect(origin: CGPoint(x: dateStatusX, y: dateStatusY), size: dateIconImage.size)) - } - - transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - statusFrame.size.width - statusOffset, y: statusFrame.minY), size: statusFrame.size)) - - var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleFrame.size.width + 3.0 + titleOffset - - if let credibilityIconView = self.credibilityIconView { - transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: credibilityIconView.frame.origin.y), size: credibilityIconView.bounds.size)) - nextTitleIconOrigin += credibilityIconView.bounds.size.width + 4.0 - } - - let mutedIconFrame = self.mutedIconNode.frame - transition.updateFrameAdditive(node: self.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 5.0, y: mutedIconFrame.minY), size: mutedIconFrame.size)) - nextTitleIconOrigin += mutedIconFrame.size.width + 3.0 - - let badgeFrame = self.badgeNode.frame - let updatedBadgeFrame = CGRect(origin: CGPoint(x: contentRect.maxX - badgeFrame.size.width, y: contentRect.maxY - badgeFrame.size.height - 2.0), size: badgeFrame.size) - transition.updateFrame(node: self.badgeNode, frame: updatedBadgeFrame) - - var mentionBadgeFrame = self.mentionBadgeNode.frame - if updatedBadgeFrame.width.isZero || self.badgeNode.isHidden { - mentionBadgeFrame.origin.x = updatedBadgeFrame.minX - mentionBadgeFrame.width - } else { - mentionBadgeFrame.origin.x = updatedBadgeFrame.minX - 6.0 - mentionBadgeFrame.width - } - transition.updateFrame(node: self.mentionBadgeNode, frame: mentionBadgeFrame) - - let pinnedIconSize = self.pinnedIconNode.bounds.size - if pinnedIconSize != CGSize.zero { - let badgeOffset: CGFloat - if countersSize.isZero { - badgeOffset = contentRect.maxX - pinnedIconSize.width - } else { - badgeOffset = contentRect.maxX - updatedBadgeFrame.size.width - 6.0 - pinnedIconSize.width - } - - let badgeBackgroundWidth = pinnedIconSize.width - let badgeBackgroundFrame = CGRect(x: badgeOffset, y: self.pinnedIconNode.frame.origin.y, width: badgeBackgroundWidth, height: pinnedIconSize.height) - transition.updateFrame(node: self.pinnedIconNode, frame: badgeBackgroundFrame) - } - }*/ } override func touchesToOtherItemsPrevented() { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 6d3ff83128..0c353c8edf 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -126,6 +126,7 @@ swift_library( "//submodules/TelegramBaseController:TelegramBaseController", "//submodules/DeviceLocationManager:DeviceLocationManager", "//submodules/AvatarNode:AvatarNode", + "//submodules/AvatarVideoNode:AvatarVideoNode", "//submodules/OverlayStatusController:OverlayStatusController", "//submodules/TelegramStringFormatting:TelegramStringFormatting", "//submodules/LiveLocationManager:LiveLocationManager", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 97c6956b19..635bcc49f3 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -3077,11 +3077,14 @@ public final class EmojiPagerContentComponent: Component { override public var contents: Any? { didSet { + self.onContentsUpdate() if let cloneLayer = self.cloneLayer { cloneLayer.contents = self.contents } } } + + public var onContentsUpdate: () -> Void = {} public init( item: Item, diff --git a/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift index 469e613d56..8968c95674 100644 --- a/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Sources/ChatAvatarNavigationNode.swift @@ -15,26 +15,22 @@ import HierarchyTrackingLayer import AccountContext import ComponentFlow import EmojiStatusComponent +import AvatarVideoNode private let normalFont = avatarPlaceholderFont(size: 16.0) private let smallFont = avatarPlaceholderFont(size: 12.0) -private let maxVideoLoopCount = 3 - final class ChatAvatarNavigationNode: ASDisplayNode { private var context: AccountContext? private let containerNode: ContextControllerSourceNode let avatarNode: AvatarNode - private var videoNode: UniversalVideoNode? + private var avatarVideoNode: AvatarVideoNode? let statusView: ComponentView - private var videoContent: NativeVideoContent? - private let playbackStartDisposable = MetaDisposable() private var cachedDataDisposable = MetaDisposable() private var hierarchyTrackingLayer: HierarchyTrackingLayer? - private var videoLoopCount = 0 private var trackingIsInHierarchy: Bool = false { didSet { @@ -79,7 +75,6 @@ final class ChatAvatarNavigationNode: ASDisplayNode { deinit { self.cachedDataDisposable.dispose() - self.playbackStartDisposable.dispose() } override func didLoad() { @@ -116,7 +111,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode { self.context = context self.avatarNode.setPeer(context: context, theme: theme, peer: peer, authorOfMessage: authorOfMessage, overrideImage: overrideImage, emptyColor: emptyColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad, displayDimensions: displayDimensions, storeUnrounded: storeUnrounded) - if let peer = peer, peer.isPremium { + if let peer = peer, peer.isPremium || "".isEmpty { self.cachedDataDisposable.set((context.account.postbox.peerView(id: peer.id) |> deliverOnMainQueue).start(next: { [weak self] peerView in guard let strongSelf = self else { @@ -140,14 +135,16 @@ final class ChatAvatarNavigationNode: ASDisplayNode { if isKnown { let photo = personalPhoto ?? profilePhoto - if let photo = photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { - let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() - let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(videoId, "header"), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil) - if videoContent.id != strongSelf.videoContent?.id { - strongSelf.videoNode?.removeFromSupernode() - strongSelf.videoContent = videoContent + if let photo = photo, !photo.videoRepresentations.isEmpty || photo.emojiMarkup != nil { + let videoNode: AvatarVideoNode + if let current = strongSelf.avatarVideoNode { + videoNode = current + } else { + videoNode = AvatarVideoNode(context: context) + strongSelf.avatarNode.addSubnode(videoNode) + strongSelf.avatarVideoNode = videoNode } + videoNode.update(peer: peer, photo: photo, size: CGSize(width: 37.0, height: 37.0)) if strongSelf.hierarchyTrackingLayer == nil { let hierarchyTrackingLayer = HierarchyTrackingLayer() @@ -168,12 +165,13 @@ final class ChatAvatarNavigationNode: ASDisplayNode { strongSelf.layer.addSublayer(hierarchyTrackingLayer) } } else { - strongSelf.videoContent = nil - + if let avatarVideoNode = strongSelf.avatarVideoNode { + avatarVideoNode.removeFromSupernode() + strongSelf.avatarVideoNode = nil + } strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer() strongSelf.hierarchyTrackingLayer = nil } - strongSelf.updateVideoVisibility() } else { let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() @@ -181,10 +179,9 @@ final class ChatAvatarNavigationNode: ASDisplayNode { })) } else { self.cachedDataDisposable.set(nil) - self.videoContent = nil - self.videoNode?.removeFromSupernode() - self.videoNode = nil + self.avatarVideoNode?.removeFromSupernode() + self.avatarVideoNode = nil self.hierarchyTrackingLayer?.removeFromSuperlayer() self.hierarchyTrackingLayer = nil @@ -229,75 +226,12 @@ final class ChatAvatarNavigationNode: ASDisplayNode { } private func updateVideoVisibility() { - guard let context = self.context else { - return - } - let isVisible = self.trackingIsInHierarchy - if isVisible, let videoContent = self.videoContent, self.videoLoopCount != maxVideoLoopCount { - if self.videoNode == nil { - let mediaManager = context.sharedContext.mediaManager - let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .minimal) - videoNode.clipsToBounds = true - videoNode.isUserInteractionEnabled = false - videoNode.isHidden = true - videoNode.playbackCompleted = { [weak self] in - if let strongSelf = self { - strongSelf.videoLoopCount += 1 - if strongSelf.videoLoopCount == maxVideoLoopCount { - if let videoNode = strongSelf.videoNode { - strongSelf.videoNode = nil - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in - videoNode?.removeFromSupernode() - }) - } - } - } - } - - if let _ = videoContent.startTimestamp { - self.playbackStartDisposable.set((videoNode.status - |> map { status -> Bool in - if let status = status, case .playing = status.status { - return true - } else { - return false - } - } - |> filter { playing in - return playing - } - |> take(1) - |> deliverOnMainQueue).start(completed: { [weak self] in - if let strongSelf = self { - Queue.mainQueue().after(0.15) { - strongSelf.videoNode?.isHidden = false - } - } - })) - } else { - self.playbackStartDisposable.set(nil) - videoNode.isHidden = false - } - videoNode.layer.cornerRadius = self.avatarNode.frame.size.width / 2.0 - if #available(iOS 13.0, *) { - videoNode.layer.cornerCurve = .circular - } - - videoNode.canAttachContent = true - videoNode.play() - - self.containerNode.insertSubnode(videoNode, aboveSubnode: self.avatarNode) - self.videoNode = videoNode - } - } else if let videoNode = self.videoNode { - self.videoNode = nil - videoNode.removeFromSupernode() - } - - if let videoNode = self.videoNode { - videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) - videoNode.frame = self.avatarNode.frame + self.avatarVideoNode?.updateVisibility(isVisible) + + if let videoNode = self.avatarVideoNode { + videoNode.updateLayout(size: self.avatarNode.frame.size, cornerRadius: self.avatarNode.frame.size.width / 2.0, transition: .immediate) + videoNode.frame = self.avatarNode.bounds } } }