From 394c0d7a2601cebf5ace0c8a7ae1d93054c11818 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 25 Jan 2023 05:12:31 +0400 Subject: [PATCH] Avatar playback improvements --- submodules/AvatarVideoNode/BUILD | 3 + .../Sources/AvatarVideoNode.swift | 111 ++++++++---- .../Sources/PeerInfoAvatarListNode.swift | 52 +++++- submodules/PremiumUI/Sources/HelloView.swift | 4 +- .../TelegramEngine/Data/PeersData.swift | 28 +++ .../TelegramUI/Sources/ChatController.swift | 11 +- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 168 +++++++++++++++--- 7 files changed, 295 insertions(+), 82 deletions(-) diff --git a/submodules/AvatarVideoNode/BUILD b/submodules/AvatarVideoNode/BUILD index 1464b865b3..dd69602adc 100644 --- a/submodules/AvatarVideoNode/BUILD +++ b/submodules/AvatarVideoNode/BUILD @@ -25,6 +25,9 @@ swift_library( "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/StickerResources:StickerResources", ], visibility = [ "//visibility:public", diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index 9f233ccfa9..784abdacf3 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -13,6 +13,9 @@ import GradientBackground import AnimationCache import MultiAnimationRenderer import EntityKeyboard +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import StickerResources private let maxVideoLoopCount = 3 @@ -26,6 +29,8 @@ public final class AvatarVideoNode: ASDisplayNode { private var fileDisposable: Disposable? private var animationFile: TelegramMediaFile? private var itemLayer: EmojiPagerContentComponent.View.ItemLayer? + private var useAnimationNode = false + private var animationNode: AnimatedStickerNode? private var videoNode: UniversalVideoNode? private var videoContent: NativeVideoContent? @@ -69,56 +74,74 @@ public final class AvatarVideoNode: ASDisplayNode { return } - let itemNativeFitSize = self.internalSize.width > 100.0 ? CGSize(width: 256.0, height: 256.0) : CGSize(width: 128.0, height: 128.0) - - 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: { _, _ in - } - ) - itemLayer.onContentsUpdate = { [weak self] in - if let self { - if !self.didAppear { - self.didAppear = true - Queue.mainQueue().after(0.15) { - self.backgroundNode.isHidden = false + if self.useAnimationNode { + let animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.autoplay = false + self.animationNode = animationNode + animationNode.started = { [weak self] in + if let self { + if !self.didAppear { + self.didAppear = true + Queue.mainQueue().after(0.15) { + self.backgroundNode.isHidden = false + } } } } + self.backgroundNode.addSubnode(animationNode) + } else { + let itemNativeFitSize = self.internalSize.width > 100.0 ? CGSize(width: 192.0, height: 192.0) : CGSize(width: 64.0, height: 64.0) + + 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: { _, _ in + } + ) + itemLayer.onContentsUpdate = { [weak self] in + if let self { + if !self.didAppear { + self.didAppear = true + Queue.mainQueue().after(0.15) { + self.backgroundNode.isHidden = false + } + } + } + } + itemLayer.layerTintColor = UIColor.white.cgColor + itemLayer.isVisibleForAnimations = self.visibility + self.itemLayer = itemLayer + self.backgroundNode.layer.addSublayer(itemLayer) } - itemLayer.layerTintColor = UIColor.white.cgColor - itemLayer.isVisibleForAnimations = self.visibility - self.itemLayer = itemLayer - self.backgroundNode.layer.addSublayer(itemLayer) if let (size, cornerRadius) = self.validLayout { self.updateLayout(size: size, cornerRadius: cornerRadius, transition: .immediate) } } - public func update(markup: TelegramMediaImage.EmojiMarkup, size: CGSize) { + public func update(markup: TelegramMediaImage.EmojiMarkup, size: CGSize, useAnimationNode: Bool = true) { guard markup != self.emojiMarkup else { return } self.emojiMarkup = markup self.internalSize = size + //self.useAnimationNode = useAnimationNode let colors = markup.backgroundColors.map { UInt32(bitPattern: $0) } if colors.count == 1 { @@ -160,7 +183,7 @@ public final class AvatarVideoNode: ASDisplayNode { public func update(peer: EnginePeer, photo: TelegramMediaImage, size: CGSize) { self.internalSize = size if let markup = photo.emojiMarkup { - self.update(markup: markup, size: size) + self.update(markup: markup, size: size, useAnimationNode: false) } else if let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { self.backgroundNode.image = nil @@ -177,6 +200,14 @@ public final class AvatarVideoNode: ASDisplayNode { private var visibility = false public func updateVisibility(_ isVisible: Bool) { self.visibility = isVisible + if isVisible, let animationNode = self.animationNode, let file = self.animationFile { + let pathPrefix = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) + let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm") + animationNode.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) + } + self.animationNode?.visibility = isVisible if isVisible, let videoContent = self.videoContent, self.videoLoopCount != maxVideoLoopCount { if self.videoNode == nil { let context = self.context @@ -247,9 +278,13 @@ public final class AvatarVideoNode: ASDisplayNode { videoNode.updateLayout(size: size, transition: transition) } + let itemSize = CGSize(width: size.width * 0.67, height: size.height * 0.67) + let itemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - itemSize.width) / 2.0), y: floorToScreenPixels((size.height - itemSize.height) / 2.0)), size: itemSize) + if let animationNode = self.animationNode { + animationNode.frame = itemFrame + animationNode.updateLayout(size: itemSize) + } if let itemLayer = self.itemLayer { - let itemSize = CGSize(width: size.width * 0.67, height: size.height * 0.67) - let itemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - itemSize.width) / 2.0), y: floorToScreenPixels((size.height - itemSize.height) / 2.0)), size: itemSize) itemLayer.frame = itemFrame } } diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index 66f903f369..8e2337c059 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -16,6 +16,7 @@ import UniversalMediaPlayer import RadialStatusNode import TelegramUIPreferences import AvatarNode +import AvatarVideoNode private class PeerInfoAvatarListLoadingStripNode: ASImageNode { private var currentInHierarchy = false @@ -210,6 +211,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { private var videoContent: NativeVideoContent? private var videoStartTimestamp: Double? private let playbackStartDisposable = MetaDisposable() + private var markupNode: AvatarVideoNode? private let statusDisposable = MetaDisposable() private let preloadDisposable = MetaDisposable() private let statusNode: RadialStatusNode @@ -260,6 +262,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { self.preloadDisposable.set(preloadVideoResource(postbox: self.context.account.postbox, userLocation: .other, userContentType: .video, resourceReference: videoContent.fileReference.resourceReference(videoContent.fileReference.media.resource), duration: duration).start()) } } + self.markupNode?.updateVisibility(isCentral) } } @@ -346,6 +349,12 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { } transition.updateAlpha(node: videoNode, alpha: 1.0 - fraction) } + if let markupNode = self.markupNode { + if case .immediate = transition, fraction == 1.0 { + return + } + transition.updateAlpha(node: markupNode, alpha: 1.0 - fraction) + } } private func setupVideoPlayback() { @@ -443,24 +452,27 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { let videoRepresentations: [VideoRepresentationWithReference] let immediateThumbnailData: Data? var id: Int64 + let markup: TelegramMediaImage.EmojiMarkup? switch item { case let .custom(node): - id = 0 representations = [] videoRepresentations = [] immediateThumbnailData = nil + id = 0 + markup = nil if !synchronous { self.addSubnode(node) } case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): + id = self.peer.id.id._internalGetInt64Value() representations = topRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail - id = self.peer.id.id._internalGetInt64Value() if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { id = id &+ resource.photoId } - case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, _): + markup = nil + case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue): representations = imageRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail @@ -469,10 +481,37 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { } else { id = self.peer.id.id._internalGetInt64Value() } + markup = markupValue } self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.context.account, representations: representations, immediateThumbnailData: immediateThumbnailData, autoFetchFullSize: true, attemptSynchronously: synchronous, skipThumbnail: fullSizeOnly, skipBlurIfLarge: isMain), attemptSynchronously: synchronous, dispatchOnDisplayLink: false) - if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer) { + if let markup { + if let videoNode = self.videoNode { + self.videoContent = nil + self.videoStartTimestamp = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + self.statusPromise.set(.single(nil)) + self.statusDisposable.set(nil) + + let markupNode: AvatarVideoNode + if let current = self.markupNode { + markupNode = current + } else { + markupNode = AvatarVideoNode(context: self.context) + self.insertSubnode(markupNode, belowSubnode: self.statusNode) + self.markupNode = markupNode + } + markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) + markupNode.updateVisibility(self.isCentral ?? true) + + if !self.didSetReady { + self.didSetReady = true + self.isReady.set(.single(true)) + } + } else if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) let videoContent = NativeVideoContent(id: .profileVideo(id, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: fullSizeOnly, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) @@ -491,7 +530,6 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { } self.statusPromise.set(.single(nil)) - self.statusDisposable.set(nil) self.imageNode.imageUpdated = { [weak self] _ in @@ -520,6 +558,10 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { videoNode.updateLayout(size: imageSize, transition: .immediate) videoNode.frame = imageFrame } + if let markupNode = self.markupNode { + markupNode.updateLayout(size: imageSize, cornerRadius: 0.0, transition: .immediate) + markupNode.frame = imageFrame + } } } diff --git a/submodules/PremiumUI/Sources/HelloView.swift b/submodules/PremiumUI/Sources/HelloView.swift index a7fe6eb60e..f682a5cab7 100644 --- a/submodules/PremiumUI/Sources/HelloView.swift +++ b/submodules/PremiumUI/Sources/HelloView.swift @@ -24,7 +24,7 @@ private let phrases = [ "Halo" ] -private var activeCount = 13 +private var simultaneousDisplayCount = 13 private let referenceWidth: CGFloat = 1180 private let positions: [CGPoint] = [ @@ -69,7 +69,7 @@ final class HelloView: UIView, PhoneDemoDecorationView { let phraseIds = Array(self.availablePhraseIds()).shuffled() let positionIds = Array(self.availablePositionIds()).shuffled() - for i in 0 ..< activeCount { + for i in 0 ..< simultaneousDisplayCount { let delay: Double = Double.random(in: 0.0 ..< 0.8) Queue.mainQueue().after(delay) { self.spawnPhrase(phraseIds[i], positionIndex: positionIds[i]) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index dca233f9f0..45e73825c7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -838,6 +838,34 @@ public extension TelegramEngine.EngineData.Item { } } + public struct TranslationHidden: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.flags.contains(.translationHidden) + } else { + return false + } + } + } + public struct LegacyGroupParticipants: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = EnginePeerCachedInfoItem<[EngineLegacyGroupParticipant]> diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6650241eeb..6a43524d3a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -6737,15 +6737,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return peer?.isPremium ?? false } |> distinctUntilChanged - let isHidden = self.chatDisplayNode.historyNode.cachedPeerDataAndMessages - |> map { cachedDataAndMessages -> Bool in - let (cachedData, _) = cachedDataAndMessages - var isHidden = false - if let cachedData = cachedData as? CachedChannelData, cachedData.flags.contains(.translationHidden) { - isHidden = true - } - return isHidden - } |> distinctUntilChanged + let isHidden = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.TranslationHidden(id: self.context.account.peerId)) + |> distinctUntilChanged self.translationStateDisposable = (combineLatest( queue: .concurrentDefaultQueue(), isPremium, diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index f78fcacc9c..9fbc8ad3f6 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -29,6 +29,7 @@ import MultiAnimationRenderer import ComponentDisplayAdapters import ChatTitleView import AppBundle +import AvatarVideoNode enum PeerInfoHeaderButtonKey: Hashable { case message @@ -309,6 +310,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let avatarNode: AvatarNode fileprivate var videoNode: UniversalVideoNode? + fileprivate var markupNode: AvatarVideoNode? fileprivate var iconView: ComponentView? private var videoContent: NativeVideoContent? private var videoStartTimestamp: Double? @@ -380,12 +382,23 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { return } if fraction > 0.0 { - self.videoNode?.pause() + videoNode.pause() } else { - self.videoNode?.play() + videoNode.play() } transition.updateAlpha(node: videoNode, alpha: 1.0 - fraction) } + if let markupNode = self.markupNode { + if case .immediate = transition, fraction == 1.0 { + return + } + if fraction > 0.0 { + markupNode.updateVisibility(false) + } else { + markupNode.updateVisibility(true) + } + transition.updateAlpha(node: markupNode, alpha: 1.0 - fraction) + } } var removedPhotoResourceIds = Set() @@ -461,9 +474,11 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { } } + var isForum = false let avatarCornerRadius: CGFloat if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { avatarCornerRadius = floor(avatarSize * 0.25) + isForum = true } else { avatarCornerRadius = avatarSize / 2.0 } @@ -485,12 +500,14 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let videoRepresentations: [VideoRepresentationWithReference] let immediateThumbnailData: Data? var videoId: Int64 + let markup: TelegramMediaImage.EmojiMarkup? switch item { case .custom: representations = [] videoRepresentations = [] immediateThumbnailData = nil videoId = 0 + markup = nil case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): representations = topRepresentations videoRepresentations = videoRepresentationsValue @@ -499,7 +516,8 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { videoId = videoId &+ resource.photoId } - case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, _): + markup = nil + case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue): representations = imageRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail @@ -508,11 +526,31 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { } else { videoId = peer.id.id._internalGetInt64Value() } + markup = markupValue } self.containerNode.isGestureEnabled = !isSettings - if threadInfo == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { + if let markup { + if let videoNode = self.videoNode { + self.videoContent = nil + self.videoStartTimestamp = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + + let markupNode: AvatarVideoNode + if let current = self.markupNode { + markupNode = current + } else { + markupNode = AvatarVideoNode(context: self.context) + self.containerNode.addSubnode(markupNode) + self.markupNode = markupNode + } + markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) + markupNode.updateVisibility(true) + } else if threadInfo == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { @@ -553,28 +591,51 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { self.videoContent = videoContent self.videoNode = videoNode - let maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size)) + let maskPath: UIBezierPath + if isForum { + maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius) + } else { + maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size)) + } let shape = CAShapeLayer() shape.path = maskPath.cgPath videoNode.layer.mask = shape self.containerNode.addSubnode(videoNode) } - } else if let videoNode = self.videoNode { + } else { + if let markupNode = self.markupNode { + self.markupNode = nil + markupNode.removeFromSupernode() + } + if let videoNode = self.videoNode { + self.videoStartTimestamp = nil + self.videoContent = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + } + } else { + if let markupNode = self.markupNode { + self.markupNode = nil + markupNode.removeFromSupernode() + } + if let videoNode = self.videoNode { + self.videoStartTimestamp = nil self.videoContent = nil self.videoNode = nil videoNode.removeFromSupernode() } - } else if let videoNode = self.videoNode { - self.videoContent = nil - self.videoNode = nil - - videoNode.removeFromSupernode() - self.containerNode.isGestureEnabled = false } + if let markupNode = self.markupNode { + markupNode.frame = self.avatarNode.frame + markupNode.updateLayout(size: self.avatarNode.frame.size, cornerRadius: avatarCornerRadius, transition: .immediate) + } + if let videoNode = self.videoNode { if self.canAttachVideo { videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) @@ -730,6 +791,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { private let context: AccountContext let avatarNode: AvatarNode fileprivate var videoNode: UniversalVideoNode? + fileprivate var markupNode: AvatarVideoNode? private var videoContent: NativeVideoContent? private var videoStartTimestamp: Double? var item: PeerInfoAvatarListItem? @@ -799,8 +861,10 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) self.avatarNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) + var isForum = false let avatarCornerRadius: CGFloat if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { + isForum = true avatarCornerRadius = floor(avatarSize * 0.25) } else { avatarCornerRadius = avatarSize / 2.0 @@ -816,35 +880,63 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { let representations: [ImageRepresentationWithReference] let videoRepresentations: [VideoRepresentationWithReference] let immediateThumbnailData: Data? - var id: Int64 + var videoId: Int64 + let markup: TelegramMediaImage.EmojiMarkup? switch item { case .custom: representations = [] videoRepresentations = [] immediateThumbnailData = nil - id = 0 + videoId = 0 + markup = nil case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): representations = topRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail - id = peer.id.id._internalGetInt64Value() + videoId = peer.id.id._internalGetInt64Value() if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { - id = id &+ resource.photoId + videoId = videoId &+ resource.photoId } - case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, _): + markup = nil + case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue): representations = imageRepresentations videoRepresentations = videoRepresentationsValue immediateThumbnailData = immediateThumbnail if case let .cloud(imageId, _, _) = reference { - id = imageId + videoId = imageId } else { - id = peer.id.id._internalGetInt64Value() + videoId = peer.id.id._internalGetInt64Value() } + markup = markupValue } - if threadData == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { + if let markup { + if let videoNode = self.videoNode { + self.videoContent = nil + self.videoStartTimestamp = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } + + let markupNode: AvatarVideoNode + if let current = self.markupNode { + markupNode = current + } else { + markupNode = AvatarVideoNode(context: self.context) + self.insertSubnode(markupNode, aboveSubnode: self.avatarNode) + self.markupNode = markupNode + } + markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0)) + markupNode.updateVisibility(true) + } else if threadData == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) { + if let markupNode = self.markupNode { + self.markupNode = nil + markupNode.removeFromSupernode() + } + let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [])])) - let videoContent = NativeVideoContent(id: .profileVideo(id, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) + let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil) if videoContent.id != self.videoContent?.id { self.videoNode?.removeFromSupernode() @@ -855,19 +947,30 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { self.videoContent = videoContent self.videoNode = videoNode - let maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size)) + let maskPath: UIBezierPath + if isForum { + maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius) + } else { + maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size)) + } let shape = CAShapeLayer() shape.path = maskPath.cgPath videoNode.layer.mask = shape self.insertSubnode(videoNode, aboveSubnode: self.avatarNode) } - } else if let videoNode = self.videoNode { - self.videoStartTimestamp = nil - self.videoContent = nil - self.videoNode = nil - - videoNode.removeFromSupernode() + } else { + if let markupNode = self.markupNode { + self.markupNode = nil + markupNode.removeFromSupernode() + } + if let videoNode = self.videoNode { + self.videoStartTimestamp = nil + self.videoContent = nil + self.videoNode = nil + + videoNode.removeFromSupernode() + } } } else if let videoNode = self.videoNode { self.videoStartTimestamp = nil @@ -877,6 +980,11 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { videoNode.removeFromSupernode() } + if let markupNode = self.markupNode { + markupNode.frame = self.avatarNode.frame + markupNode.updateLayout(size: self.avatarNode.frame.size, cornerRadius: avatarCornerRadius, transition: .immediate) + } + if let videoNode = self.videoNode { if self.canAttachVideo { videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) @@ -897,6 +1005,8 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { } } + + final class PeerInfoAvatarListNode: ASDisplayNode { private let isSettings: Bool let pinchSourceNode: PinchSourceContainerNode @@ -1018,6 +1128,8 @@ final class PeerInfoAvatarListNode: ASDisplayNode { if let currentItemNode = self.listContainerNode.currentItemNode, case .animated = transition { if let _ = self.avatarContainerNode.videoNode { + } else if let _ = self.avatarContainerNode.markupNode { + } else if let unroundedImage = self.avatarContainerNode.avatarNode.unroundedImage { let avatarCopyView = UIImageView() avatarCopyView.image = unroundedImage