mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-07 05:45:53 +00:00
Fixes
fix localeWithStrings globally (#30)
Fix badge on zoomed devices. closes #9
Hide channel bottom panel closes #27
Another attempt to fix badge on some Zoomed devices
Force System Share sheet tg://sg/debug
fixes for device badge
New Crowdin updates (#34)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
Fix input panel hidden on selection (#31)
* added if check for selectionState != nil
* same order of subnodes
Revert "Fix input panel hidden on selection (#31)"
This reverts commit e8a8bb1496.
Fix input panel for channels Closes #37
Quickly share links with system's share menu
force tabbar when editing
increase height for correct animation
New translations sglocalizable.strings (Ukrainian) (#38)
Hide Post Story button
Fix 10.15.1
Fix archive option for long-tap
Enable in-app Safari
Disable some unsupported purchases
disableDeleteChatSwipeOption + refactor restart alert
Hide bot in suggestions list
Fix merge v11.0
Fix exceptions for safari webview controller
New Crowdin updates (#47)
* New translations sglocalizable.strings (Romanian)
* New translations sglocalizable.strings (French)
* New translations sglocalizable.strings (Spanish)
* New translations sglocalizable.strings (Afrikaans)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Catalan)
* New translations sglocalizable.strings (Czech)
* New translations sglocalizable.strings (Danish)
* New translations sglocalizable.strings (German)
* New translations sglocalizable.strings (Greek)
* New translations sglocalizable.strings (Finnish)
* New translations sglocalizable.strings (Hebrew)
* New translations sglocalizable.strings (Hungarian)
* New translations sglocalizable.strings (Italian)
* New translations sglocalizable.strings (Japanese)
* New translations sglocalizable.strings (Korean)
* New translations sglocalizable.strings (Dutch)
* New translations sglocalizable.strings (Norwegian)
* New translations sglocalizable.strings (Polish)
* New translations sglocalizable.strings (Portuguese)
* New translations sglocalizable.strings (Serbian (Cyrillic))
* New translations sglocalizable.strings (Swedish)
* New translations sglocalizable.strings (Turkish)
* New translations sglocalizable.strings (Vietnamese)
* New translations sglocalizable.strings (Indonesian)
* New translations sglocalizable.strings (Hindi)
* New translations sglocalizable.strings (Uzbek)
New Crowdin updates (#49)
* New translations sglocalizable.strings (Arabic)
* New translations sglocalizable.strings (Arabic)
New translations sglocalizable.strings (Russian) (#51)
Call confirmation
WIP Settings search
Settings Search
Localize placeholder
Update AccountUtils.swift
mark mutual contact
Align back context action to left
New Crowdin updates (#54)
* New translations sglocalizable.strings (Chinese Simplified)
* New translations sglocalizable.strings (Chinese Traditional)
* New translations sglocalizable.strings (Ukrainian)
Independent Playground app for simulator
New translations sglocalizable.strings (Ukrainian) (#55)
Playground UIKit base and controllers
Inject SwiftUI view with overflow to AsyncDisplayKit
Launch Playgound project on simulator
Create .swiftformat
Move Playground to example
Update .swiftformat
Init SwiftUIViewController
wip
New translations sglocalizable.strings (Chinese Traditional) (#57)
Xcode 16 fixes
Fix
New translations sglocalizable.strings (Italian) (#59)
New translations sglocalizable.strings (Chinese Simplified) (#63)
Force disable CallKit integration due to missing NSE Entitlement
Fix merge
Fix whole chat translator
Sweetpad config
Bump version
11.3.1 fixes
Mutual contact placement fix
Disable Video PIP swipe
Update versions.json
Fix PIP crash
1853 lines
101 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|
|
}
|