Avatar playback improvements

This commit is contained in:
Ilya Laktyushin 2023-01-25 05:12:31 +04:00
parent cc0595de4b
commit 394c0d7a26
7 changed files with 295 additions and 82 deletions

View File

@ -25,6 +25,9 @@ swift_library(
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/StickerResources:StickerResources",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -13,6 +13,9 @@ import GradientBackground
import AnimationCache import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
import EntityKeyboard import EntityKeyboard
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import StickerResources
private let maxVideoLoopCount = 3 private let maxVideoLoopCount = 3
@ -26,6 +29,8 @@ public final class AvatarVideoNode: ASDisplayNode {
private var fileDisposable: Disposable? private var fileDisposable: Disposable?
private var animationFile: TelegramMediaFile? private var animationFile: TelegramMediaFile?
private var itemLayer: EmojiPagerContentComponent.View.ItemLayer? private var itemLayer: EmojiPagerContentComponent.View.ItemLayer?
private var useAnimationNode = false
private var animationNode: AnimatedStickerNode?
private var videoNode: UniversalVideoNode? private var videoNode: UniversalVideoNode?
private var videoContent: NativeVideoContent? private var videoContent: NativeVideoContent?
@ -69,56 +74,74 @@ public final class AvatarVideoNode: ASDisplayNode {
return return
} }
let itemNativeFitSize = self.internalSize.width > 100.0 ? CGSize(width: 256.0, height: 256.0) : CGSize(width: 128.0, height: 128.0) if self.useAnimationNode {
let animationNode = DefaultAnimatedStickerNodeImpl()
let animationData = EntityKeyboardAnimationData(file: animationFile) animationNode.autoplay = false
let itemLayer = EmojiPagerContentComponent.View.ItemLayer( self.animationNode = animationNode
item: EmojiPagerContentComponent.Item( animationNode.started = { [weak self] in
animationData: animationData, if let self {
content: .animation(animationData), if !self.didAppear {
itemFile: animationFile, self.didAppear = true
subgroupId: nil, Queue.mainQueue().after(0.15) {
icon: .none, self.backgroundNode.isHidden = false
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
} }
} }
} }
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 { if let (size, cornerRadius) = self.validLayout {
self.updateLayout(size: size, cornerRadius: cornerRadius, transition: .immediate) 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 { guard markup != self.emojiMarkup else {
return return
} }
self.emojiMarkup = markup self.emojiMarkup = markup
self.internalSize = size self.internalSize = size
//self.useAnimationNode = useAnimationNode
let colors = markup.backgroundColors.map { UInt32(bitPattern: $0) } let colors = markup.backgroundColors.map { UInt32(bitPattern: $0) }
if colors.count == 1 { if colors.count == 1 {
@ -160,7 +183,7 @@ public final class AvatarVideoNode: ASDisplayNode {
public func update(peer: EnginePeer, photo: TelegramMediaImage, size: CGSize) { public func update(peer: EnginePeer, photo: TelegramMediaImage, size: CGSize) {
self.internalSize = size self.internalSize = size
if let markup = photo.emojiMarkup { 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()) { } else if let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) {
self.backgroundNode.image = nil self.backgroundNode.image = nil
@ -177,6 +200,14 @@ public final class AvatarVideoNode: ASDisplayNode {
private var visibility = false private var visibility = false
public func updateVisibility(_ isVisible: Bool) { public func updateVisibility(_ isVisible: Bool) {
self.visibility = isVisible 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 isVisible, let videoContent = self.videoContent, self.videoLoopCount != maxVideoLoopCount {
if self.videoNode == nil { if self.videoNode == nil {
let context = self.context let context = self.context
@ -247,9 +278,13 @@ public final class AvatarVideoNode: ASDisplayNode {
videoNode.updateLayout(size: size, transition: transition) 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 { 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 itemLayer.frame = itemFrame
} }
} }

View File

@ -16,6 +16,7 @@ import UniversalMediaPlayer
import RadialStatusNode import RadialStatusNode
import TelegramUIPreferences import TelegramUIPreferences
import AvatarNode import AvatarNode
import AvatarVideoNode
private class PeerInfoAvatarListLoadingStripNode: ASImageNode { private class PeerInfoAvatarListLoadingStripNode: ASImageNode {
private var currentInHierarchy = false private var currentInHierarchy = false
@ -210,6 +211,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
private var videoContent: NativeVideoContent? private var videoContent: NativeVideoContent?
private var videoStartTimestamp: Double? private var videoStartTimestamp: Double?
private let playbackStartDisposable = MetaDisposable() private let playbackStartDisposable = MetaDisposable()
private var markupNode: AvatarVideoNode?
private let statusDisposable = MetaDisposable() private let statusDisposable = MetaDisposable()
private let preloadDisposable = MetaDisposable() private let preloadDisposable = MetaDisposable()
private let statusNode: RadialStatusNode 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.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) 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() { private func setupVideoPlayback() {
@ -443,24 +452,27 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
let videoRepresentations: [VideoRepresentationWithReference] let videoRepresentations: [VideoRepresentationWithReference]
let immediateThumbnailData: Data? let immediateThumbnailData: Data?
var id: Int64 var id: Int64
let markup: TelegramMediaImage.EmojiMarkup?
switch item { switch item {
case let .custom(node): case let .custom(node):
id = 0
representations = [] representations = []
videoRepresentations = [] videoRepresentations = []
immediateThumbnailData = nil immediateThumbnailData = nil
id = 0
markup = nil
if !synchronous { if !synchronous {
self.addSubnode(node) self.addSubnode(node)
} }
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
id = self.peer.id.id._internalGetInt64Value()
representations = topRepresentations representations = topRepresentations
videoRepresentations = videoRepresentationsValue videoRepresentations = videoRepresentationsValue
immediateThumbnailData = immediateThumbnail immediateThumbnailData = immediateThumbnail
id = self.peer.id.id._internalGetInt64Value()
if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource {
id = id &+ resource.photoId id = id &+ resource.photoId
} }
case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, _): markup = nil
case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue):
representations = imageRepresentations representations = imageRepresentations
videoRepresentations = videoRepresentationsValue videoRepresentations = videoRepresentationsValue
immediateThumbnailData = immediateThumbnail immediateThumbnailData = immediateThumbnail
@ -469,10 +481,37 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
} else { } else {
id = self.peer.id.id._internalGetInt64Value() 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) 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 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) 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.statusPromise.set(.single(nil))
self.statusDisposable.set(nil) self.statusDisposable.set(nil)
self.imageNode.imageUpdated = { [weak self] _ in self.imageNode.imageUpdated = { [weak self] _ in
@ -520,6 +558,10 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
videoNode.updateLayout(size: imageSize, transition: .immediate) videoNode.updateLayout(size: imageSize, transition: .immediate)
videoNode.frame = imageFrame videoNode.frame = imageFrame
} }
if let markupNode = self.markupNode {
markupNode.updateLayout(size: imageSize, cornerRadius: 0.0, transition: .immediate)
markupNode.frame = imageFrame
}
} }
} }

View File

@ -24,7 +24,7 @@ private let phrases = [
"Halo" "Halo"
] ]
private var activeCount = 13 private var simultaneousDisplayCount = 13
private let referenceWidth: CGFloat = 1180 private let referenceWidth: CGFloat = 1180
private let positions: [CGPoint] = [ private let positions: [CGPoint] = [
@ -69,7 +69,7 @@ final class HelloView: UIView, PhoneDemoDecorationView {
let phraseIds = Array(self.availablePhraseIds()).shuffled() let phraseIds = Array(self.availablePhraseIds()).shuffled()
let positionIds = Array(self.availablePositionIds()).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) let delay: Double = Double.random(in: 0.0 ..< 0.8)
Queue.mainQueue().after(delay) { Queue.mainQueue().after(delay) {
self.spawnPhrase(phraseIds[i], positionIndex: positionIds[i]) self.spawnPhrase(phraseIds[i], positionIndex: positionIds[i])

View File

@ -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 struct LegacyGroupParticipants: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem {
public typealias Result = EnginePeerCachedInfoItem<[EngineLegacyGroupParticipant]> public typealias Result = EnginePeerCachedInfoItem<[EngineLegacyGroupParticipant]>

View File

@ -6737,15 +6737,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return peer?.isPremium ?? false return peer?.isPremium ?? false
} |> distinctUntilChanged } |> distinctUntilChanged
let isHidden = self.chatDisplayNode.historyNode.cachedPeerDataAndMessages let isHidden = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.TranslationHidden(id: self.context.account.peerId))
|> map { cachedDataAndMessages -> Bool in |> distinctUntilChanged
let (cachedData, _) = cachedDataAndMessages
var isHidden = false
if let cachedData = cachedData as? CachedChannelData, cachedData.flags.contains(.translationHidden) {
isHidden = true
}
return isHidden
} |> distinctUntilChanged
self.translationStateDisposable = (combineLatest( self.translationStateDisposable = (combineLatest(
queue: .concurrentDefaultQueue(), queue: .concurrentDefaultQueue(),
isPremium, isPremium,

View File

@ -29,6 +29,7 @@ import MultiAnimationRenderer
import ComponentDisplayAdapters import ComponentDisplayAdapters
import ChatTitleView import ChatTitleView
import AppBundle import AppBundle
import AvatarVideoNode
enum PeerInfoHeaderButtonKey: Hashable { enum PeerInfoHeaderButtonKey: Hashable {
case message case message
@ -309,6 +310,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
let avatarNode: AvatarNode let avatarNode: AvatarNode
fileprivate var videoNode: UniversalVideoNode? fileprivate var videoNode: UniversalVideoNode?
fileprivate var markupNode: AvatarVideoNode?
fileprivate var iconView: ComponentView<Empty>? fileprivate var iconView: ComponentView<Empty>?
private var videoContent: NativeVideoContent? private var videoContent: NativeVideoContent?
private var videoStartTimestamp: Double? private var videoStartTimestamp: Double?
@ -380,12 +382,23 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
return return
} }
if fraction > 0.0 { if fraction > 0.0 {
self.videoNode?.pause() videoNode.pause()
} else { } else {
self.videoNode?.play() videoNode.play()
} }
transition.updateAlpha(node: videoNode, alpha: 1.0 - fraction) 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<String>() var removedPhotoResourceIds = Set<String>()
@ -461,9 +474,11 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
} }
} }
var isForum = false
let avatarCornerRadius: CGFloat let avatarCornerRadius: CGFloat
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
avatarCornerRadius = floor(avatarSize * 0.25) avatarCornerRadius = floor(avatarSize * 0.25)
isForum = true
} else { } else {
avatarCornerRadius = avatarSize / 2.0 avatarCornerRadius = avatarSize / 2.0
} }
@ -485,12 +500,14 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
let videoRepresentations: [VideoRepresentationWithReference] let videoRepresentations: [VideoRepresentationWithReference]
let immediateThumbnailData: Data? let immediateThumbnailData: Data?
var videoId: Int64 var videoId: Int64
let markup: TelegramMediaImage.EmojiMarkup?
switch item { switch item {
case .custom: case .custom:
representations = [] representations = []
videoRepresentations = [] videoRepresentations = []
immediateThumbnailData = nil immediateThumbnailData = nil
videoId = 0 videoId = 0
markup = nil
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
representations = topRepresentations representations = topRepresentations
videoRepresentations = videoRepresentationsValue videoRepresentations = videoRepresentationsValue
@ -499,7 +516,8 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource {
videoId = videoId &+ 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 representations = imageRepresentations
videoRepresentations = videoRepresentationsValue videoRepresentations = videoRepresentationsValue
immediateThumbnailData = immediateThumbnail immediateThumbnailData = immediateThumbnail
@ -508,11 +526,31 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
} else { } else {
videoId = peer.id.id._internalGetInt64Value() videoId = peer.id.id._internalGetInt64Value()
} }
markup = markupValue
} }
self.containerNode.isGestureEnabled = !isSettings 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 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) 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 { if videoContent.id != self.videoContent?.id {
@ -553,28 +591,51 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
self.videoContent = videoContent self.videoContent = videoContent
self.videoNode = videoNode 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() let shape = CAShapeLayer()
shape.path = maskPath.cgPath shape.path = maskPath.cgPath
videoNode.layer.mask = shape videoNode.layer.mask = shape
self.containerNode.addSubnode(videoNode) 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.videoContent = nil
self.videoNode = nil self.videoNode = nil
videoNode.removeFromSupernode() videoNode.removeFromSupernode()
} }
} else if let videoNode = self.videoNode {
self.videoContent = nil
self.videoNode = nil
videoNode.removeFromSupernode()
self.containerNode.isGestureEnabled = false 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 let videoNode = self.videoNode {
if self.canAttachVideo { if self.canAttachVideo {
videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate)
@ -730,6 +791,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
private let context: AccountContext private let context: AccountContext
let avatarNode: AvatarNode let avatarNode: AvatarNode
fileprivate var videoNode: UniversalVideoNode? fileprivate var videoNode: UniversalVideoNode?
fileprivate var markupNode: AvatarVideoNode?
private var videoContent: NativeVideoContent? private var videoContent: NativeVideoContent?
private var videoStartTimestamp: Double? private var videoStartTimestamp: Double?
var item: PeerInfoAvatarListItem? 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.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)) 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 let avatarCornerRadius: CGFloat
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) { if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
isForum = true
avatarCornerRadius = floor(avatarSize * 0.25) avatarCornerRadius = floor(avatarSize * 0.25)
} else { } else {
avatarCornerRadius = avatarSize / 2.0 avatarCornerRadius = avatarSize / 2.0
@ -816,35 +880,63 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
let representations: [ImageRepresentationWithReference] let representations: [ImageRepresentationWithReference]
let videoRepresentations: [VideoRepresentationWithReference] let videoRepresentations: [VideoRepresentationWithReference]
let immediateThumbnailData: Data? let immediateThumbnailData: Data?
var id: Int64 var videoId: Int64
let markup: TelegramMediaImage.EmojiMarkup?
switch item { switch item {
case .custom: case .custom:
representations = [] representations = []
videoRepresentations = [] videoRepresentations = []
immediateThumbnailData = nil immediateThumbnailData = nil
id = 0 videoId = 0
markup = nil
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail): case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
representations = topRepresentations representations = topRepresentations
videoRepresentations = videoRepresentationsValue videoRepresentations = videoRepresentationsValue
immediateThumbnailData = immediateThumbnail immediateThumbnailData = immediateThumbnail
id = peer.id.id._internalGetInt64Value() videoId = peer.id.id._internalGetInt64Value()
if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource { 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 representations = imageRepresentations
videoRepresentations = videoRepresentationsValue videoRepresentations = videoRepresentationsValue
immediateThumbnailData = immediateThumbnail immediateThumbnailData = immediateThumbnail
if case let .cloud(imageId, _, _) = reference { if case let .cloud(imageId, _, _) = reference {
id = imageId videoId = imageId
} else { } 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 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 { if videoContent.id != self.videoContent?.id {
self.videoNode?.removeFromSupernode() self.videoNode?.removeFromSupernode()
@ -855,19 +947,30 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
self.videoContent = videoContent self.videoContent = videoContent
self.videoNode = videoNode 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() let shape = CAShapeLayer()
shape.path = maskPath.cgPath shape.path = maskPath.cgPath
videoNode.layer.mask = shape videoNode.layer.mask = shape
self.insertSubnode(videoNode, aboveSubnode: self.avatarNode) self.insertSubnode(videoNode, aboveSubnode: self.avatarNode)
} }
} else if let videoNode = self.videoNode { } else {
self.videoStartTimestamp = nil if let markupNode = self.markupNode {
self.videoContent = nil self.markupNode = nil
self.videoNode = nil markupNode.removeFromSupernode()
}
videoNode.removeFromSupernode() if let videoNode = self.videoNode {
self.videoStartTimestamp = nil
self.videoContent = nil
self.videoNode = nil
videoNode.removeFromSupernode()
}
} }
} else if let videoNode = self.videoNode { } else if let videoNode = self.videoNode {
self.videoStartTimestamp = nil self.videoStartTimestamp = nil
@ -877,6 +980,11 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
videoNode.removeFromSupernode() 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 let videoNode = self.videoNode {
if self.canAttachVideo { if self.canAttachVideo {
videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate) videoNode.updateLayout(size: self.avatarNode.frame.size, transition: .immediate)
@ -897,6 +1005,8 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
} }
} }
final class PeerInfoAvatarListNode: ASDisplayNode { final class PeerInfoAvatarListNode: ASDisplayNode {
private let isSettings: Bool private let isSettings: Bool
let pinchSourceNode: PinchSourceContainerNode let pinchSourceNode: PinchSourceContainerNode
@ -1018,6 +1128,8 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
if let currentItemNode = self.listContainerNode.currentItemNode, case .animated = transition { if let currentItemNode = self.listContainerNode.currentItemNode, case .animated = transition {
if let _ = self.avatarContainerNode.videoNode { if let _ = self.avatarContainerNode.videoNode {
} else if let _ = self.avatarContainerNode.markupNode {
} else if let unroundedImage = self.avatarContainerNode.avatarNode.unroundedImage { } else if let unroundedImage = self.avatarContainerNode.avatarNode.unroundedImage {
let avatarCopyView = UIImageView() let avatarCopyView = UIImageView()
avatarCopyView.image = unroundedImage avatarCopyView.image = unroundedImage