mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1426 lines
65 KiB
Swift
1426 lines
65 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
import AccountContext
|
|
import TelegramPresentationData
|
|
import PhotoResources
|
|
import PeerAvatarGalleryUI
|
|
import TelegramStringFormatting
|
|
import TelegramUniversalVideoContent
|
|
import GalleryUI
|
|
import UniversalMediaPlayer
|
|
import RadialStatusNode
|
|
import TelegramUIPreferences
|
|
import AvatarNode
|
|
import AvatarVideoNode
|
|
|
|
private class PeerInfoAvatarListLoadingStripNode: ASImageNode {
|
|
private var currentInHierarchy = false
|
|
|
|
let imageNode = ASImageNode()
|
|
|
|
override init() {
|
|
super.init()
|
|
|
|
self.addSubnode(self.imageNode)
|
|
}
|
|
|
|
override public var isHidden: Bool {
|
|
didSet {
|
|
self.updateAnimation()
|
|
}
|
|
}
|
|
private var isAnimating = false {
|
|
didSet {
|
|
if self.isAnimating != oldValue {
|
|
if self.isAnimating {
|
|
let basicAnimation = CABasicAnimation(keyPath: "opacity")
|
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
basicAnimation.duration = 0.45
|
|
basicAnimation.fromValue = 0.1
|
|
basicAnimation.toValue = 0.75
|
|
basicAnimation.repeatCount = Float.infinity
|
|
basicAnimation.autoreverses = true
|
|
|
|
self.imageNode.layer.add(basicAnimation, forKey: "loading")
|
|
} else {
|
|
self.imageNode.layer.removeAnimation(forKey: "loading")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateAnimation() {
|
|
self.isAnimating = !self.isHidden && self.currentInHierarchy
|
|
}
|
|
|
|
override public func willEnterHierarchy() {
|
|
super.willEnterHierarchy()
|
|
|
|
self.currentInHierarchy = true
|
|
self.updateAnimation()
|
|
}
|
|
|
|
override public func didExitHierarchy() {
|
|
super.didExitHierarchy()
|
|
|
|
self.currentInHierarchy = false
|
|
self.updateAnimation()
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
|
|
self.imageNode.frame = self.bounds
|
|
}
|
|
}
|
|
|
|
private struct CustomListItemResourceId {
|
|
public var uniqueId: String {
|
|
return "customNode"
|
|
}
|
|
|
|
public var hashValue: Int {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
public enum PeerInfoAvatarListItem: Equatable {
|
|
case custom(ASDisplayNode)
|
|
case topImage([ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?)
|
|
case image(TelegramMediaImageReference?, [ImageRepresentationWithReference], [VideoRepresentationWithReference], Data?, Bool, TelegramMediaImage.EmojiMarkup?)
|
|
|
|
var id: EngineMediaResource.Id {
|
|
switch self {
|
|
case .custom:
|
|
return EngineMediaResource.Id(CustomListItemResourceId().uniqueId)
|
|
case let .topImage(representations, _, _):
|
|
let representation = largestImageRepresentation(representations.map { $0.representation }) ?? representations[representations.count - 1].representation
|
|
return EngineMediaResource.Id(representation.resource.id)
|
|
case let .image(_, representations, _, _, _, _):
|
|
let representation = largestImageRepresentation(representations.map { $0.representation }) ?? representations[representations.count - 1].representation
|
|
return EngineMediaResource.Id(representation.resource.id)
|
|
}
|
|
}
|
|
|
|
func isSemanticallyEqual(to: PeerInfoAvatarListItem) -> Bool {
|
|
if case let .topImage(lhsRepresentations, _, _) = self {
|
|
if case let .topImage(rhsRepresentations, _, _) = self {
|
|
if let lhsRepresentation = largestImageRepresentation(lhsRepresentations.map { $0.representation }),
|
|
let rhsRepresentation = largestImageRepresentation(rhsRepresentations.map { $0.representation }) {
|
|
return lhsRepresentation.isSemanticallyEqual(to: rhsRepresentation)
|
|
} else {
|
|
return false
|
|
}
|
|
} else if case let .image(_, rhsRepresentations, _, _, _, _) = self {
|
|
if let lhsRepresentation = largestImageRepresentation(lhsRepresentations.map { $0.representation }),
|
|
let rhsRepresentation = largestImageRepresentation(rhsRepresentations.map { $0.representation }) {
|
|
return lhsRepresentation.isSemanticallyEqual(to: rhsRepresentation)
|
|
} else {
|
|
return false
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
} else if case let .image(_, lhsRepresentations, _, _, _, _) = self {
|
|
if case let .topImage(rhsRepresentations, _, _) = self {
|
|
if let lhsRepresentation = largestImageRepresentation(lhsRepresentations.map { $0.representation }),
|
|
let rhsRepresentation = largestImageRepresentation(rhsRepresentations.map { $0.representation }) {
|
|
return lhsRepresentation.isSemanticallyEqual(to: rhsRepresentation)
|
|
} else {
|
|
return false
|
|
}
|
|
} else if case let .image(_, rhsRepresentations, _, _, _, _) = self {
|
|
if let lhsRepresentation = largestImageRepresentation(lhsRepresentations.map { $0.representation }),
|
|
let rhsRepresentation = largestImageRepresentation(rhsRepresentations.map { $0.representation }) {
|
|
return lhsRepresentation.isSemanticallyEqual(to: rhsRepresentation)
|
|
} else {
|
|
return false
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var representations: [ImageRepresentationWithReference] {
|
|
switch self {
|
|
case .custom:
|
|
return []
|
|
case let .topImage(representations, _, _):
|
|
return representations
|
|
case let .image(_, representations, _, _, _, _):
|
|
return representations
|
|
}
|
|
}
|
|
|
|
|
|
var videoRepresentations: [VideoRepresentationWithReference] {
|
|
switch self {
|
|
case .custom:
|
|
return []
|
|
case let .topImage(_, videoRepresentations, _):
|
|
return videoRepresentations
|
|
case let .image(_, _, videoRepresentations, _, _, _):
|
|
return videoRepresentations
|
|
}
|
|
}
|
|
|
|
var isFallback: Bool {
|
|
switch self {
|
|
case .custom, .topImage:
|
|
return false
|
|
case let .image(_, _, _, _, isFallback, _):
|
|
return isFallback
|
|
}
|
|
}
|
|
|
|
var emojiMarkup: TelegramMediaImage.EmojiMarkup? {
|
|
switch self {
|
|
case let .image(_, _, _, _, _, emojiMarkup):
|
|
return emojiMarkup
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public init?(entry: AvatarGalleryEntry) {
|
|
switch entry {
|
|
case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _):
|
|
self = .topImage(representations, videoRepresentations, immediateThumbnailData)
|
|
case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _, isFallback, emojiMarkup):
|
|
if representations.isEmpty {
|
|
return nil
|
|
}
|
|
self = .image(reference, representations, videoRepresentations, immediateThumbnailData, isFallback, emojiMarkup)
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class PeerInfoAvatarListItemNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
private let peer: EnginePeer
|
|
public let imageNode: TransformImageNode
|
|
private var videoNode: UniversalVideoNode?
|
|
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
|
|
|
|
private var playerStatus: MediaPlayerStatus?
|
|
private var isLoading = Promise<Bool>(false)
|
|
private var loadingProgress = Promise<Float?>(nil)
|
|
private var progress: Signal<Float?, NoError>?
|
|
private var loadingProgressDisposable = MetaDisposable()
|
|
private var hasProgress = false
|
|
|
|
public let isReady = Promise<Bool>()
|
|
private var didSetReady: Bool = false
|
|
|
|
public var item: PeerInfoAvatarListItem?
|
|
|
|
private var statusPromise = Promise<(MediaPlayerStatus?, Double?)?>()
|
|
var mediaStatus: Signal<(MediaPlayerStatus?, Double?)?, NoError> {
|
|
get {
|
|
return self.statusPromise.get()
|
|
}
|
|
}
|
|
|
|
var delayCentralityLose = false
|
|
var isCentral: Bool? = nil {
|
|
didSet {
|
|
guard self.isCentral != oldValue, let isCentral = self.isCentral else {
|
|
return
|
|
}
|
|
if isCentral {
|
|
self.setupVideoPlayback()
|
|
self.preloadDisposable.set(nil)
|
|
} else {
|
|
if let videoNode = self.videoNode {
|
|
self.playbackStartDisposable.set(nil)
|
|
self.statusPromise.set(.single(nil))
|
|
self.videoNode = nil
|
|
if self.delayCentralityLose {
|
|
Queue.mainQueue().after(0.5) {
|
|
videoNode.removeFromSupernode()
|
|
}
|
|
} else {
|
|
videoNode.removeFromSupernode()
|
|
}
|
|
}
|
|
if let videoContent = self.videoContent {
|
|
let duration: Double = (self.videoStartTimestamp ?? 0.0) + 4.0
|
|
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)
|
|
}
|
|
}
|
|
|
|
init(context: AccountContext, peer: EnginePeer) {
|
|
self.context = context
|
|
self.peer = peer
|
|
self.imageNode = TransformImageNode()
|
|
|
|
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(rgb: 0x000000, alpha: 0.3))
|
|
self.statusNode.isUserInteractionEnabled = false
|
|
|
|
super.init()
|
|
|
|
self.clipsToBounds = true
|
|
|
|
self.imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
|
|
self.addSubnode(self.imageNode)
|
|
self.addSubnode(self.statusNode)
|
|
|
|
self.loadingProgressDisposable.set((combineLatest(self.isLoading.get()
|
|
|> mapToSignal { value -> Signal<Bool, NoError> in
|
|
if value {
|
|
return .single(value) |> delay(0.5, queue: Queue.mainQueue())
|
|
} else {
|
|
return .single(value)
|
|
}
|
|
} |> distinctUntilChanged, self.loadingProgress.get() |> distinctUntilChanged)).start(next: { [weak self] isLoading, progress in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if isLoading, let progress = progress {
|
|
strongSelf.hasProgress = true
|
|
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(max(0.027, progress)), cancelEnabled: false, animateRotation: true), completion: {})
|
|
} else if strongSelf.hasProgress {
|
|
strongSelf.hasProgress = false
|
|
strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: false, animateRotation: true), completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !strongSelf.hasProgress {
|
|
Queue.mainQueue().after(0.3) {
|
|
strongSelf.statusNode.transitionToState(.none, completion: {})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}))
|
|
}
|
|
|
|
deinit {
|
|
self.statusDisposable.dispose()
|
|
self.playbackStartDisposable.dispose()
|
|
self.preloadDisposable.dispose()
|
|
}
|
|
|
|
private func updateStatus() {
|
|
guard let videoContent = self.videoContent else {
|
|
return
|
|
}
|
|
|
|
var bufferingProgress: Float?
|
|
if isMediaStreamable(resource: videoContent.fileReference.media.resource) {
|
|
if let playerStatus = self.playerStatus {
|
|
if case let .buffering(_, _, progress, _) = playerStatus.status {
|
|
bufferingProgress = progress
|
|
} else if case .playing = playerStatus.status {
|
|
bufferingProgress = nil
|
|
}
|
|
} else {
|
|
bufferingProgress = nil
|
|
}
|
|
}
|
|
|
|
if self.progress == nil {
|
|
self.loadingProgress.set(.single(bufferingProgress))
|
|
self.isLoading.set(.single(bufferingProgress != nil))
|
|
}
|
|
}
|
|
|
|
public func updateTransitionFraction(_ fraction: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
if let videoNode = self.videoNode {
|
|
if case .immediate = transition, fraction == 1.0 {
|
|
return
|
|
}
|
|
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() {
|
|
guard let videoContent = self.videoContent, let isCentral = self.isCentral, isCentral, self.videoNode == nil else {
|
|
return
|
|
}
|
|
|
|
let mediaManager = self.context.sharedContext.mediaManager
|
|
let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .secondaryOverlay)
|
|
videoNode.isUserInteractionEnabled = false
|
|
videoNode.canAttachContent = true
|
|
videoNode.isHidden = true
|
|
|
|
if let videoStartTimestamp = self.videoStartTimestamp {
|
|
self.playbackStartDisposable.set((videoNode.status
|
|
|> castError(Bool.self)
|
|
|> mapToSignal { status -> Signal<Bool, Bool> in
|
|
if let status = status, case .playing = status.status {
|
|
if videoStartTimestamp > 0.0 && videoStartTimestamp > status.duration - 1.0 {
|
|
return .fail(true)
|
|
}
|
|
return .single(true)
|
|
} else {
|
|
return .single(false)
|
|
}
|
|
}
|
|
|> filter { playing in
|
|
return playing
|
|
}
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(error: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
if let _ = strongSelf.videoNode {
|
|
videoNode.seek(0.0)
|
|
Queue.mainQueue().after(0.1) {
|
|
strongSelf.videoNode?.layer.allowsGroupOpacity = true
|
|
strongSelf.videoNode?.alpha = 0.0
|
|
strongSelf.videoNode?.isHidden = false
|
|
|
|
strongSelf.videoNode?.alpha = 1.0
|
|
strongSelf.videoNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.01)
|
|
}
|
|
}
|
|
}
|
|
}, completed: { [weak self] in
|
|
if let strongSelf = self {
|
|
Queue.mainQueue().after(0.1) {
|
|
strongSelf.videoNode?.isHidden = false
|
|
}
|
|
}
|
|
}))
|
|
} else {
|
|
self.playbackStartDisposable.set(nil)
|
|
videoNode.isHidden = false
|
|
}
|
|
videoNode.play()
|
|
|
|
self.videoNode = videoNode
|
|
let videoStartTimestamp = self.videoStartTimestamp
|
|
self.statusPromise.set(videoNode.status |> map { ($0, videoStartTimestamp) })
|
|
|
|
self.statusDisposable.set((self.mediaStatus
|
|
|> deliverOnMainQueue).start(next: { [weak self] mediaStatus in
|
|
if let strongSelf = self {
|
|
if let mediaStatusAndStartTimestamp = mediaStatus {
|
|
strongSelf.playerStatus = mediaStatusAndStartTimestamp.0
|
|
}
|
|
strongSelf.updateStatus()
|
|
}
|
|
}))
|
|
|
|
self.insertSubnode(videoNode, belowSubnode: self.statusNode)
|
|
|
|
self.isReady.set(videoNode.ready |> map { return true })
|
|
}
|
|
|
|
func setup(item: PeerInfoAvatarListItem, isMain: Bool, progress: Signal<Float?, NoError>? = nil, synchronous: Bool, fullSizeOnly: Bool = false) {
|
|
let previousItem = self.item
|
|
self.item = item
|
|
self.progress = progress
|
|
|
|
var fullSizeOnly = fullSizeOnly
|
|
if let previousItem = previousItem, previousItem.isSemanticallyEqual(to: item) && self.didSetReady && isMain {
|
|
fullSizeOnly = true
|
|
}
|
|
|
|
if let progress = progress {
|
|
self.loadingProgress.set((progress
|
|
|> beforeNext { [weak self] next in
|
|
self?.isLoading.set(.single(next != nil))
|
|
}))
|
|
}
|
|
|
|
let representations: [ImageRepresentationWithReference]
|
|
let videoRepresentations: [VideoRepresentationWithReference]
|
|
let immediateThumbnailData: Data?
|
|
var id: Int64
|
|
let markup: TelegramMediaImage.EmojiMarkup?
|
|
switch item {
|
|
case let .custom(node):
|
|
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
|
|
if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource {
|
|
id = id &+ resource.photoId
|
|
}
|
|
markup = nil
|
|
case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue):
|
|
representations = imageRepresentations
|
|
videoRepresentations = videoRepresentationsValue
|
|
immediateThumbnailData = immediateThumbnail
|
|
if case let .cloud(imageId, _, _) = reference {
|
|
id = imageId
|
|
} 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 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._asPeer()) {
|
|
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(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: [], preloadSize: 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)
|
|
|
|
if videoContent.id != self.videoContent?.id {
|
|
self.videoContent = videoContent
|
|
self.videoStartTimestamp = video.representation.startTimestamp
|
|
self.setupVideoPlayback()
|
|
}
|
|
} else {
|
|
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)
|
|
|
|
self.imageNode.imageUpdated = { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !strongSelf.didSetReady {
|
|
strongSelf.didSetReady = true
|
|
strongSelf.isReady.set(.single(true))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
let imageSize = CGSize(width: min(size.width, size.height), height: min(size.width, size.height))
|
|
let makeLayout = self.imageNode.asyncLayout()
|
|
let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))
|
|
let _ = applyLayout()
|
|
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)
|
|
transition.updateFrame(node: self.imageNode, frame: imageFrame)
|
|
|
|
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: floor((size.width - 50.0) / 2.0), y: floor((size.height - 50.0) / 2.0)), size: CGSize(width: 50.0, height: 50.0)))
|
|
|
|
if let videoNode = self.videoNode {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
private let fadeWidth: CGFloat = 70.0
|
|
|
|
public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
private let isSettings: Bool
|
|
public var peer: EnginePeer?
|
|
|
|
public let controlsContainerNode: ASDisplayNode
|
|
public let controlsClippingNode: ASDisplayNode
|
|
public let controlsClippingOffsetNode: ASDisplayNode
|
|
public let topShadowNode: ASImageNode
|
|
public let bottomShadowNode: ASImageNode
|
|
|
|
public let contentNode: ASDisplayNode
|
|
let leftHighlightNode: ASDisplayNode
|
|
let rightHighlightNode: ASDisplayNode
|
|
var highlightedSide: Bool?
|
|
public let stripContainerNode: ASDisplayNode
|
|
public let highlightContainerNode: ASDisplayNode
|
|
public let setByYouNode: ImmediateTextNode
|
|
private let setByYouImageNode: ImageNode
|
|
private var setByYouTapRecognizer: UITapGestureRecognizer?
|
|
|
|
public private(set) var galleryEntries: [AvatarGalleryEntry] = []
|
|
private var items: [PeerInfoAvatarListItem] = []
|
|
private var itemNodes: [EngineMediaResource.Id: PeerInfoAvatarListItemNode] = [:]
|
|
private var stripNodes: [ASImageNode] = []
|
|
private var activeStripNode: ASImageNode
|
|
private var loadingStripNode: PeerInfoAvatarListLoadingStripNode
|
|
private let activeStripImage: UIImage
|
|
private var appliedStripNodeCurrentIndex: Int?
|
|
var currentIndex: Int = 0
|
|
private var transitionFraction: CGFloat = 0.0
|
|
|
|
private var validLayout: CGSize?
|
|
public var isCollapsing = false
|
|
private var isExpanded = false
|
|
|
|
public var firstFullSizeOnly = false
|
|
public var customCenterTapAction: (() -> Void)?
|
|
|
|
private let disposable = MetaDisposable()
|
|
private let positionDisposable = MetaDisposable()
|
|
private var initializedList = false
|
|
private var ignoreNextProfilePhotoUpdate = false
|
|
public var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
|
|
public var currentIndexUpdated: (() -> Void)?
|
|
|
|
public let isReady = Promise<Bool>()
|
|
private var didSetReady = false
|
|
|
|
public var currentItemNode: PeerInfoAvatarListItemNode? {
|
|
if self.currentIndex >= 0 && self.currentIndex < self.items.count {
|
|
return self.itemNodes[self.items[self.currentIndex].id]
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public var currentEntry: AvatarGalleryEntry? {
|
|
if self.currentIndex >= 0 && self.currentIndex < self.galleryEntries.count {
|
|
return self.galleryEntries[self.currentIndex]
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private var playerUpdateTimer: SwiftSignalKit.Timer?
|
|
private var playerStatus: (MediaPlayerStatus?, Double?)? {
|
|
didSet {
|
|
if self.playerStatus?.0 != oldValue?.0 || self.playerStatus?.1 != oldValue?.1 {
|
|
if let (playerStatus, _) = self.playerStatus, let status = playerStatus, case .playing = status.status {
|
|
self.ensureHasTimer()
|
|
} else {
|
|
self.stopTimer()
|
|
}
|
|
self.updateStatus()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func ensureHasTimer() {
|
|
if self.playerUpdateTimer == nil {
|
|
let timer = SwiftSignalKit.Timer(timeout: 0.016, repeat: true, completion: { [weak self] in
|
|
self?.updateStatus()
|
|
}, queue: Queue.mainQueue())
|
|
self.playerUpdateTimer = timer
|
|
timer.start()
|
|
}
|
|
}
|
|
|
|
private var playbackProgress: CGFloat?
|
|
private var loading: Bool = false
|
|
private func updateStatus() {
|
|
var position: CGFloat = 1.0
|
|
var loading = false
|
|
if let (status, videoStartTimestamp) = self.playerStatus, let playerStatus = status {
|
|
var playerPosition: Double
|
|
if case .buffering = playerStatus.status {
|
|
loading = true
|
|
}
|
|
if !playerStatus.generationTimestamp.isZero, case .playing = playerStatus.status {
|
|
playerPosition = playerStatus.timestamp + (CACurrentMediaTime() - playerStatus.generationTimestamp)
|
|
} else {
|
|
playerPosition = playerStatus.timestamp
|
|
}
|
|
|
|
if let videoStartTimestamp = videoStartTimestamp, false {
|
|
playerPosition -= videoStartTimestamp
|
|
if playerPosition < 0.0 {
|
|
playerPosition = playerStatus.duration + playerPosition
|
|
}
|
|
}
|
|
|
|
if playerStatus.duration.isZero {
|
|
position = 0.0
|
|
} else {
|
|
position = CGFloat(playerPosition / playerStatus.duration)
|
|
}
|
|
} else {
|
|
self.playbackProgress = nil
|
|
}
|
|
|
|
if let size = self.validLayout {
|
|
self.playbackProgress = position
|
|
self.loading = loading
|
|
self.updateStrips(size: size, itemsAdded: false, stripTransition: .animated(duration: 0.3, curve: .spring))
|
|
}
|
|
}
|
|
|
|
private func stopTimer() {
|
|
self.playerUpdateTimer?.invalidate()
|
|
self.playerUpdateTimer = nil
|
|
}
|
|
|
|
public init(context: AccountContext, isSettings: Bool = false) {
|
|
self.context = context
|
|
self.isSettings = isSettings
|
|
|
|
self.contentNode = ASDisplayNode()
|
|
|
|
self.leftHighlightNode = ASDisplayNode()
|
|
self.leftHighlightNode.displaysAsynchronously = false
|
|
self.leftHighlightNode.backgroundColor = generateImage(CGSize(width: fadeWidth, height: 24.0), contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
|
|
let topColor = UIColor(rgb: 0x000000, alpha: 0.1)
|
|
let bottomColor = UIColor(rgb: 0x000000, alpha: 0.0)
|
|
|
|
var locations: [CGFloat] = [0.0, 1.0]
|
|
let colors: [CGColor] = [topColor.cgColor, bottomColor.cgColor]
|
|
|
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
|
|
|
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
|
|
}).flatMap { UIColor(patternImage: $0) }
|
|
self.leftHighlightNode.alpha = 0.0
|
|
|
|
self.rightHighlightNode = ASDisplayNode()
|
|
self.rightHighlightNode.displaysAsynchronously = false
|
|
self.rightHighlightNode.backgroundColor = generateImage(CGSize(width: fadeWidth, height: 24.0), contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
|
|
let topColor = UIColor(rgb: 0x000000, alpha: 0.1)
|
|
let bottomColor = UIColor(rgb: 0x000000, alpha: 0.0)
|
|
|
|
var locations: [CGFloat] = [0.0, 1.0]
|
|
let colors: [CGColor] = [topColor.cgColor, bottomColor.cgColor]
|
|
|
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
|
|
|
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: 0.0), end: CGPoint(x: 0.0, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
|
|
}).flatMap { UIColor(patternImage: $0) }
|
|
self.rightHighlightNode.alpha = 0.0
|
|
|
|
self.stripContainerNode = ASDisplayNode()
|
|
self.contentNode.addSubnode(self.stripContainerNode)
|
|
self.activeStripImage = generateSmallHorizontalStretchableFilledCircleImage(diameter: 2.0, color: .white)!
|
|
|
|
self.activeStripNode = ASImageNode()
|
|
self.activeStripNode.image = self.activeStripImage
|
|
|
|
self.loadingStripNode = PeerInfoAvatarListLoadingStripNode()
|
|
self.loadingStripNode.imageNode.image = self.activeStripImage
|
|
|
|
self.highlightContainerNode = ASDisplayNode()
|
|
self.highlightContainerNode.addSubnode(self.leftHighlightNode)
|
|
self.highlightContainerNode.addSubnode(self.rightHighlightNode)
|
|
|
|
self.setByYouNode = ImmediateTextNode()
|
|
self.setByYouNode.alpha = 0.0
|
|
self.setByYouNode.isUserInteractionEnabled = false
|
|
|
|
self.setByYouImageNode = ImageNode()
|
|
self.setByYouImageNode.alpha = 0.0
|
|
self.setByYouImageNode.isUserInteractionEnabled = false
|
|
|
|
self.controlsContainerNode = ASDisplayNode()
|
|
self.controlsContainerNode.isUserInteractionEnabled = false
|
|
|
|
self.controlsClippingOffsetNode = ASDisplayNode()
|
|
|
|
self.controlsClippingNode = ASDisplayNode()
|
|
self.controlsClippingNode.isUserInteractionEnabled = false
|
|
self.controlsClippingNode.clipsToBounds = true
|
|
|
|
self.topShadowNode = ASImageNode()
|
|
self.topShadowNode.displaysAsynchronously = false
|
|
self.topShadowNode.displayWithoutProcessing = true
|
|
self.topShadowNode.contentMode = .scaleToFill
|
|
|
|
self.bottomShadowNode = ASImageNode()
|
|
self.bottomShadowNode.displaysAsynchronously = false
|
|
self.bottomShadowNode.displayWithoutProcessing = true
|
|
self.bottomShadowNode.contentMode = .scaleToFill
|
|
|
|
do {
|
|
let size = CGSize(width: 88.0, height: 88.0)
|
|
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
|
if let context = UIGraphicsGetCurrentContext() {
|
|
context.clip(to: CGRect(origin: CGPoint(), size: size))
|
|
|
|
let topColor = UIColor(rgb: 0x000000, alpha: 0.4)
|
|
let bottomColor = UIColor(rgb: 0x000000, alpha: 0.0)
|
|
|
|
var locations: [CGFloat] = [0.0, 1.0]
|
|
let colors: [CGColor] = [topColor.cgColor, bottomColor.cgColor]
|
|
|
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
|
|
|
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
|
|
|
|
let image = UIGraphicsGetImageFromCurrentImageContext()
|
|
UIGraphicsEndImageContext()
|
|
if let image = image {
|
|
self.topShadowNode.image = generateImage(image.size, contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.rotate(by: -CGFloat.pi / 2.0)
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
|
|
})
|
|
self.bottomShadowNode.image = generateImage(image.size, contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.rotate(by: CGFloat.pi / 2.0)
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = .black
|
|
|
|
self.addSubnode(self.contentNode)
|
|
|
|
self.controlsContainerNode.addSubnode(self.highlightContainerNode)
|
|
self.controlsContainerNode.addSubnode(self.topShadowNode)
|
|
self.addSubnode(self.bottomShadowNode)
|
|
self.controlsContainerNode.addSubnode(self.stripContainerNode)
|
|
self.controlsClippingNode.addSubnode(self.controlsContainerNode)
|
|
self.controlsClippingOffsetNode.addSubnode(self.controlsClippingNode)
|
|
self.stripContainerNode.addSubnode(self.setByYouNode)
|
|
self.stripContainerNode.addSubnode(self.setByYouImageNode)
|
|
|
|
self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return false
|
|
}
|
|
return strongSelf.currentIndex != 0
|
|
}
|
|
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
|
|
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
|
recognizer.tapActionAtPoint = { _ in
|
|
return .keepWithSingleTap
|
|
}
|
|
recognizer.highlight = { [weak self] point in
|
|
guard let strongSelf = self, let size = strongSelf.validLayout else {
|
|
return
|
|
}
|
|
var highlightedSide: Bool?
|
|
if let point = point {
|
|
if point.x < size.width * 1.0 / 5.0 {
|
|
if strongSelf.items.count > 1 {
|
|
highlightedSide = false
|
|
}
|
|
} else {
|
|
if strongSelf.items.count > 1 {
|
|
highlightedSide = true
|
|
}
|
|
}
|
|
}
|
|
if strongSelf.highlightedSide != highlightedSide {
|
|
strongSelf.highlightedSide = highlightedSide
|
|
let leftAlpha: CGFloat
|
|
let rightAlpha: CGFloat
|
|
if let highlightedSide = highlightedSide {
|
|
leftAlpha = highlightedSide ? 0.0 : 1.0
|
|
rightAlpha = highlightedSide ? 1.0 : 0.0
|
|
} else {
|
|
leftAlpha = 0.0
|
|
rightAlpha = 0.0
|
|
}
|
|
if strongSelf.leftHighlightNode.alpha != leftAlpha {
|
|
strongSelf.leftHighlightNode.alpha = leftAlpha
|
|
if leftAlpha.isZero {
|
|
strongSelf.leftHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring)
|
|
} else {
|
|
strongSelf.leftHighlightNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08)
|
|
}
|
|
}
|
|
if strongSelf.rightHighlightNode.alpha != rightAlpha {
|
|
strongSelf.rightHighlightNode.alpha = rightAlpha
|
|
if rightAlpha.isZero {
|
|
strongSelf.rightHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring)
|
|
} else {
|
|
strongSelf.rightHighlightNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.view.addGestureRecognizer(recognizer)
|
|
}
|
|
|
|
deinit {
|
|
self.disposable.dispose()
|
|
self.positionDisposable.dispose()
|
|
}
|
|
|
|
public override func didLoad() {
|
|
super.didLoad()
|
|
|
|
let setByYouTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.setByYouTapped))
|
|
self.setByYouNode.isUserInteractionEnabled = true
|
|
self.setByYouNode.view.addGestureRecognizer(setByYouTapRecognizer)
|
|
self.setByYouTapRecognizer = setByYouTapRecognizer
|
|
}
|
|
|
|
@objc private func setByYouTapped() {
|
|
self.selectLastItem()
|
|
}
|
|
|
|
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
public func selectFirstItem() {
|
|
let previousIndex = self.currentIndex
|
|
self.currentIndex = 0
|
|
if self.currentIndex != previousIndex {
|
|
self.currentIndexUpdated?()
|
|
}
|
|
if let size = self.validLayout {
|
|
self.updateItems(size: size, transition: .immediate, stripTransition: .immediate)
|
|
}
|
|
}
|
|
|
|
public func selectLastItem() {
|
|
let previousIndex = self.currentIndex
|
|
self.currentIndex = self.items.count - 1
|
|
if self.currentIndex != previousIndex {
|
|
self.currentIndexUpdated?()
|
|
}
|
|
if let size = self.validLayout {
|
|
self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true)
|
|
}
|
|
}
|
|
|
|
public func updateEntryIsHidden(entry: AvatarGalleryEntry?) {
|
|
if let entry = entry, let index = self.galleryEntries.firstIndex(of: entry) {
|
|
self.currentItemNode?.isHidden = index == self.currentIndex
|
|
} else {
|
|
self.currentItemNode?.isHidden = false
|
|
}
|
|
}
|
|
|
|
public var offsetLocation = false
|
|
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .ended:
|
|
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
|
if let size = self.validLayout, case .tap = gesture {
|
|
var location = location
|
|
if self.offsetLocation {
|
|
location.x += size.width / 2.0
|
|
}
|
|
if location.x < size.width * 1.0 / 5.0 {
|
|
if self.currentIndex != 0 {
|
|
let previousIndex = self.currentIndex
|
|
self.currentIndex -= 1
|
|
if self.currentIndex != previousIndex {
|
|
self.currentIndexUpdated?()
|
|
}
|
|
self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true)
|
|
} else if self.items.count > 1 {
|
|
let previousIndex = self.currentIndex
|
|
self.currentIndex = self.items.count - 1
|
|
if self.currentIndex != previousIndex {
|
|
self.currentIndexUpdated?()
|
|
}
|
|
self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true)
|
|
}
|
|
} else {
|
|
if let customAction = self.customCenterTapAction, location.x < size.width - size.width * 1.0 / 5.0 {
|
|
customAction()
|
|
return
|
|
}
|
|
if self.currentIndex < self.items.count - 1 {
|
|
let previousIndex = self.currentIndex
|
|
self.currentIndex += 1
|
|
if self.currentIndex != previousIndex {
|
|
self.currentIndexUpdated?()
|
|
}
|
|
self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true)
|
|
} else if self.items.count > 1 {
|
|
let previousIndex = self.currentIndex
|
|
self.currentIndex = 0
|
|
if self.currentIndex != previousIndex {
|
|
self.currentIndexUpdated?()
|
|
}
|
|
self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private var pageChangedByPan = false
|
|
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .changed:
|
|
let translation = recognizer.translation(in: self.view)
|
|
var transitionFraction = translation.x / self.bounds.width
|
|
if self.currentIndex <= 0 {
|
|
transitionFraction = min(0.0, transitionFraction)
|
|
}
|
|
if self.currentIndex >= self.items.count - 1 {
|
|
transitionFraction = max(0.0, transitionFraction)
|
|
}
|
|
self.transitionFraction = transitionFraction
|
|
if let size = self.validLayout {
|
|
self.updateItems(size: size, transition: .animated(duration: 0.3, curve: .spring), stripTransition: .animated(duration: 0.3, curve: .spring))
|
|
}
|
|
case .cancelled, .ended:
|
|
let velocity = recognizer.velocity(in: self.view)
|
|
var directionIsToRight: Bool?
|
|
if abs(velocity.x) > 10.0 {
|
|
directionIsToRight = velocity.x < 0.0
|
|
} else if abs(self.transitionFraction) > 0.5 {
|
|
directionIsToRight = self.transitionFraction < 0.0
|
|
}
|
|
var updatedIndex = self.currentIndex
|
|
if let directionIsToRight = directionIsToRight {
|
|
if directionIsToRight {
|
|
updatedIndex = min(updatedIndex + 1, self.items.count - 1)
|
|
} else {
|
|
updatedIndex = max(updatedIndex - 1, 0)
|
|
}
|
|
}
|
|
let previousIndex = self.currentIndex
|
|
self.currentIndex = updatedIndex
|
|
if self.currentIndex != previousIndex {
|
|
self.pageChangedByPan = true
|
|
self.currentIndexUpdated?()
|
|
}
|
|
self.transitionFraction = 0.0
|
|
if let size = self.validLayout {
|
|
self.updateItems(size: size, transition: .animated(duration: 0.3, curve: .spring), stripTransition: .animated(duration: 0.3, curve: .spring))
|
|
self.pageChangedByPan = false
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func setMainItem(_ item: PeerInfoAvatarListItem) {
|
|
guard case let .image(imageReference, _, _, _, _, _) = item else {
|
|
return
|
|
}
|
|
var items: [PeerInfoAvatarListItem] = []
|
|
var entries: [AvatarGalleryEntry] = []
|
|
for entry in self.galleryEntries {
|
|
switch entry {
|
|
case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _):
|
|
entries.append(entry)
|
|
items.append(.topImage(representations, videoRepresentations, immediateThumbnailData))
|
|
case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _, isFallback, emojiMarkup):
|
|
if representations.isEmpty {
|
|
continue
|
|
}
|
|
if imageReference == reference {
|
|
entries.insert(entry, at: 0)
|
|
items.insert(.image(reference, representations, videoRepresentations, immediateThumbnailData, isFallback, emojiMarkup), at: 0)
|
|
} else {
|
|
entries.append(entry)
|
|
items.append(.image(reference, representations, videoRepresentations, immediateThumbnailData, isFallback, emojiMarkup))
|
|
}
|
|
}
|
|
}
|
|
self.galleryEntries = normalizeEntries(entries)
|
|
self.items = items
|
|
self.itemsUpdated?(items)
|
|
let previousIndex = self.currentIndex
|
|
self.currentIndex = 0
|
|
if self.currentIndex != previousIndex {
|
|
self.currentIndexUpdated?()
|
|
}
|
|
self.ignoreNextProfilePhotoUpdate = true
|
|
if let size = self.validLayout {
|
|
self.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: true)
|
|
}
|
|
}
|
|
|
|
public func deleteItem(_ item: PeerInfoAvatarListItem) -> Bool {
|
|
guard case let .image(imageReference, _, _, _, _, _) = item else {
|
|
return false
|
|
}
|
|
|
|
var items: [PeerInfoAvatarListItem] = []
|
|
var entries: [AvatarGalleryEntry] = []
|
|
let previousIndex = self.currentIndex
|
|
|
|
var index = 0
|
|
var deletedIndex: Int?
|
|
for entry in self.galleryEntries {
|
|
switch entry {
|
|
case let .topImage(representations, videoRepresentations, _, _, immediateThumbnailData, _):
|
|
entries.append(entry)
|
|
items.append(.topImage(representations, videoRepresentations, immediateThumbnailData))
|
|
case let .image(_, reference, representations, videoRepresentations, _, _, _, _, immediateThumbnailData, _, isFallback, emojiMarkup):
|
|
if representations.isEmpty {
|
|
continue
|
|
}
|
|
if imageReference != reference {
|
|
entries.append(entry)
|
|
items.append(.image(reference, representations, videoRepresentations, immediateThumbnailData, isFallback, emojiMarkup))
|
|
} else {
|
|
deletedIndex = index
|
|
}
|
|
}
|
|
index += 1
|
|
}
|
|
|
|
switch self.peer {
|
|
case .legacyGroup, .channel:
|
|
if deletedIndex == 0 {
|
|
self.galleryEntries = []
|
|
self.items = []
|
|
self.itemsUpdated?([])
|
|
self.currentIndex = 0
|
|
if let size = self.validLayout {
|
|
self.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: true)
|
|
}
|
|
return true
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
self.galleryEntries = normalizeEntries(entries)
|
|
self.items = items
|
|
self.itemsUpdated?(items)
|
|
self.currentIndex = max(0, previousIndex - 1)
|
|
if self.currentIndex != previousIndex {
|
|
self.currentIndexUpdated?()
|
|
}
|
|
self.ignoreNextProfilePhotoUpdate = true
|
|
if let size = self.validLayout {
|
|
self.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: true)
|
|
}
|
|
|
|
return items.count == 0
|
|
}
|
|
|
|
private var additionalEntryProgress: Signal<Float?, NoError>? = nil
|
|
public func update(size: CGSize, peer: EnginePeer?, customNode: ASDisplayNode? = nil, additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError> = .single(nil), isExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = size
|
|
let previousExpanded = self.isExpanded
|
|
self.isExpanded = isExpanded
|
|
if !isExpanded && previousExpanded {
|
|
self.isCollapsing = true
|
|
}
|
|
self.leftHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: fadeWidth, height: size.height))
|
|
self.rightHighlightNode.frame = CGRect(origin: CGPoint(x: size.width - fadeWidth, y: 0.0), size: CGSize(width: fadeWidth, height: size.height))
|
|
|
|
if let peer = peer, !self.initializedList {
|
|
self.initializedList = true
|
|
|
|
let entry = additionalEntry
|
|
|> map { representation -> AvatarGalleryEntry? in
|
|
return representation.flatMap { AvatarGalleryEntry(representation: $0.0, peer: peer) }
|
|
}
|
|
|
|
self.disposable.set(combineLatest(queue: Queue.mainQueue(), peerInfoProfilePhotosWithCache(context: self.context, peerId: peer.id), entry).start(next: { [weak self] completeAndEntries, entry in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
var (complete, entries) = completeAndEntries
|
|
|
|
if strongSelf.galleryEntries.count > 1, entries.count == 1 && !complete {
|
|
return
|
|
}
|
|
|
|
var synchronous = false
|
|
if !strongSelf.galleryEntries.isEmpty, let updated = entries.first, case let .image(mediaId, reference, _, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption, _, emojiMarkup) = updated, !videoRepresentations.isEmpty, let previous = strongSelf.galleryEntries.first, case let .topImage(representations, _, _, _, _, _) = previous {
|
|
let firstEntry = AvatarGalleryEntry.image(mediaId, reference, representations, videoRepresentations, peer, index, indexData, messageId, thumbnailData, caption, false, emojiMarkup)
|
|
entries.remove(at: 0)
|
|
entries.insert(firstEntry, at: 0)
|
|
synchronous = true
|
|
}
|
|
|
|
if let entry = entry {
|
|
entries.insert(entry, at: 0)
|
|
|
|
strongSelf.additionalEntryProgress = additionalEntry
|
|
|> map { value -> Float? in
|
|
return value?.1
|
|
}
|
|
}
|
|
|
|
if strongSelf.ignoreNextProfilePhotoUpdate {
|
|
if entries.count == 1, let first = entries.first, case .topImage = first {
|
|
return
|
|
} else {
|
|
strongSelf.ignoreNextProfilePhotoUpdate = false
|
|
synchronous = true
|
|
}
|
|
}
|
|
|
|
var items: [PeerInfoAvatarListItem] = []
|
|
if let customNode = customNode {
|
|
items.append(.custom(customNode))
|
|
}
|
|
for entry in entries {
|
|
if let item = PeerInfoAvatarListItem(entry: entry) {
|
|
items.append(item)
|
|
}
|
|
}
|
|
strongSelf.galleryEntries = entries
|
|
strongSelf.items = items
|
|
strongSelf.itemsUpdated?(items)
|
|
if let size = strongSelf.validLayout {
|
|
strongSelf.updateItems(size: size, update: true, transition: .immediate, stripTransition: .immediate, synchronous: synchronous)
|
|
}
|
|
if items.isEmpty {
|
|
if !strongSelf.didSetReady {
|
|
strongSelf.didSetReady = true
|
|
strongSelf.isReady.set(.single(true))
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
self.updateItems(size: size, transition: transition, stripTransition: transition)
|
|
}
|
|
|
|
private func updateStrips(size: CGSize, itemsAdded: Bool, stripTransition: ContainedViewLayoutTransition) {
|
|
let hadOneStripNode = self.stripNodes.count == 1
|
|
if self.stripNodes.count != self.items.count {
|
|
if self.stripNodes.count < self.items.count {
|
|
for _ in 0 ..< self.items.count - self.stripNodes.count {
|
|
let stripNode = ASImageNode()
|
|
stripNode.displaysAsynchronously = false
|
|
stripNode.displayWithoutProcessing = true
|
|
stripNode.image = self.activeStripImage
|
|
stripNode.alpha = 0.2
|
|
self.stripNodes.append(stripNode)
|
|
self.stripContainerNode.addSubnode(stripNode)
|
|
}
|
|
} else {
|
|
for i in (self.items.count ..< self.stripNodes.count).reversed() {
|
|
self.stripNodes[i].removeFromSupernode()
|
|
self.stripNodes.remove(at: i)
|
|
}
|
|
}
|
|
self.stripContainerNode.addSubnode(self.activeStripNode)
|
|
self.stripContainerNode.addSubnode(self.loadingStripNode)
|
|
}
|
|
if self.appliedStripNodeCurrentIndex != self.currentIndex || itemsAdded {
|
|
if !self.itemNodes.isEmpty {
|
|
self.appliedStripNodeCurrentIndex = self.currentIndex
|
|
}
|
|
|
|
if let currentItemNode = self.currentItemNode {
|
|
self.positionDisposable.set((currentItemNode.mediaStatus
|
|
|> deliverOnMainQueue).start(next: { [weak self] statusAndVideoStartTimestamp in
|
|
if let strongSelf = self {
|
|
strongSelf.playerStatus = statusAndVideoStartTimestamp
|
|
}
|
|
}))
|
|
} else {
|
|
self.positionDisposable.set(nil)
|
|
}
|
|
}
|
|
if hadOneStripNode && self.stripNodes.count > 1 {
|
|
self.stripContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
|
}
|
|
let stripInset: CGFloat = 8.0
|
|
let stripSpacing: CGFloat = 4.0
|
|
let stripWidth: CGFloat = max(5.0, floorToScreenPixels((size.width - stripInset * 2.0 - stripSpacing * CGFloat(self.stripNodes.count - 1)) / CGFloat(self.stripNodes.count)))
|
|
let currentStripMinX = stripInset + CGFloat(self.currentIndex) * (stripWidth + stripSpacing)
|
|
let currentStripMidX = floorToScreenPixels(currentStripMinX + stripWidth / 2.0)
|
|
let lastStripMaxX = stripInset + CGFloat(self.stripNodes.count - 1) * (stripWidth + stripSpacing) + stripWidth
|
|
let stripOffset: CGFloat = min(0.0, max(size.width - stripInset - lastStripMaxX, size.width / 2.0 - currentStripMidX))
|
|
for i in 0 ..< self.stripNodes.count {
|
|
let stripX: CGFloat = stripInset + CGFloat(i) * (stripWidth + stripSpacing)
|
|
if i == 0 && self.stripNodes.count == 1 {
|
|
self.stripNodes[i].isHidden = true
|
|
} else {
|
|
self.stripNodes[i].isHidden = false
|
|
}
|
|
let stripFrame = CGRect(origin: CGPoint(x: stripOffset + stripX, y: 0.0), size: CGSize(width: stripWidth + 1.0, height: 2.0))
|
|
stripTransition.updateFrame(node: self.stripNodes[i], frame: stripFrame)
|
|
}
|
|
|
|
if self.currentIndex >= 0 && self.currentIndex < self.stripNodes.count {
|
|
var frame = self.stripNodes[self.currentIndex].frame
|
|
stripTransition.updateFrame(node: self.loadingStripNode, frame: frame)
|
|
if let playbackProgress = self.playbackProgress {
|
|
frame.size.width = max(frame.size.height, frame.size.width * playbackProgress)
|
|
}
|
|
stripTransition.updateFrameAdditive(node: self.activeStripNode, frame: frame)
|
|
stripTransition.updateAlpha(node: self.activeStripNode, alpha: self.loading ? 0.0 : 1.0)
|
|
stripTransition.updateAlpha(node: self.loadingStripNode, alpha: self.loading ? 1.0 : 0.0)
|
|
|
|
self.activeStripNode.isHidden = self.stripNodes.count < 2
|
|
self.loadingStripNode.isHidden = self.stripNodes.count < 2 || !self.loading
|
|
}
|
|
}
|
|
|
|
public var updateCustomItemsOnlySynchronously = false
|
|
|
|
private func updateItems(size: CGSize, update: Bool = false, transition: ContainedViewLayoutTransition, stripTransition: ContainedViewLayoutTransition, synchronous: Bool = false) {
|
|
var validIds: [EngineMediaResource.Id] = []
|
|
var addedItemNodesForAdditiveTransition: [PeerInfoAvatarListItemNode] = []
|
|
var additiveTransitionOffset: CGFloat = 0.0
|
|
var itemsAdded = false
|
|
if self.currentIndex >= 0 && self.currentIndex < self.items.count {
|
|
let preloadSpan: Int = 2
|
|
for i in max(0, self.currentIndex - preloadSpan) ... min(self.currentIndex + preloadSpan, self.items.count - 1) {
|
|
if self.items[i].representations.isEmpty {
|
|
continue
|
|
}
|
|
validIds.append(self.items[i].id)
|
|
var itemNode: PeerInfoAvatarListItemNode?
|
|
var wasAdded = false
|
|
if let current = self.itemNodes[self.items[i].id] {
|
|
itemNode = current
|
|
if update {
|
|
var synchronous = synchronous && i == self.currentIndex
|
|
if case .custom = self.items[i], self.updateCustomItemsOnlySynchronously {
|
|
synchronous = true
|
|
}
|
|
current.setup(item: self.items[i], isMain: i == 0, synchronous: synchronous && i == self.currentIndex, fullSizeOnly: self.firstFullSizeOnly && i == 0)
|
|
}
|
|
} else if let peer = self.peer {
|
|
wasAdded = true
|
|
let addedItemNode = PeerInfoAvatarListItemNode(context: self.context, peer: peer)
|
|
itemNode = addedItemNode
|
|
addedItemNode.setup(item: self.items[i], isMain: i == 0, progress: i == 0 ? self.additionalEntryProgress : nil, synchronous: (i == 0 && i == self.currentIndex) || (synchronous && i == self.currentIndex), fullSizeOnly: self.firstFullSizeOnly && i == 0)
|
|
self.itemNodes[self.items[i].id] = addedItemNode
|
|
self.contentNode.addSubnode(addedItemNode)
|
|
}
|
|
if let itemNode = itemNode {
|
|
itemNode.delayCentralityLose = self.pageChangedByPan
|
|
itemNode.isCentral = i == self.currentIndex
|
|
itemNode.delayCentralityLose = false
|
|
|
|
let indexOffset = CGFloat(i - self.currentIndex)
|
|
let itemFrame = CGRect(origin: CGPoint(x: indexOffset * size.width + self.transitionFraction * size.width - size.width / 2.0, y: -size.height / 2.0), size: size)
|
|
|
|
if wasAdded {
|
|
itemsAdded = true
|
|
addedItemNodesForAdditiveTransition.append(itemNode)
|
|
itemNode.frame = itemFrame
|
|
itemNode.update(size: size, transition: .immediate)
|
|
} else {
|
|
additiveTransitionOffset = itemNode.frame.minX - itemFrame.minX
|
|
transition.updateFrame(node: itemNode, frame: itemFrame)
|
|
itemNode.update(size: size, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !self.items.isEmpty, self.currentIndex >= 0 && self.currentIndex < self.items.count {
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
let currentItem = self.items[self.currentIndex]
|
|
|
|
var photoTitle: String?
|
|
var hasLink = false
|
|
var fallbackImageSignal: Signal<UIImage?, NoError>?
|
|
if let representation = currentItem.representations.first?.representation, representation.isPersonal {
|
|
photoTitle = representation.hasVideo ? presentationData.strings.UserInfo_CustomVideo : presentationData.strings.UserInfo_CustomPhoto
|
|
} else if currentItem.isFallback, let representation = currentItem.representations.first?.representation, self.isSettings {
|
|
photoTitle = representation.hasVideo ? presentationData.strings.UserInfo_PublicVideo : presentationData.strings.UserInfo_PublicPhoto
|
|
} else if self.currentIndex == 0, let lastItem = self.items.last, lastItem.isFallback, let representation = lastItem.representations.first?.representation, self.isSettings {
|
|
photoTitle = representation.hasVideo ? presentationData.strings.UserInfo_PublicVideo : presentationData.strings.UserInfo_PublicPhoto
|
|
hasLink = true
|
|
if let peer = self.peer {
|
|
fallbackImageSignal = peerAvatarCompleteImage(account: self.context.account, peer: peer, forceProvidedRepresentation: true, representation: representation, size: CGSize(width: 28.0, height: 28.0))
|
|
}
|
|
}
|
|
|
|
if let photoTitle = photoTitle {
|
|
transition.updateAlpha(node: self.setByYouNode, alpha: 0.7)
|
|
self.setByYouNode.attributedText = NSAttributedString(string: photoTitle, font: Font.regular(12.0), textColor: UIColor.white)
|
|
let setByYouSize = self.setByYouNode.updateLayout(size)
|
|
self.setByYouNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - setByYouSize.width) / 2.0), y: 17.0), size: setByYouSize)
|
|
self.setByYouNode.isUserInteractionEnabled = hasLink
|
|
} else {
|
|
transition.updateAlpha(node: self.setByYouNode, alpha: 0.0)
|
|
self.setByYouNode.isUserInteractionEnabled = false
|
|
}
|
|
|
|
if let fallbackImageSignal = fallbackImageSignal {
|
|
self.setByYouImageNode.setSignal(fallbackImageSignal)
|
|
transition.updateAlpha(node: self.setByYouImageNode, alpha: 1.0)
|
|
self.setByYouImageNode.frame = CGRect(origin: CGPoint(x: self.setByYouNode.frame.minX - 32.0, y: 11.0), size: CGSize(width: 28.0, height: 28.0))
|
|
} else {
|
|
transition.updateAlpha(node: self.setByYouImageNode, alpha: 0.0)
|
|
}
|
|
}
|
|
|
|
for itemNode in addedItemNodesForAdditiveTransition {
|
|
transition.animatePositionAdditive(node: itemNode, offset: CGPoint(x: additiveTransitionOffset, y: 0.0))
|
|
}
|
|
var removeIds: [EngineMediaResource.Id] = []
|
|
for (id, _) in self.itemNodes {
|
|
if !validIds.contains(id) {
|
|
removeIds.append(id)
|
|
}
|
|
}
|
|
for id in removeIds {
|
|
if let itemNode = self.itemNodes.removeValue(forKey: id) {
|
|
itemNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
self.updateStrips(size: size, itemsAdded: itemsAdded, stripTransition: stripTransition)
|
|
|
|
if let item = self.items.first, let itemNode = self.itemNodes[item.id] {
|
|
if !self.didSetReady {
|
|
self.didSetReady = true
|
|
self.isReady.set(itemNode.isReady.get())
|
|
}
|
|
}
|
|
}
|
|
}
|