mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00

Added ability to download music without streaming Added progress indicators for various blocking tasks Fixed image gallery swipe to dismiss after zooming Added online member count indication in supergroups Fixed contact statuses in contact search
808 lines
40 KiB
Swift
808 lines
40 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
|
|
private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:])
|
|
|
|
private let redColors: (UInt32, UInt32) = (0xf0625d, 0xde524e)
|
|
private let greenColors: (UInt32, UInt32) = (0x72ce76, 0x54b658)
|
|
private let blueColors: (UInt32, UInt32) = (0x60b0e8, 0x4597d1)
|
|
private let yellowColors: (UInt32, UInt32) = (0xf5c565, 0xe5a64e)
|
|
|
|
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: 42.0, height: 42.0), contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
context.translateBy(x: -size.width / 2.0 + 1.0, y: -size.height / 2.0 + 1.0)
|
|
|
|
let radius: CGFloat = 2.0
|
|
let cornerSize: CGFloat = 10.0
|
|
let size = CGSize(width: 42.0, height: 42.0)
|
|
|
|
context.setFillColor(UIColor(rgb: colors.0).cgColor)
|
|
context.beginPath()
|
|
context.move(to: CGPoint(x: 0.0, y: radius))
|
|
if !radius.isZero {
|
|
context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: radius, y: 0.0), radius: radius)
|
|
}
|
|
context.addLine(to: CGPoint(x: size.width - cornerSize, y: 0.0))
|
|
context.addLine(to: CGPoint(x: size.width - cornerSize + cornerSize / 4.0, y: cornerSize - cornerSize / 4.0))
|
|
context.addLine(to: CGPoint(x: size.width, y: cornerSize))
|
|
context.addLine(to: CGPoint(x: size.width, y: size.height - radius))
|
|
if !radius.isZero {
|
|
context.addArc(tangent1End: CGPoint(x: size.width, y: size.height), tangent2End: CGPoint(x: size.width - radius, y: size.height), radius: radius)
|
|
}
|
|
context.addLine(to: CGPoint(x: radius, y: size.height))
|
|
|
|
if !radius.isZero {
|
|
context.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - radius), radius: radius)
|
|
}
|
|
context.closePath()
|
|
context.fillPath()
|
|
|
|
context.setFillColor(UIColor(rgb: colors.1).cgColor)
|
|
context.beginPath()
|
|
context.move(to: CGPoint(x: size.width - cornerSize, y: 0.0))
|
|
context.addLine(to: CGPoint(x: size.width, y: cornerSize))
|
|
context.addLine(to: CGPoint(x: size.width - cornerSize + radius, y: cornerSize))
|
|
|
|
if !radius.isZero {
|
|
context.addArc(tangent1End: CGPoint(x: size.width - cornerSize, y: cornerSize), tangent2End: CGPoint(x: size.width - cornerSize, y: cornerSize - radius), radius: radius)
|
|
}
|
|
|
|
context.closePath()
|
|
context.fillPath()
|
|
})
|
|
}
|
|
|
|
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 titleFont = Font.medium(16.0)
|
|
private let audioTitleFont = Font.regular(16.0)
|
|
private let descriptionFont = Font.regular(13.0)
|
|
private let extensionFont = Font.medium(13.0)
|
|
|
|
private struct FetchControls {
|
|
let fetch: () -> Void
|
|
let cancel: () -> Void
|
|
}
|
|
|
|
private enum FileIconImage: Equatable {
|
|
case imageRepresentation(TelegramMediaFile, TelegramMediaImageRepresentation)
|
|
case albumArt(SharedMediaPlaybackAlbumArt)
|
|
|
|
static func ==(lhs: FileIconImage, rhs: FileIconImage) -> Bool {
|
|
switch lhs {
|
|
case let .imageRepresentation(file, value):
|
|
if case .imageRepresentation(file, value) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .albumArt(value):
|
|
if case .albumArt(value) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final class ListMessageFileItemNode: ListMessageNode {
|
|
private let highlightedBackgroundNode: ASDisplayNode
|
|
private let separatorNode: ASDisplayNode
|
|
|
|
private var selectionNode: ItemListSelectableControlNode?
|
|
|
|
private let titleNode: TextNode
|
|
private let descriptionNode: TextNode
|
|
|
|
private let extensionIconNode: ASImageNode
|
|
private let extensionIconText: TextNode
|
|
private let iconImageNode: TransformImageNode
|
|
|
|
private var currentIconImage: FileIconImage?
|
|
private var currentMedia: Media?
|
|
|
|
private let statusDisposable = MetaDisposable()
|
|
private let fetchControls = Atomic<FetchControls?>(value: nil)
|
|
private var resourceStatus: FileMediaResourceMediaStatus?
|
|
private let fetchDisposable = MetaDisposable()
|
|
|
|
private var downloadStatusIconNode: ASImageNode
|
|
private var linearProgressNode: ASDisplayNode
|
|
|
|
private let progressNode: RadialProgressNode
|
|
private var playbackOverlayNode: ListMessagePlaybackOverlayNode?
|
|
|
|
private var account: Account?
|
|
private (set) var message: Message?
|
|
|
|
private var appliedItem: ListMessageItem?
|
|
private var layoutParams: ListViewItemLayoutParams?
|
|
private var currentLeftOffet: CGFloat = 0.0
|
|
|
|
override var canBeLongTapped: Bool {
|
|
return true
|
|
}
|
|
|
|
public required init() {
|
|
self.separatorNode = ASDisplayNode()
|
|
self.separatorNode.displaysAsynchronously = false
|
|
self.separatorNode.isLayerBacked = true
|
|
|
|
self.highlightedBackgroundNode = ASDisplayNode()
|
|
self.highlightedBackgroundNode.isLayerBacked = true
|
|
|
|
self.titleNode = TextNode()
|
|
self.titleNode.isLayerBacked = true
|
|
|
|
self.descriptionNode = TextNode()
|
|
self.descriptionNode.isLayerBacked = true
|
|
|
|
self.extensionIconNode = ASImageNode()
|
|
self.extensionIconNode.isLayerBacked = true
|
|
self.extensionIconNode.displaysAsynchronously = false
|
|
self.extensionIconNode.displayWithoutProcessing = true
|
|
|
|
self.extensionIconText = TextNode()
|
|
self.extensionIconText.isLayerBacked = true
|
|
|
|
self.iconImageNode = TransformImageNode()
|
|
self.iconImageNode.displaysAsynchronously = false
|
|
self.iconImageNode.contentAnimations = .subsequentUpdates
|
|
|
|
self.downloadStatusIconNode = ASImageNode()
|
|
self.downloadStatusIconNode.isLayerBacked = true
|
|
self.downloadStatusIconNode.displaysAsynchronously = false
|
|
self.downloadStatusIconNode.displayWithoutProcessing = true
|
|
|
|
self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: .black, foregroundColor: .white, icon: nil))
|
|
self.progressNode.isLayerBacked = true
|
|
|
|
self.linearProgressNode = ASDisplayNode()
|
|
self.linearProgressNode.isLayerBacked = true
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.separatorNode)
|
|
self.addSubnode(self.titleNode)
|
|
self.addSubnode(self.descriptionNode)
|
|
self.addSubnode(self.extensionIconNode)
|
|
self.addSubnode(self.extensionIconText)
|
|
}
|
|
|
|
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, short: Bool) {
|
|
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
|
|
|
self.transitionOffset = self.bounds.size.height * 1.6
|
|
self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
|
|
//self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration)
|
|
}
|
|
|
|
override func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
|
let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode)
|
|
let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode)
|
|
let extensionIconTextMakeLayout = TextNode.asyncLayout(self.extensionIconText)
|
|
let iconImageLayout = self.iconImageNode.asyncLayout()
|
|
|
|
let currentMedia = self.currentMedia
|
|
let currentMessage = self.message
|
|
let currentIconImage = self.currentIconImage
|
|
|
|
let currentItem = self.appliedItem
|
|
|
|
let selectionNodeLayout = ItemListSelectableControlNode.asyncLayout(self.selectionNode)
|
|
|
|
return { [weak self] item, params, _, _, dateHeaderAtBottom in
|
|
var updatedTheme: PresentationTheme?
|
|
|
|
if currentItem?.theme !== item.theme {
|
|
updatedTheme = item.theme
|
|
}
|
|
|
|
var 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.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected)
|
|
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 updatedFetchControls: FetchControls?
|
|
|
|
var isAudio = false
|
|
|
|
let message = item.message
|
|
|
|
var selectedMedia: TelegramMediaFile?
|
|
for media in message.media {
|
|
if let file = media as? TelegramMediaFile {
|
|
selectedMedia = file
|
|
|
|
for attribute in file.attributes {
|
|
if case let .Audio(voice, _, title, performer, _) = attribute {
|
|
isAudio = true
|
|
|
|
titleText = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
|
|
|
let descriptionString: String
|
|
if let performer = performer {
|
|
descriptionString = performer
|
|
} else if let size = file.size {
|
|
descriptionString = dataSizeString(size)
|
|
} else {
|
|
descriptionString = ""
|
|
}
|
|
|
|
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
|
|
|
if !voice {
|
|
iconImage = .albumArt(SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: false)))
|
|
}
|
|
}
|
|
}
|
|
|
|
if !isAudio {
|
|
let fileName: String = file.fileName ?? ""
|
|
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.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: extensionFont, textColor: UIColor.white)
|
|
}
|
|
|
|
if let representation = smallestImageRepresentation(file.previewRepresentations) {
|
|
iconImage = .imageRepresentation(file, representation)
|
|
}
|
|
|
|
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.strings, dateTimeFormat: item.dateTimeFormat)
|
|
|
|
let descriptionString: String
|
|
if let size = file.size {
|
|
descriptionString = "\(dataSizeString(size)) • \(dateString)"
|
|
} else {
|
|
descriptionString = "\(dateString)"
|
|
}
|
|
|
|
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if isAudio {
|
|
leftInset += 14.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 selectedMedia = selectedMedia {
|
|
if mediaUpdated {
|
|
let account = item.account
|
|
updatedFetchControls = FetchControls(fetch: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, message: message, file: selectedMedia, userInitiated: true).start())
|
|
}
|
|
}, cancel: {
|
|
messageMediaFileCancelInteractiveFetch(account: account, messageId: message.id, file: selectedMedia)
|
|
})
|
|
}
|
|
|
|
if statusUpdated {
|
|
updatedStatusSignal = messageFileMediaResourceStatus(account: item.account, file: selectedMedia, message: message, isRecentActions: false)
|
|
|
|
if isAudio {
|
|
if let currentUpdatedStatusSignal = updatedStatusSignal {
|
|
updatedStatusSignal = currentUpdatedStatusSignal
|
|
|> map { status in
|
|
switch status.mediaStatus {
|
|
case .fetchStatus:
|
|
return FileMediaResourceStatus(mediaStatus: .fetchStatus(.Local), fetchStatus: status.fetchStatus)
|
|
case .playbackStatus:
|
|
return status
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
|
|
let (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: 42.0, height: 42.0)
|
|
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
|
|
let arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets())
|
|
iconImageApply = iconImageLayout(arguments)
|
|
case .albumArt:
|
|
let iconSize = CGSize(width: 46.0, height: 46.0)
|
|
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
|
|
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets())
|
|
iconImageApply = iconImageLayout(arguments)
|
|
}
|
|
}
|
|
|
|
if currentIconImage != iconImage {
|
|
if let iconImage = iconImage {
|
|
switch iconImage {
|
|
case let .imageRepresentation(file, representation):
|
|
updateIconImageSignal = chatWebpageSnippetFile(account: item.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation)
|
|
case let .albumArt(albumArt):
|
|
updateIconImageSignal = playerAlbumArt(postbox: item.account.postbox, albumArt: albumArt, thumbnail: true)
|
|
|
|
}
|
|
} 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
|
|
}
|
|
|
|
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: isAudio ? 56.0 : 52.0), insets: insets)
|
|
|
|
return (nodeLayout, { animation in
|
|
if let strongSelf = self {
|
|
let transition: ContainedViewLayoutTransition
|
|
if animation.isAnimated {
|
|
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
|
} else {
|
|
transition = .immediate
|
|
}
|
|
|
|
strongSelf.currentMedia = selectedMedia
|
|
strongSelf.message = message
|
|
strongSelf.account = item.account
|
|
strongSelf.appliedItem = item
|
|
strongSelf.layoutParams = params
|
|
strongSelf.currentLeftOffet = leftOffset
|
|
|
|
if let _ = updatedTheme {
|
|
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
|
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
|
|
|
strongSelf.progressNode.updateTheme(RadialProgressTheme(backgroundColor: item.theme.list.itemAccentColor, foregroundColor: item.theme.list.plainBackgroundColor, icon: nil))
|
|
strongSelf.linearProgressNode.backgroundColor = item.theme.list.itemAccentColor
|
|
|
|
}
|
|
|
|
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.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: -UIScreenPixel - nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel))
|
|
|
|
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 8.0), size: titleNodeLayout.size))
|
|
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:
|
|
descriptionOffset = 14.0
|
|
case .Local:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: isAudio ? 32.0 : 29.0), size: descriptionNodeLayout.size))
|
|
let _ = descriptionNodeApply()
|
|
|
|
let iconFrame: CGRect
|
|
if isAudio {
|
|
let iconSize = CGSize(width: 48.0, height: 48.0)
|
|
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 20.0, y: 5.0), size: iconSize)
|
|
} else {
|
|
let iconSize = CGSize(width: 42.0, height: 42.0)
|
|
iconFrame = CGRect(origin: CGPoint(x: params.leftInset + leftOffset + 9.0, y: 5.0), size: iconSize)
|
|
}
|
|
transition.updateFrame(node: strongSelf.extensionIconNode, frame: iconFrame)
|
|
strongSelf.extensionIconNode.image = extensionIconImage
|
|
transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: leftOffset + 9.0 + floor((42.0 - extensionTextLayout.size.width) / 2.0), y: 5.0 + floor((42.0 - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size))
|
|
|
|
let _ = extensionTextApply()
|
|
|
|
strongSelf.currentIconImage = iconImage
|
|
|
|
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.addSubnode(strongSelf.iconImageNode)
|
|
}
|
|
|
|
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.addSubnode(strongSelf.extensionIconNode)
|
|
}
|
|
if strongSelf.extensionIconText.supernode == nil {
|
|
strongSelf.addSubnode(strongSelf.extensionIconText)
|
|
}
|
|
}
|
|
|
|
if let playbackOverlayNode = strongSelf.playbackOverlayNode {
|
|
transition.updateFrame(node: playbackOverlayNode, frame: iconFrame)
|
|
}
|
|
|
|
if let updatedStatusSignal = updatedStatusSignal {
|
|
strongSelf.statusDisposable.set((updatedStatusSignal
|
|
|> deliverOnMainQueue).start(next: { [weak strongSelf] fileStatus in
|
|
let status = fileStatus.mediaStatus
|
|
displayLinkDispatcher.dispatch {
|
|
if let strongSelf = strongSelf {
|
|
strongSelf.resourceStatus = status
|
|
var musicIsPlaying: Bool?
|
|
|
|
if !isAudio {
|
|
if let layoutParams = strongSelf.layoutParams {
|
|
strongSelf.updateProgressFrame(size: nodeLayout.contentSize, leftInset: layoutParams.leftInset, rightInset: layoutParams.rightInset, transition: .immediate)
|
|
}
|
|
} else {
|
|
switch status {
|
|
case let .fetchStatus(fetchStatus):
|
|
switch fetchStatus {
|
|
case let .Fetching(isActive, progress):
|
|
var adjustedProgress = progress
|
|
if isActive {
|
|
adjustedProgress = max(adjustedProgress, 0.027)
|
|
}
|
|
strongSelf.progressNode.state = .Fetching(progress: adjustedProgress)
|
|
case .Local:
|
|
if isAudio {
|
|
strongSelf.progressNode.state = .Play
|
|
} else {
|
|
strongSelf.progressNode.state = .Icon
|
|
}
|
|
case .Remote:
|
|
if isAudio {
|
|
strongSelf.progressNode.state = .Play
|
|
} else {
|
|
strongSelf.progressNode.state = .Remote
|
|
}
|
|
}
|
|
case let .playbackStatus(playbackStatus):
|
|
switch playbackStatus {
|
|
case .playing:
|
|
musicIsPlaying = true
|
|
strongSelf.progressNode.state = .Pause
|
|
case .paused:
|
|
musicIsPlaying = false
|
|
strongSelf.progressNode.state = .Play
|
|
}
|
|
}
|
|
}
|
|
if let musicIsPlaying = musicIsPlaying {
|
|
if strongSelf.playbackOverlayNode == nil {
|
|
let playbackOverlayNode = ListMessagePlaybackOverlayNode()
|
|
playbackOverlayNode.frame = strongSelf.iconImageNode.frame
|
|
strongSelf.playbackOverlayNode = playbackOverlayNode
|
|
strongSelf.addSubnode(playbackOverlayNode)
|
|
}
|
|
strongSelf.playbackOverlayNode?.isPlaying = musicIsPlaying
|
|
} else if let playbackOverlayNode = strongSelf.playbackOverlayNode {
|
|
strongSelf.playbackOverlayNode = nil
|
|
playbackOverlayNode.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
strongSelf.updateProgressFrame(size: CGSize(width: params.width, height: 52.0), leftInset: params.leftInset, rightInset: params.rightInset, transition: transition)
|
|
transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 31.0), size: CGSize(width: 11.0, height: 11.0)))
|
|
|
|
if let updatedFetchControls = updatedFetchControls {
|
|
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
override 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 {
|
|
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
|
|
}
|
|
} 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 func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? {
|
|
if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil {
|
|
let iconImageNode = self.iconImageNode
|
|
return (self.iconImageNode, { [weak iconImageNode] in
|
|
return iconImageNode?.view.snapshotContentTree(unhide: true)
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
override func updateHiddenMedia() {
|
|
if let controllerInteraction = self.controllerInteraction, let item = self.item, controllerInteraction.hiddenMedia[item.message.id] != nil {
|
|
self.iconImageNode.isHidden = true
|
|
} else {
|
|
self.iconImageNode.isHidden = false
|
|
}
|
|
}
|
|
|
|
override func updateSelectionState(animated: Bool) {
|
|
}
|
|
|
|
private func updateProgressFrame(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
var descriptionOffset: CGFloat = 0.0
|
|
|
|
if let resourceStatus = self.resourceStatus, let item = self.appliedItem {
|
|
var maybeFetchStatus: MediaResourceStatus = .Local
|
|
switch resourceStatus {
|
|
case .playbackStatus:
|
|
break
|
|
case let .fetchStatus(fetchStatus):
|
|
maybeFetchStatus = fetchStatus
|
|
switch fetchStatus {
|
|
case .Remote, .Fetching:
|
|
descriptionOffset = 14.0
|
|
case .Local:
|
|
break
|
|
}
|
|
}
|
|
|
|
switch maybeFetchStatus {
|
|
case let .Fetching(_, progress):
|
|
let progressFrame = CGRect(x: self.currentLeftOffet + leftInset + 65.0, y: size.height - 2.0, width: floor((size.width - 65.0 - leftInset - rightInset) * CGFloat(progress)), height: 2.0)
|
|
if self.linearProgressNode.supernode == nil {
|
|
self.addSubnode(self.linearProgressNode)
|
|
}
|
|
transition.updateFrame(node: self.linearProgressNode, frame: progressFrame)
|
|
if self.downloadStatusIconNode.supernode == nil {
|
|
self.addSubnode(self.downloadStatusIconNode)
|
|
}
|
|
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.theme)
|
|
case .Local:
|
|
if self.linearProgressNode.supernode != nil {
|
|
self.linearProgressNode.removeFromSupernode()
|
|
}
|
|
if self.downloadStatusIconNode.supernode != nil {
|
|
self.downloadStatusIconNode.removeFromSupernode()
|
|
}
|
|
self.downloadStatusIconNode.image = nil
|
|
case .Remote:
|
|
if self.linearProgressNode.supernode != nil {
|
|
self.linearProgressNode.removeFromSupernode()
|
|
}
|
|
if self.downloadStatusIconNode.supernode == nil {
|
|
self.addSubnode(self.downloadStatusIconNode)
|
|
}
|
|
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.theme)
|
|
}
|
|
} else {
|
|
if self.linearProgressNode.supernode != nil {
|
|
self.linearProgressNode.removeFromSupernode()
|
|
}
|
|
if self.downloadStatusIconNode.supernode != nil {
|
|
self.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)
|
|
}
|
|
}
|
|
|
|
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:
|
|
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
|
|
fetch()
|
|
}
|
|
case .Local:
|
|
if let item = self.item, let controllerInteraction = self.controllerInteraction {
|
|
let _ = controllerInteraction.openMessage(item.message)
|
|
}
|
|
}
|
|
case .playbackStatus:
|
|
if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext {
|
|
applicationContext.mediaManager?.playlistControl(.playback(.togglePlayPause))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func header() -> ListViewItemHeader? {
|
|
return self.item?.header
|
|
}
|
|
|
|
override 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)
|
|
}
|
|
|
|
override func longTapped() {
|
|
if let item = self.item {
|
|
item.controllerInteraction.openMessageContextMenu(item.message, self, self.bounds)
|
|
}
|
|
}
|
|
}
|