Swiftgram/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift
2025-05-21 18:35:07 +03:00

1853 lines
101 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import TelegramStringFormatting
import AccountContext
import RadialStatusNode
import SemanticStatusNode
import PhotoResources
import MusicAlbumArtResources
import UniversalMediaPlayer
import ContextUI
import FileMediaResourceStatus
import ManagedAnimationNode
import ShimmerEffect
import ComponentFlow
import EmojiStatusComponent
private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:])
private let redColors: (UInt32, UInt32) = (0xed6b7b, 0xe63f45)
private let greenColors: (UInt32, UInt32) = (0x99de6f, 0x5fb84f)
private let blueColors: (UInt32, UInt32) = (0x72d5fd, 0x2a9ef1)
private let yellowColors: (UInt32, UInt32) = (0xffa24b, 0xed705c)
private let extensionColorsMap: [String: (UInt32, UInt32)] = [
"ppt": redColors,
"pptx": redColors,
"pdf": redColors,
"key": redColors,
"xls": greenColors,
"xlsx": greenColors,
"csv": greenColors,
"zip": yellowColors,
"rar": yellowColors,
"gzip": yellowColors,
"ai": yellowColors
]
private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.saveGState()
context.beginPath()
let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ")
context.clip()
let gradientColors = [UIColor(rgb: colors.0).cgColor, UIColor(rgb: colors.1).cgColor] as CFArray
var locations: [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
context.restoreGState()
context.beginPath()
let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ")
context.clip()
context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.2).cgColor)
context.translateBy(x: 40.0 - 14.0, y: 0.0)
let _ = try? drawSvgPath(context, path: "M-1,0 L14,0 L14,15 L14,14 C14,12.8954305 13.1045695,12 12,12 L4,12 C2.8954305,12 2,11.1045695 2,10 L2,2 C2,0.8954305 1.1045695,-2.02906125e-16 0,0 L-1,0 L-1,0 Z ")
})
}
private func extensionImage(fileExtension: String?) -> UIImage? {
let colors: (UInt32, UInt32)
if let fileExtension = fileExtension {
if let extensionColors = extensionColorsMap[fileExtension] {
colors = extensionColors
} else {
colors = blueColors
}
} else {
colors = blueColors
}
if let cachedImage = (extensionImageCache.with { dict in
return dict[colors.0]
}) {
return cachedImage
} else if let image = generateExtensionImage(colors: colors) {
let _ = extensionImageCache.modify { dict in
var dict = dict
dict[colors.0] = image
return dict
}
return image
} else {
return nil
}
}
private let extensionFont = Font.with(size: 15.0, design: .round, weight: .bold)
private let mediumExtensionFont = Font.with(size: 14.0, design: .round, weight: .bold)
private let smallExtensionFont = Font.with(size: 12.0, design: .round, weight: .bold)
private struct FetchControls {
let fetch: () -> Void
let cancel: () -> Void
}
private enum FileIconImage: Equatable {
case imageRepresentation(Media, TelegramMediaImageRepresentation)
case albumArt(TelegramMediaFile, SharedMediaPlaybackAlbumArt)
case roundVideo(TelegramMediaFile)
static func ==(lhs: FileIconImage, rhs: FileIconImage) -> Bool {
switch lhs {
case let .imageRepresentation(lhsMedia, lhsValue):
if case let .imageRepresentation(rhsMedia, rhsValue) = rhs, lhsMedia.isEqual(to: rhsMedia), lhsValue == rhsValue {
return true
} else {
return false
}
case let .albumArt(file, value):
if case .albumArt(file, value) = rhs {
return true
} else {
return false
}
case let .roundVideo(file):
if case .roundVideo(file) = rhs {
return true
} else {
return false
}
}
}
}
final class CachedChatListSearchResult {
let text: String
let searchQuery: String
let resultRanges: [Range<String.Index>]
init(text: String, searchQuery: String, resultRanges: [Range<String.Index>]) {
self.text = text
self.searchQuery = searchQuery
self.resultRanges = resultRanges
}
func matches(text: String, searchQuery: String) -> Bool {
if self.text != text {
return false
}
if self.searchQuery != searchQuery {
return false
}
return true
}
}
private func selectStoryMedia(item: Stories.Item, preferredHighQuality: Bool) -> Media? {
if !preferredHighQuality, let alternativeMediaValue = item.alternativeMediaList.first {
return alternativeMediaValue
} else {
return item.media
}
}
public final class ListMessageFileItemNode: ListMessageNode {
public final class DescriptionNode: ASDisplayNode {
let descriptionNode: TextNode
var titleTopicArrowNode: ASImageNode?
var topicTitleNode: TextNode?
var titleTopicIconView: ComponentHostView<Empty>?
var titleTopicIconComponent: EmojiStatusComponent?
var visibilityStatus: Bool = false {
didSet {
if self.visibilityStatus != oldValue {
if let titleTopicIconView = self.titleTopicIconView, let titleTopicIconComponent = self.titleTopicIconComponent {
let _ = titleTopicIconView.update(
transition: .immediate,
component: AnyComponent(titleTopicIconComponent.withVisibleForAnimations(self.visibilityStatus)),
environment: {},
containerSize: titleTopicIconView.bounds.size
)
}
}
}
}
override init() {
self.descriptionNode = TextNode()
self.descriptionNode.displaysAsynchronously = true
super.init()
self.addSubnode(self.descriptionNode)
}
func asyncLayout() -> (_ context: AccountContext, _ constrainedWidth: CGFloat, _ theme: PresentationTheme, _ authorTitle: NSAttributedString?, _ topic: (title: NSAttributedString, showIcon: Bool, iconId: Int64?, iconColor: Int32)?) -> (CGSize, () -> Void) {
let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
let makeTopicTitleLayout = TextNode.asyncLayout(self.topicTitleNode)
return { [weak self] context, constrainedWidth, theme, authorTitle, topic in
var maxTitleWidth = constrainedWidth
if let _ = topic {
maxTitleWidth = floor(constrainedWidth * 0.7)
}
let descriptionLayout = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: authorTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)))
var remainingWidth = constrainedWidth - descriptionLayout.0.size.width
var topicTitleArguments: TextNodeLayoutArguments?
var arrowIconImage: UIImage?
if let topic = topic {
remainingWidth -= 22.0 + 2.0
if authorTitle != nil {
arrowIconImage = PresentationResourcesItemList.topicArrowDescriptionIcon(theme)
if let arrowIconImage = arrowIconImage {
remainingWidth -= arrowIconImage.size.width + 6.0 * 2.0
}
}
topicTitleArguments = TextNodeLayoutArguments(attributedString: topic.title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: remainingWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))
}
let topicTitleLayout = topicTitleArguments.flatMap(makeTopicTitleLayout)
var size = descriptionLayout.0.size
if let topicTitleLayout = topicTitleLayout {
size.height = max(size.height, topicTitleLayout.0.size.height)
size.width += 10.0 + topicTitleLayout.0.size.width
}
return (size, {
guard let self else {
return
}
let _ = descriptionLayout.1()
let authorFrame = CGRect(origin: CGPoint(), size: descriptionLayout.0.size)
self.descriptionNode.frame = authorFrame
var nextX = authorFrame.maxX - 1.0
if authorTitle == nil {
nextX = 0.0
}
if let arrowIconImage = arrowIconImage {
let titleTopicArrowNode: ASImageNode
if let current = self.titleTopicArrowNode {
titleTopicArrowNode = current
} else {
titleTopicArrowNode = ASImageNode()
self.titleTopicArrowNode = titleTopicArrowNode
self.addSubnode(titleTopicArrowNode)
}
titleTopicArrowNode.image = arrowIconImage
nextX += 6.0
titleTopicArrowNode.frame = CGRect(origin: CGPoint(x: nextX, y: 5.0), size: arrowIconImage.size)
nextX += arrowIconImage.size.width + 6.0
} else {
if let titleTopicArrowNode = self.titleTopicArrowNode {
self.titleTopicArrowNode = nil
titleTopicArrowNode.removeFromSupernode()
}
}
if let topic, topic.showIcon {
let titleTopicIconView: ComponentHostView<Empty>
if let current = self.titleTopicIconView {
titleTopicIconView = current
} else {
titleTopicIconView = ComponentHostView<Empty>()
self.titleTopicIconView = titleTopicIconView
self.view.addSubview(titleTopicIconView)
}
let titleTopicIconContent: EmojiStatusComponent.Content
if let fileId = topic.iconId, fileId != 0 {
titleTopicIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 36.0, height: 36.0), placeholderColor: theme.list.mediaPlaceholderColor, themeColor: theme.list.itemAccentColor, loopMode: .count(2))
} else {
titleTopicIconContent = .topic(title: String(topic.title.string.prefix(1)), color: topic.iconColor, size: CGSize(width: 22.0, height: 22.0))
}
let titleTopicIconComponent = EmojiStatusComponent(
context: context,
animationCache: context.animationCache,
animationRenderer: context.animationRenderer,
content: titleTopicIconContent,
isVisibleForAnimations: self.visibilityStatus,
action: nil
)
self.titleTopicIconComponent = titleTopicIconComponent
let iconSize = titleTopicIconView.update(
transition: .immediate,
component: AnyComponent(titleTopicIconComponent),
environment: {},
containerSize: CGSize(width: 22.0, height: 22.0)
)
titleTopicIconView.frame = CGRect(origin: CGPoint(x: nextX, y: UIScreenPixel), size: iconSize)
nextX += iconSize.width + 2.0
} else {
if let titleTopicIconView = self.titleTopicIconView {
self.titleTopicIconView = nil
titleTopicIconView.removeFromSuperview()
}
}
if let topicTitleLayout = topicTitleLayout {
let topicTitleNode = topicTitleLayout.1()
if topicTitleNode.supernode == nil {
self.addSubnode(topicTitleNode)
self.topicTitleNode = topicTitleNode
}
topicTitleNode.frame = CGRect(origin: CGPoint(x: nextX - 1.0, y: 0.0), size: topicTitleLayout.0.size)
} else if let topicTitleNode = self.topicTitleNode {
self.topicTitleNode = nil
topicTitleNode.removeFromSupernode()
}
})
}
}
}
private let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
private let extractedBackgroundImageNode: ASImageNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private let offsetContainerNode: ASDisplayNode
private var backgroundNode: ASDisplayNode?
private let highlightedBackgroundNode: ASDisplayNode
public let separatorNode: ASDisplayNode
private let maskNode: ASImageNode
private var selectionNode: ItemListSelectableControlNode?
public let titleNode: DescriptionNode
public let textNode: TextNode
public let descriptionNode: DescriptionNode
private let descriptionProgressNode: ImmediateTextNode
public let dateNode: TextNode
public let extensionIconNode: ASImageNode
private let extensionIconText: TextNode
public let iconImageNode: TransformImageNode
private let iconStatusNode: SemanticStatusNode
private let restrictionNode: ASDisplayNode
private var currentIconImage: FileIconImage?
public var currentMedia: Media?
private let statusDisposable = MetaDisposable()
private let fetchControls = Atomic<FetchControls?>(value: nil)
private var fetchStatus: MediaResourceStatus?
private var resourceStatus: FileMediaResourceMediaStatus?
private let fetchDisposable = MetaDisposable()
private let playbackStatusDisposable = MetaDisposable()
private let playbackStatus = Promise<MediaPlayerStatus>()
private var downloadStatusIconNode: DownloadIconNode?
private var linearProgressNode: LinearProgressNode?
private var placeholderNode: ShimmerEffectNode?
private var absoluteLocation: (CGRect, CGSize)?
private var context: AccountContext?
private(set) var message: Message?
private var appliedItem: ListMessageItem?
private var layoutParams: ListViewItemLayoutParams?
private var contentSizeValue: CGSize?
private var currentLeftOffset: CGFloat = 0.0
private var currentIsRestricted = false
private var cachedSearchResult: CachedChatListSearchResult?
public override var visibility: ListViewItemNodeVisibility {
didSet {
let wasVisible = self.visibilityStatus
let isVisible: Bool
switch self.visibility {
case let .visible(fraction, _):
isVisible = fraction > 0.2
case .none:
isVisible = false
}
if wasVisible != isVisible {
self.visibilityStatus = isVisible
}
}
}
private var visibilityStatus: Bool = false {
didSet {
if self.visibilityStatus != oldValue {
self.descriptionNode.visibilityStatus = self.visibilityStatus
}
}
}
public required init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.separatorNode = ASDisplayNode()
self.separatorNode.displaysAsynchronously = false
self.separatorNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.offsetContainerNode = ASDisplayNode()
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.titleNode = DescriptionNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.isUserInteractionEnabled = false
self.textNode = TextNode()
self.textNode.displaysAsynchronously = false
self.textNode.isUserInteractionEnabled = false
self.descriptionNode = DescriptionNode()
self.descriptionNode.displaysAsynchronously = false
self.descriptionNode.isUserInteractionEnabled = false
self.descriptionProgressNode = ImmediateTextNode()
self.descriptionProgressNode.displaysAsynchronously = false
self.descriptionProgressNode.isUserInteractionEnabled = false
self.descriptionProgressNode.maximumNumberOfLines = 1
self.dateNode = TextNode()
self.dateNode.isUserInteractionEnabled = false
self.extensionIconNode = ASImageNode()
self.extensionIconNode.isLayerBacked = true
self.extensionIconNode.displaysAsynchronously = false
self.extensionIconNode.displayWithoutProcessing = true
self.extensionIconText = TextNode()
self.extensionIconText.displaysAsynchronously = false
self.extensionIconText.isUserInteractionEnabled = false
self.iconImageNode = TransformImageNode()
self.iconImageNode.displaysAsynchronously = false
self.iconImageNode.contentAnimations = .subsequentUpdates
self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
self.iconStatusNode.isUserInteractionEnabled = false
self.restrictionNode = ASDisplayNode()
self.restrictionNode.isHidden = true
super.init()
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
self.offsetContainerNode.addSubnode(self.titleNode)
self.offsetContainerNode.addSubnode(self.textNode)
self.offsetContainerNode.addSubnode(self.descriptionNode)
self.offsetContainerNode.addSubnode(self.descriptionProgressNode)
self.offsetContainerNode.addSubnode(self.dateNode)
self.offsetContainerNode.addSubnode(self.extensionIconNode)
self.offsetContainerNode.addSubnode(self.extensionIconText)
self.offsetContainerNode.addSubnode(self.iconStatusNode)
self.addSubnode(self.restrictionNode)
self.addSubnode(self.separatorNode)
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item, let message = item.message else {
return
}
cancelParentGestures(view: strongSelf.view)
item.interaction.openMessageContextMenu(message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.theme.list.plainBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0))
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
transition.updateAlpha(node: strongSelf.dateNode, alpha: isExtracted ? 0.0 : 1.0)
}
}
deinit {
self.statusDisposable.dispose()
self.fetchDisposable.dispose()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupItem(_ item: ListMessageItem) {
self.item = item
}
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = item as? ListMessageItem {
let doLayout = self.asyncLayout()
let merged = (top: false, bottom: false, dateAtBottom: item.getDateAtBottom(top: previousItem, bottom: nextItem))
let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom)
self.contentSize = layout.contentSize
self.insets = layout.insets
apply(.None)
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
super.animateInsertion(currentTimestamp, duration: duration, options: options)
self.transitionOffset = self.bounds.size.height * 1.6
self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
var rect = rect
rect.origin.y += self.insets.top
self.absoluteLocation = (rect, containerSize)
if let shimmerNode = self.placeholderNode {
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
override public func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let titleNodeMakeLayout = self.titleNode.asyncLayout()
let textNodeMakeLayout = TextNode.asyncLayout(self.textNode)
let descriptionNodeMakeLayout = self.descriptionNode.asyncLayout()
let extensionIconTextMakeLayout = TextNode.asyncLayout(self.extensionIconText)
let dateNodeMakeLayout = TextNode.asyncLayout(self.dateNode)
let iconImageLayout = self.iconImageNode.asyncLayout()
let currentMedia = self.currentMedia
let currentMessage = self.message
let currentIconImage = self.currentIconImage
let currentSearchResult = self.cachedSearchResult
let currentItem = self.appliedItem
let selectionNodeLayout = ItemListSelectableControlNode.asyncLayout(self.selectionNode)
return { [weak self] item, params, mergedTop, mergedBottom, dateHeaderAtBottom in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme.theme !== item.presentationData.theme.theme {
updatedTheme = item.presentationData.theme.theme
}
let titleFont = Font.semibold(floor(item.presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
let audioTitleFont = Font.semibold(floor(item.presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
let descriptionFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0))
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let leftInset: CGFloat = 65.0 + params.leftInset
let rightInset: CGFloat = 8.0 + params.rightInset
var leftOffset: CGFloat = 0.0
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
if case let .selectable(selected) = item.selection {
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, .regular)
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
leftOffset += selectionWidth
}
var extensionIconImage: UIImage?
var titleText: NSAttributedString?
var descriptionText: NSAttributedString?
var extensionText: NSAttributedString?
var iconImage: FileIconImage?
var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var updatedStatusSignal: Signal<FileMediaResourceStatus, NoError>?
var updatedPlaybackStatusSignal: Signal<MediaPlayerStatus, NoError>?
var updatedFetchControls: FetchControls?
var isAudio = false
var isVoice = false
var isInstantVideo = false
var isRestricted = false
let message = item.message
var titleExtraData: (title: NSAttributedString, showIcon: Bool, iconId: Int64?, iconColor: Int32)? = nil
var descriptionExtraData: (title: NSAttributedString, showIcon: Bool, iconId: Int64?, iconColor: Int32)? = nil
var globalAuthorTitle: String?
var selectedMedia: Media?
if let message = message {
var effectiveMessageMedia = message.media
for media in message.media {
if let storyMedia = media as? TelegramMediaStory {
if let story = message.associatedStories[storyMedia.storyId], !story.data.isEmpty, case let .item(storyItem) = story.get(Stories.StoredItem.self), let media = selectStoryMedia(item: storyItem, preferredHighQuality: item.interaction.preferredStoryHighQuality) {
effectiveMessageMedia = [media]
break
}
}
}
for media in effectiveMessageMedia {
if let file = media as? TelegramMediaFile {
selectedMedia = file
isInstantVideo = file.isInstantVideo
for attribute in file.attributes {
if case let .Audio(voice, duration, title, performer, _) = attribute {
isAudio = true
isVoice = voice
titleText = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
var descriptionString: String
if let performer = performer {
if item.isGlobalSearchResult || item.isDownloadList {
descriptionString = performer
} else {
descriptionString = "\(stringForDuration(Int32(duration)))\(performer)"
}
} else if let size = file.size {
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
} else {
descriptionString = ""
}
if item.isGlobalSearchResult || item.isDownloadList {
let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if authorString.count > 1 {
globalAuthorTitle = authorString.last ?? ""
}
if descriptionString.isEmpty {
descriptionString = authorString.first ?? ""
} else {
descriptionString = "\(descriptionString)\(authorString.first ?? "")"
}
}
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
if !voice {
if file.fileName?.lowercased().hasSuffix(".ogg") == true {
iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: "", performer: "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: "", performer: "", isThumbnail: false)))
} else {
iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(message), media: file), title: title ?? "", performer: performer ?? "", isThumbnail: false)))
}
} else {
titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
descriptionText = NSAttributedString(string: message.author.flatMap(EnginePeer.init)?.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
}
}
}
if isInstantVideo || isVoice {
var authorName: String
if let author = message.forwardInfo?.author {
if author.id == item.context.account.peerId {
authorName = item.presentationData.strings.DialogList_You
} else {
authorName = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
} else if let signature = message.forwardInfo?.authorSignature {
authorName = signature
} else if let author = message.author {
if author.id == item.context.account.peerId {
authorName = item.presentationData.strings.DialogList_You
} else {
authorName = EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
} else {
authorName = " "
}
if item.isGlobalSearchResult || item.isDownloadList {
let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if authorString.count > 1 {
globalAuthorTitle = authorString.last ?? ""
}
authorName = authorString.first ?? ""
}
titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
let dateString = stringForFullDate(timestamp: message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
var descriptionString: String = ""
if let duration = file.duration {
if item.isGlobalSearchResult || item.isDownloadList || !item.displayFileInfo {
descriptionString = stringForDuration(Int32(duration))
} else {
descriptionString = "\(stringForDuration(Int32(duration)))\(dateString)"
}
} else {
if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = dateString
}
}
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
iconImage = .roundVideo(file)
} else if !isAudio {
var fileName: String = file.fileName ?? "File"
if file.isVideo {
fileName = item.presentationData.strings.Message_Video
}
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
var fileExtension: String?
if let range = fileName.range(of: ".", options: [.backwards]) {
fileExtension = fileName[range.upperBound...].lowercased()
}
extensionIconImage = extensionImage(fileExtension: fileExtension)
if let fileExtension = fileExtension {
extensionText = NSAttributedString(string: fileExtension, font: fileExtension.count > 3 ? mediumExtensionFont : extensionFont, textColor: UIColor.white)
}
if let representation = smallestImageRepresentation(file.previewRepresentations) {
iconImage = .imageRepresentation(file, representation)
}
let dateString = stringForFullDate(timestamp: message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
var descriptionString: String = ""
if let size = file.size {
if item.isGlobalSearchResult || item.isDownloadList || !item.displayFileInfo {
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
} else {
descriptionString = "\(dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))\(dateString)"
}
} else {
if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = "\(dateString)"
}
}
if item.isGlobalSearchResult || item.isDownloadList {
let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if authorString.count > 1 {
globalAuthorTitle = authorString.last ?? ""
}
if descriptionString.isEmpty {
descriptionString = authorString.first ?? ""
} else {
descriptionString = "\(descriptionString)\(authorString.first ?? "")"
}
}
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
}
break
} else if let image = media as? TelegramMediaImage {
selectedMedia = image
let fileName: String = item.presentationData.strings.Message_Photo
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
if let representation = smallestImageRepresentation(image.representations) {
iconImage = .imageRepresentation(image, representation)
}
let dateString = stringForFullDate(timestamp: message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
var descriptionString: String = ""
if !(item.isGlobalSearchResult || item.isDownloadList) {
descriptionString = "\(dateString)"
}
if item.isGlobalSearchResult || item.isDownloadList {
let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
if authorString.count > 1 {
globalAuthorTitle = authorString.last ?? ""
}
if descriptionString.isEmpty {
descriptionString = authorString.first ?? ""
} else {
descriptionString = "\(descriptionString)\(authorString.first ?? "")"
}
}
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
}
}
for attribute in message.attributes {
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }, chatId: message.author?.id.id._internalGetInt64Value()) != nil {
isRestricted = true
break
}
}
} else {
titleText = NSAttributedString(string: " ", font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
descriptionText = NSAttributedString(string: " ", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
}
if let _ = item.message?.threadId, item.message?.id.peerId.namespace == Namespaces.Peer.CloudChannel, let threadInfo = item.message?.associatedThreadInfo {
if isInstantVideo || isVoice {
titleExtraData = (NSAttributedString(string: threadInfo.title, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor), true, threadInfo.icon, threadInfo.iconColor)
} else {
descriptionExtraData = (NSAttributedString(string: threadInfo.title, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor), true, threadInfo.icon, threadInfo.iconColor)
}
} else if let globalAuthorTitle = globalAuthorTitle {
if isInstantVideo || isVoice {
titleExtraData = (NSAttributedString(string: globalAuthorTitle, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor), false, nil, 0)
} else {
descriptionExtraData = (NSAttributedString(string: globalAuthorTitle, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor), false, nil, 0)
}
}
var mediaUpdated = false
if let currentMedia = currentMedia {
if let selectedMedia = selectedMedia {
mediaUpdated = !selectedMedia.isEqual(to: currentMedia)
} else {
mediaUpdated = true
}
} else {
mediaUpdated = selectedMedia != nil
}
var statusUpdated = mediaUpdated
if currentMessage?.id != message?.id || currentMessage?.flags != message?.flags {
statusUpdated = true
}
if let message = message, let selectedMedia = selectedMedia {
if mediaUpdated {
let context = item.context
updatedFetchControls = FetchControls(fetch: { [weak self] in
if let strongSelf = self {
if let file = selectedMedia as? TelegramMediaFile {
strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: true).start())
} else if let image = selectedMedia as? TelegramMediaImage, let representation = image.representations.last {
strongSelf.fetchDisposable.set(messageMediaImageInteractiveFetched(context: context, message: message, image: image, resource: representation.resource, userInitiated: true, storeToDownloadsPeerId: nil).start())
}
}
}, cancel: {
if let file = selectedMedia as? TelegramMediaFile {
if item.isDownloadList {
context.fetchManager.toggleInteractiveFetchPaused(resourceId: file.resource.id.stringRepresentation, isPaused: true)
} else {
messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file)
}
} else if let image = selectedMedia as? TelegramMediaImage, let representation = image.representations.last {
if item.isDownloadList {
context.fetchManager.toggleInteractiveFetchPaused(resourceId: representation.resource.id.stringRepresentation, isPaused: true)
} else {
messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: image, resource: representation.resource)
}
}
})
}
if statusUpdated && item.displayFileInfo {
if let file = selectedMedia as? TelegramMediaFile {
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
if case .Fetching = value.fetchStatus, !item.isDownloadList {
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
} else {
return .single(value)
}
}
if isAudio || isInstantVideo {
if let currentUpdatedStatusSignal = updatedStatusSignal {
updatedStatusSignal = currentUpdatedStatusSignal
|> map { status in
switch status.mediaStatus {
case .fetchStatus:
if item.isDownloadList {
return FileMediaResourceStatus(mediaStatus: .fetchStatus(status.fetchStatus), fetchStatus: status.fetchStatus)
} else {
return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus)
}
case .playbackStatus:
return status
}
}
}
}
if isVoice {
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
}
} else if let image = selectedMedia as? TelegramMediaImage {
updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
if case .Fetching = value.fetchStatus, !item.isDownloadList {
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
} else {
return .single(value)
}
}
}
}
}
var chatListSearchResult: CachedChatListSearchResult?
let messageText = foldLineBreaks(item.message?.text ?? "")
if let searchQuery = item.interaction.searchTextHighightState {
if let cached = currentSearchResult, cached.matches(text: messageText, searchQuery: searchQuery) {
chatListSearchResult = cached
} else {
let (ranges, text) = findSubstringRanges(in: messageText, query: searchQuery)
chatListSearchResult = CachedChatListSearchResult(text: text, searchQuery: searchQuery, resultRanges: ranges)
}
} else {
chatListSearchResult = nil
}
var captionText: NSMutableAttributedString?
if let chatListSearchResult = chatListSearchResult, let firstRange = chatListSearchResult.resultRanges.first {
var text = NSMutableAttributedString(string: messageText, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
for range in chatListSearchResult.resultRanges {
let stringRange = NSRange(range, in: chatListSearchResult.text)
if stringRange.location >= 0 && stringRange.location + stringRange.length <= text.length {
text.addAttribute(.foregroundColor, value: item.presentationData.theme.theme.chatList.messageHighlightedTextColor, range: stringRange)
}
}
let firstRangeOrigin = chatListSearchResult.text.distance(from: chatListSearchResult.text.startIndex, to: firstRange.lowerBound)
if firstRangeOrigin > 24 {
var leftOrigin: Int = 0
(text.string as NSString).enumerateSubstrings(in: NSMakeRange(0, firstRangeOrigin), options: [.byWords, .reverse]) { (str, range1, _, _) in
let distanceFromEnd = firstRangeOrigin - range1.location
if (distanceFromEnd > 12 || range1.location == 0) && leftOrigin == 0 {
leftOrigin = range1.location
}
}
text = text.attributedSubstring(from: NSMakeRange(leftOrigin, text.length - leftOrigin)).mutableCopy() as! NSMutableAttributedString
text.insert(NSAttributedString(string: "\u{2026}", attributes: [NSAttributedString.Key.font: descriptionFont, NSAttributedString.Key.foregroundColor: item.presentationData.theme.theme.list.itemSecondaryTextColor]), at: 0)
}
captionText = text
}
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message?.timestamp ?? 0, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat)
let dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(item.context, params.width - leftInset - leftOffset - rightInset - dateNodeLayout.size.width - 4.0, item.presentationData.theme.theme, titleText, titleExtraData)
let (textNodeLayout, textNodeApply) = textNodeMakeLayout(TextNodeLayoutArguments(attributedString: captionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(item.context, params.width - leftInset - rightInset - 30.0, item.presentationData.theme.theme, descriptionText, descriptionExtraData)
var (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
if extensionTextLayout.truncated, let text = extensionText?.string {
extensionText = NSAttributedString(string: text, font: smallExtensionFont, textColor: .white, paragraphAlignment: .center)
(extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
var iconImageApply: (() -> Void)?
if let iconImage = iconImage {
switch iconImage {
case let .imageRepresentation(_, representation):
let iconSize = CGSize(width: 40.0, height: 40.0)
let imageCorners = ImageCorners(radius: 6.0)
let arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor)
iconImageApply = iconImageLayout(arguments)
case .albumArt:
let iconSize = CGSize(width: 40.0, height: 40.0)
let imageCorners = ImageCorners(radius: iconSize.width / 2.0)
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor)
iconImageApply = iconImageLayout(arguments)
case let .roundVideo(file):
let iconSize = CGSize(width: 40.0, height: 40.0)
let imageCorners = ImageCorners(radius: iconSize.width / 2.0)
let arguments = TransformImageArguments(corners: imageCorners, imageSize: (file.dimensions ?? PixelDimensions(width: 320, height: 320)).cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor)
iconImageApply = iconImageLayout(arguments)
}
}
if let message = message {
if currentIconImage != iconImage {
if let iconImage = iconImage {
switch iconImage {
case let .imageRepresentation(media, representation):
if let file = media as? TelegramMediaFile {
updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, userLocation: .peer(message.id.peerId), mediaReference: FileMediaReference.message(message: MessageReference(message), media: file).abstract, representation: representation)
} else if let image = media as? TelegramMediaImage {
updateIconImageSignal = mediaGridMessagePhoto(account: item.context.account, userLocation: .peer(message.id.peerId), photoReference: ImageMediaReference.message(message: MessageReference(message), media: image))
} else {
updateIconImageSignal = .complete()
}
case let .albumArt(file, albumArt):
updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, engine: item.context.engine, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3), emptyColor: item.presentationData.theme.theme.list.itemAccentColor)
case let .roundVideo(file):
updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, userLocation: .peer(message.id.peerId), videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3))
}
} else {
updateIconImageSignal = .complete()
}
}
}
var insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
if dateHeaderAtBottom, let header = item.header {
insets.top += header.height
}
if !mergedBottom, case .blocks = item.style {
insets.bottom += 35.0
}
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 8.0 * 2.0 + titleNodeLayout.height - 5.0 + descriptionNodeLayout.height + (textNodeLayout.size.height > 0.0 ? textNodeLayout.size.height + 3.0 : 0.0)), insets: insets)
return (nodeLayout, { animation in
if let strongSelf = self {
if strongSelf.downloadStatusIconNode == nil {
strongSelf.downloadStatusIconNode = DownloadIconNode(theme: item.presentationData.theme.theme)
}
let transition: ContainedViewLayoutTransition
if animation.isAnimated && currentItem?.message != nil {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
strongSelf.restrictionNode.isHidden = !isRestricted
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
strongSelf.restrictionNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: nodeLayout.contentSize.width - 16.0, height: nodeLayout.contentSize.height))
let extractedRect = CGRect(origin: CGPoint(), size: nodeLayout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0)
strongSelf.extractedRect = extractedRect
strongSelf.nonExtractedRect = nonExtractedRect
if strongSelf.contextSourceNode.isExtractedToContextPreview {
strongSelf.extractedBackgroundImageNode.frame = extractedRect
} else {
strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect
}
strongSelf.contextSourceNode.contentRect = extractedRect
strongSelf.containerNode.isGestureEnabled = item.displayFileInfo
strongSelf.currentIsRestricted = isRestricted || item.message == nil
strongSelf.currentMedia = selectedMedia
strongSelf.message = message
strongSelf.context = item.context
strongSelf.appliedItem = item
strongSelf.layoutParams = params
strongSelf.contentSizeValue = nodeLayout.contentSize
strongSelf.currentLeftOffset = leftOffset
if let _ = updatedTheme {
if item.displayBackground {
let backgroundNode: ASDisplayNode
if let current = strongSelf.backgroundNode {
backgroundNode = current
} else {
backgroundNode = ASDisplayNode()
strongSelf.backgroundNode = backgroundNode
strongSelf.insertSubnode(backgroundNode, at: 0)
}
backgroundNode.backgroundColor = item.presentationData.theme.theme.list.itemBlocksBackgroundColor
}
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.theme.list.itemPlainSeparatorColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.theme.list.itemHighlightedBackgroundColor
strongSelf.linearProgressNode?.updateTheme(theme: item.presentationData.theme.theme)
strongSelf.restrictionNode.backgroundColor = item.presentationData.theme.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6)
strongSelf.downloadStatusIconNode?.updateTheme(theme: item.presentationData.theme.theme)
}
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
let selectionFrame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: selectionWidth, height: nodeLayout.contentSize.height))
let selectionNode = selectionApply(selectionFrame.size, transition.isAnimated)
if selectionNode !== strongSelf.selectionNode {
strongSelf.selectionNode?.removeFromSupernode()
strongSelf.selectionNode = selectionNode
strongSelf.contextSourceNode.contentNode.addSubnode(selectionNode)
selectionNode.frame = selectionFrame
transition.animatePosition(node: selectionNode, from: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY))
} else {
transition.updateFrame(node: selectionNode, frame: selectionFrame)
}
} else if let selectionNode = strongSelf.selectionNode {
strongSelf.selectionNode = nil
let selectionFrame = selectionNode.frame
transition.updatePosition(node: selectionNode, position: CGPoint(x: -selectionFrame.size.width / 2.0, y: selectionFrame.midY), completion: { [weak selectionNode] _ in
selectionNode?.removeFromSupernode()
})
}
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel)))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - UIScreenPixel), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel - nodeLayout.insets.bottom))
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height - nodeLayout.insets.bottom))
}
switch item.style {
case .plain:
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
case .blocks:
if strongSelf.maskNode.supernode == nil {
strongSelf.addSubnode(strongSelf.maskNode)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
if !mergedTop {
hasTopCorners = true
}
if !mergedBottom {
hasBottomCorners = true
strongSelf.separatorNode.isHidden = hasCorners
} else {
strongSelf.separatorNode.isHidden = false
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
if let backgroundNode = strongSelf.backgroundNode {
strongSelf.maskNode.frame = backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
}
}
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: 7.0), size: titleNodeLayout))
let _ = titleNodeApply()
var descriptionOffset: CGFloat = 0.0
if let resourceStatus = strongSelf.resourceStatus {
switch resourceStatus {
case .playbackStatus:
break
case let .fetchStatus(fetchStatus):
switch fetchStatus {
case .Remote, .Fetching, .Paused:
descriptionOffset = 14.0
case .Local:
break
}
}
}
transition.updateFrame(node: strongSelf.textNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: strongSelf.titleNode.frame.maxY + 1.0), size: textNodeLayout.size))
let _ = textNodeApply()
transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset - 1.0, y: strongSelf.titleNode.frame.maxY - 3.0 + (textNodeLayout.size.height > 0.0 ? textNodeLayout.size.height + 3.0 : 0.0)), size: descriptionNodeLayout))
let _ = descriptionNodeApply()
let _ = dateNodeApply()
transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: params.width - rightInset - dateNodeLayout.size.width, y: 11.0), size: dateNodeLayout.size))
strongSelf.dateNode.isHidden = !item.isGlobalSearchResult
let iconSize = CGSize(width: 40.0, height: 40.0)
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 12.0, y: 8.0), size: iconSize)
transition.updateFrame(node: strongSelf.extensionIconNode, frame: iconFrame)
strongSelf.extensionIconNode.image = extensionIconImage
transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floorToScreenPixels((iconFrame.width - extensionTextLayout.size.width) / 2.0), y: iconFrame.minY + 7.0 + floorToScreenPixels((iconFrame.height - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size))
transition.updateFrame(node: strongSelf.iconStatusNode, frame: iconFrame)
let _ = extensionTextApply()
strongSelf.currentIconImage = iconImage
if let updateIconImageSignal, let iconImage, case .albumArt = iconImage {
strongSelf.iconStatusNode.setBackgroundImage(updateIconImageSignal, size: CGSize(width: 40.0, height: 40.0))
}
if let iconImageApply = iconImageApply {
if let updateImageSignal = updateIconImageSignal {
strongSelf.iconImageNode.setSignal(updateImageSignal)
}
transition.updateFrame(node: strongSelf.iconImageNode, frame: iconFrame)
if strongSelf.iconImageNode.supernode == nil {
strongSelf.offsetContainerNode.insertSubnode(strongSelf.iconImageNode, belowSubnode: strongSelf.iconStatusNode)
}
iconImageApply()
if strongSelf.extensionIconNode.supernode != nil {
strongSelf.extensionIconNode.removeFromSupernode()
}
if strongSelf.extensionIconText.supernode != nil {
strongSelf.extensionIconText.removeFromSupernode()
}
} else if strongSelf.iconImageNode.supernode != nil {
strongSelf.iconImageNode.removeFromSupernode()
if strongSelf.extensionIconNode.supernode == nil {
strongSelf.offsetContainerNode.insertSubnode(strongSelf.extensionIconNode, belowSubnode: strongSelf.iconStatusNode)
}
if strongSelf.extensionIconText.supernode == nil {
strongSelf.offsetContainerNode.insertSubnode(strongSelf.extensionIconText, belowSubnode: strongSelf.iconStatusNode)
}
}
if let updatedStatusSignal = updatedStatusSignal {
strongSelf.statusDisposable.set((updatedStatusSignal
|> deliverOnMainQueue).start(next: { [weak strongSelf] fileStatus in
if let strongSelf = strongSelf {
strongSelf.fetchStatus = fileStatus.fetchStatus._asStatus()
strongSelf.resourceStatus = fileStatus.mediaStatus
strongSelf.updateStatus(transition: .immediate)
}
}))
}
if let downloadStatusIconNode = strongSelf.downloadStatusIconNode {
transition.updateFrame(node: downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 3.0, y: strongSelf.descriptionNode.frame.minY + floorToScreenPixels((strongSelf.descriptionNode.frame.height - 18.0) / 2.0) + UIScreenPixel), size: CGSize(width: 18.0, height: 18.0)))
}
if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
}
if let updatedPlaybackStatusSignal = updatedPlaybackStatusSignal {
strongSelf.playbackStatus.set(updatedPlaybackStatusSignal)
/*strongSelf.playbackStatusDisposable.set((updatedPlaybackStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in
displayLinkDispatcher.dispatch {
if let strongSelf = strongSelf {
strongSelf.playerStatus = status
}
}
}))*/
}
strongSelf.updateStatus(transition: transition)
if item.message == nil {
let shimmerNode: ShimmerEffectNode
if let current = strongSelf.placeholderNode {
shimmerNode = current
} else {
shimmerNode = ShimmerEffectNode()
strongSelf.placeholderNode = shimmerNode
if strongSelf.separatorNode.supernode != nil {
strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.separatorNode)
} else {
strongSelf.addSubnode(shimmerNode)
}
}
shimmerNode.frame = CGRect(origin: CGPoint(), size: nodeLayout.contentSize)
if let (rect, size) = strongSelf.absoluteLocation {
shimmerNode.updateAbsoluteRect(rect, within: size)
}
var shapes: [ShimmerEffectNode.Shape] = []
let titleLineWidth: CGFloat = 120.0
let descriptionLineWidth: CGFloat = 60.0
let lineDiameter: CGFloat = 8.0
let titleFrame = strongSelf.titleNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: titleFrame.minX, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - lineDiameter) / 2.0)), width: titleLineWidth, diameter: lineDiameter))
let descriptionFrame = strongSelf.descriptionNode.frame
shapes.append(.roundedRectLine(startPoint: CGPoint(x: descriptionFrame.minX, y: descriptionFrame.minY + floorToScreenPixels((descriptionFrame.height - lineDiameter) / 2.0)), width: descriptionLineWidth, diameter: lineDiameter))
if let media = selectedMedia as? TelegramMediaFile, media.isInstantVideo {
shapes.append(.circle(iconFrame))
} else {
shapes.append(.roundedRect(rect: iconFrame, cornerRadius: 6.0))
}
shimmerNode.update(backgroundColor: item.presentationData.theme.theme.list.itemBlocksBackgroundColor, foregroundColor: item.presentationData.theme.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: nodeLayout.contentSize)
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
}
})
}
}
private func updateStatus(transition: ContainedViewLayoutTransition) {
guard let item = self.item, let media = self.currentMedia, let _ = self.fetchStatus, let status = self.resourceStatus, let layoutParams = self.layoutParams, let contentSize = self.contentSizeValue else {
return
}
var isAudio = false
var isVoice = false
var isInstantVideo = false
if let file = media as? TelegramMediaFile {
isAudio = file.isMusic || file.isVoice
isVoice = file.isVoice
isInstantVideo = file.isInstantVideo
}
var iconStatusState: SemanticStatusNodeState = .none
var iconStatusBackgroundColor: UIColor = .clear
var iconStatusForegroundColor: UIColor = .white
if isVoice {
iconStatusBackgroundColor = item.presentationData.theme.theme.list.itemAccentColor
iconStatusForegroundColor = item.presentationData.theme.theme.list.itemCheckColors.foregroundColor
} else if isAudio {
iconStatusBackgroundColor = item.presentationData.theme.theme.list.itemAccentColor
iconStatusForegroundColor = item.presentationData.theme.theme.list.itemCheckColors.foregroundColor
}
if !isAudio && !isInstantVideo {
self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate)
} else {
if item.isDownloadList {
self.updateProgressFrame(size: contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate)
}
switch status {
case let .fetchStatus(fetchStatus):
switch fetchStatus {
case let .Fetching(_, progress):
if item.isDownloadList {
iconStatusState = .progress(value: CGFloat(progress), cancelEnabled: true, appearance: nil)
}
case .Local:
if isAudio || isInstantVideo {
iconStatusState = .play
}
case .Remote, .Paused:
if isAudio || isInstantVideo {
iconStatusState = .play
}
}
case let .playbackStatus(playbackStatus):
switch playbackStatus {
case .playing:
iconStatusState = .pause
case .paused:
iconStatusState = .play
}
}
}
self.iconStatusNode.backgroundNodeColor = iconStatusBackgroundColor
self.iconStatusNode.foregroundNodeColor = iconStatusForegroundColor
self.iconStatusNode.overlayForegroundNodeColor = .white
self.iconStatusNode.transitionToState(iconStatusState)
}
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted, let item = self.item, case .none = item.selection {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
if let backgroundNode = self.backgroundNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: backgroundNode)
} else {
self.insertSubnode(self.highlightedBackgroundNode, at: 0)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override public func transitionNode(id: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if let item = self.item, let message = item.message, message.id == id, self.iconImageNode.supernode != nil {
let iconImageNode = self.iconImageNode
return (self.iconImageNode, self.iconImageNode.bounds, { [weak iconImageNode] in
return (iconImageNode?.view.snapshotContentTree(unhide: true), nil)
})
}
return nil
}
override public func updateHiddenMedia() {
if let interaction = self.interaction, let item = self.item, let message = item.message, interaction.getHiddenMedia()[message.id] != nil {
self.iconImageNode.isHidden = true
} else {
self.iconImageNode.isHidden = false
}
}
override public func updateSelectionState(animated: Bool) {
}
public func cancelPreviewGesture() {
self.containerNode.cancelGesture()
}
private func updateProgressFrame(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let item = self.appliedItem else {
return
}
var descriptionOffset: CGFloat = 0.0
var downloadingString: String?
if let resourceStatus = self.resourceStatus {
var maybeFetchStatus: MediaResourceStatus = .Local
switch resourceStatus {
case .playbackStatus:
break
case let .fetchStatus(fetchStatus):
maybeFetchStatus = fetchStatus._asStatus()
}
if item.isDownloadList, let fetchStatus = self.fetchStatus {
maybeFetchStatus = fetchStatus
}
switch maybeFetchStatus {
case .Fetching(_, let progress), .Paused(let progress):
if let file = self.currentMedia as? TelegramMediaFile, let size = file.size {
downloadingString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))) / \(dataSizeString(size, forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))"
}
descriptionOffset = 14.0
case .Remote:
descriptionOffset = 14.0
case .Local:
break
}
switch maybeFetchStatus {
case .Fetching(_, let progress), .Paused(let progress):
let progressFrame = CGRect(x: self.currentLeftOffset + leftInset + 65.0, y: size.height - 3.0, width: floorToScreenPixels((size.width - 65.0 - leftInset - rightInset)), height: 3.0)
let linearProgressNode: LinearProgressNode
if let current = self.linearProgressNode {
linearProgressNode = current
} else {
linearProgressNode = LinearProgressNode()
linearProgressNode.updateTheme(theme: item.presentationData.theme.theme)
self.linearProgressNode = linearProgressNode
self.addSubnode(linearProgressNode)
}
transition.updateFrame(node: linearProgressNode, frame: progressFrame)
linearProgressNode.updateProgress(value: CGFloat(progress), completion: {})
var animated = true
if let downloadStatusIconNode = self.downloadStatusIconNode {
if downloadStatusIconNode.supernode == nil {
animated = false
self.offsetContainerNode.addSubnode(downloadStatusIconNode)
}
if case .Paused = maybeFetchStatus {
downloadStatusIconNode.enqueueState(.download, animated: animated)
} else {
downloadStatusIconNode.enqueueState(.pause, animated: animated)
}
}
case .Local:
if let linearProgressNode = self.linearProgressNode {
self.linearProgressNode = nil
linearProgressNode.updateProgress(value: 1.0, completion: { [weak linearProgressNode] in
linearProgressNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
linearProgressNode?.removeFromSupernode()
})
})
}
if let downloadStatusIconNode = self.downloadStatusIconNode {
if downloadStatusIconNode.supernode != nil {
downloadStatusIconNode.removeFromSupernode()
}
}
case .Remote:
if let linearProgressNode = self.linearProgressNode {
self.linearProgressNode = nil
linearProgressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak linearProgressNode] _ in
linearProgressNode?.removeFromSupernode()
})
}
if let downloadStatusIconNode = self.downloadStatusIconNode {
var animated = true
if downloadStatusIconNode.supernode == nil {
animated = false
self.offsetContainerNode.addSubnode(downloadStatusIconNode)
}
downloadStatusIconNode.enqueueState(.download, animated: animated)
}
}
} else {
if let linearProgressNode = self.linearProgressNode {
self.linearProgressNode = nil
linearProgressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linearProgressNode] _ in
linearProgressNode?.removeFromSupernode()
})
}
if let downloadStatusIconNode = self.downloadStatusIconNode {
if downloadStatusIconNode.supernode != nil {
downloadStatusIconNode.removeFromSupernode()
}
}
}
var descriptionFrame = self.descriptionNode.frame
let originX = self.titleNode.frame.minX + descriptionOffset
if !descriptionFrame.origin.x.isEqual(to: originX) {
descriptionFrame.origin.x = originX
transition.updateFrame(node: self.descriptionNode, frame: descriptionFrame)
}
let alphaTransition: ContainedViewLayoutTransition
if item.isDownloadList {
alphaTransition = .immediate
} else {
alphaTransition = .animated(duration: 0.3, curve: .easeInOut)
}
if downloadingString != nil {
alphaTransition.updateAlpha(node: self.descriptionProgressNode, alpha: 1.0)
alphaTransition.updateAlpha(node: self.descriptionNode, alpha: 0.0)
} else {
alphaTransition.updateAlpha(node: self.descriptionProgressNode, alpha: 0.0)
alphaTransition.updateAlpha(node: self.descriptionNode, alpha: 1.0)
}
let descriptionFont = Font.with(size: floorToScreenPixels(item.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers])
self.descriptionProgressNode.attributedText = NSAttributedString(string: downloadingString ?? "", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
let descriptionSize = self.descriptionProgressNode.updateLayout(CGSize(width: size.width - 14.0, height: size.height))
transition.updateFrame(node: self.descriptionProgressNode, frame: CGRect(origin: CGPoint(x: self.descriptionNode.frame.minX, y: self.descriptionNode.frame.minY + floorToScreenPixels((self.descriptionNode.bounds.height - descriptionSize.height) / 2.0)), size: descriptionSize))
}
public func activateMedia() {
self.progressPressed()
}
func progressPressed() {
if let resourceStatus = self.resourceStatus {
switch resourceStatus {
case let .fetchStatus(fetchStatus):
switch fetchStatus {
case .Fetching:
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
cancel()
}
case .Remote, .Paused:
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
fetch()
}
case .Local:
if let item = self.item, let message = item.message, let interaction = self.interaction {
let _ = interaction.openMessage(message, .default)
}
}
case .playbackStatus:
if let context = self.context {
context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: nil)
}
}
}
}
override public func headers() -> [ListViewItemHeader]? {
return self.item?.header.flatMap { [$0] }
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let item = self.item, case .selectable = item.selection {
if self.bounds.contains(point) {
return self.view
}
}
return super.hitTest(point, with: event)
}
@objc private func statusPressed() {
guard let _ = self.item, let fetchStatus = self.fetchStatus else {
return
}
switch fetchStatus {
case .Fetching:
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
cancel()
}
case .Remote, .Paused:
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
fetch()
}
case .Local:
break
}
}
public override var canBeSelected: Bool {
return !self.currentIsRestricted
}
}
private final class LinearProgressNode: ASDisplayNode {
private let trackingNode: HierarchyTrackingNode
private let barNode: ASImageNode
private let shimmerNode: ASImageNode
private let shimmerClippingNode: ASDisplayNode
private var currentProgress: CGFloat = 0.0
private var currentProgressAnimation: (from: CGFloat, to: CGFloat, startTime: Double, completion: () -> Void)?
private var shimmerPhase: CGFloat = 0.0
private var inHierarchyValue: Bool = false
private var shouldAnimate: Bool = false
private let animator: ConstantDisplayLinkAnimator
override init() {
var updateInHierarchy: ((Bool) -> Void)?
self.trackingNode = HierarchyTrackingNode { value in
updateInHierarchy?(value)
}
var animationStep: (() -> Void)?
self.animator = ConstantDisplayLinkAnimator {
animationStep?()
}
self.barNode = ASImageNode()
self.barNode.isLayerBacked = true
self.shimmerNode = ASImageNode()
self.shimmerNode.contentMode = .scaleToFill
self.shimmerClippingNode = ASDisplayNode()
self.shimmerClippingNode.clipsToBounds = true
super.init()
self.addSubnode(trackingNode)
self.addSubnode(self.barNode)
self.shimmerClippingNode.addSubnode(self.shimmerNode)
self.addSubnode(self.shimmerClippingNode)
updateInHierarchy = { [weak self] value in
guard let strongSelf = self else {
return
}
if strongSelf.inHierarchyValue != value {
strongSelf.inHierarchyValue = value
strongSelf.updateAnimations()
}
}
animationStep = { [weak self] in
self?.update()
}
}
func updateTheme(theme: PresentationTheme) {
self.barNode.image = generateStretchableFilledCircleImage(diameter: 3.0, color: theme.list.itemAccentColor)
self.shimmerNode.image = generateImage(CGSize(width: 100.0, height: 3.0), opaque: false, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let foregroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.4)
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
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: CGGradientDrawingOptions())
})
}
func updateProgress(value: CGFloat, completion: @escaping () -> Void = {}) {
if self.currentProgress.isEqual(to: value) {
self.currentProgressAnimation = nil
completion()
} else {
self.currentProgressAnimation = (self.currentProgress, value, CACurrentMediaTime(), completion)
}
}
private func updateAnimations() {
let shouldAnimate = self.inHierarchyValue
if shouldAnimate != self.shouldAnimate {
self.shouldAnimate = shouldAnimate
self.animator.isPaused = !shouldAnimate
}
}
private func update() {
if let (fromValue, toValue, startTime, completion) = self.currentProgressAnimation {
let duration: Double = 0.15
let timestamp = CACurrentMediaTime()
let t = CGFloat((timestamp - startTime) / duration)
if t >= 1.0 {
self.currentProgress = toValue
self.currentProgressAnimation = nil
completion()
} else {
let clippedT = max(0.0, t)
self.currentProgress = (1.0 - clippedT) * fromValue + clippedT * toValue
}
var progressWidth: CGFloat = self.bounds.width * self.currentProgress
if progressWidth < 6.0 {
progressWidth = 0.0
}
let progressFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: progressWidth, height: 3.0))
self.barNode.frame = progressFrame
self.shimmerClippingNode.frame = progressFrame
}
self.shimmerPhase += 3.5
let shimmerWidth: CGFloat = 160.0
let shimmerOffset = self.shimmerPhase.remainder(dividingBy: self.bounds.width + shimmerWidth / 2.0)
self.shimmerNode.frame = CGRect(origin: CGPoint(x: shimmerOffset - shimmerWidth / 2.0, y: 0.0), size: CGSize(width: shimmerWidth, height: 3.0))
}
}
private enum DownloadIconNodeState: Equatable {
case download
case pause
}
private func generateDownloadIcon(color: UIColor) -> UIImage? {
let animation = ManagedAnimationNode(size: CGSize(width: 18.0, height: 18.0))
animation.customColor = color
animation.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
return animation.image
}
private final class DownloadIconNode: ASImageNode {
private var customColor: UIColor
private let duration: Double = 0.3
private var iconState: DownloadIconNodeState = .download
private var animationNode: ManagedAnimationNode?
init(theme: PresentationTheme) {
self.customColor = theme.list.itemAccentColor
super.init()
self.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(theme, generate: {
return generateDownloadIcon(color: theme.list.itemAccentColor)
})
self.contentMode = .center
}
func updateTheme(theme: PresentationTheme) {
if self.image != nil {
self.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(theme, generate: {
return generateDownloadIcon(color: theme.list.itemAccentColor)
})
}
self.customColor = theme.list.itemAccentColor
self.animationNode?.customColor = self.customColor
}
func enqueueState(_ state: DownloadIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
if self.animationNode == nil {
let animationNode = ManagedAnimationNode(size: CGSize(width: 18.0, height: 18.0))
self.animationNode = animationNode
animationNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 18.0, height: 18.0))
animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
self.addSubnode(animationNode)
self.image = nil
}
guard let animationNode = self.animationNode else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .download:
if animated {
animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 100, endFrame: 120), duration: self.duration))
} else {
animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .download:
switch state {
case .pause:
if animated {
animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
} else {
animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 60, endFrame: 60), duration: 0.01))
}
case .download:
break
}
}
}
}