mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
331 lines
18 KiB
Swift
331 lines
18 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import TelegramPresentationData
|
|
import TextFormat
|
|
import RadialStatusNode
|
|
import AppBundle
|
|
|
|
private let font = Font.with(size: 11.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
|
|
private let boldFont = Font.with(size: 11.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers])
|
|
|
|
public enum ChatMessageInteractiveMediaDownloadState: Equatable {
|
|
case remote
|
|
case fetching(progress: Float?)
|
|
case compactRemote
|
|
case compactFetching(progress: Float)
|
|
}
|
|
|
|
public enum ChatMessageInteractiveMediaBadgeContent: Equatable {
|
|
case text(inset: CGFloat, backgroundColor: UIColor, foregroundColor: UIColor, text: NSAttributedString, iconName: String?)
|
|
case mediaDownload(backgroundColor: UIColor, foregroundColor: UIColor, duration: String, size: String?, muted: Bool, active: Bool)
|
|
|
|
public static func ==(lhs: ChatMessageInteractiveMediaBadgeContent, rhs: ChatMessageInteractiveMediaBadgeContent) -> Bool {
|
|
switch lhs {
|
|
case let .text(lhsInset, lhsBackgroundColor, lhsForegroundColor, lhsText, lhsIconName):
|
|
if case let .text(rhsInset, rhsBackgroundColor, rhsForegroundColor, rhsText, rhsIconName) = rhs, lhsInset.isEqual(to: rhsInset), lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsText.isEqual(to: rhsText), lhsIconName == rhsIconName {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .mediaDownload(lhsBackgroundColor, lhsForegroundColor, lhsDuration, lhsSize, lhsMuted, lhsActive):
|
|
if case let .mediaDownload(rhsBackgroundColor, rhsForegroundColor, rhsDuration, rhsSize, rhsMuted, rhsActive) = rhs, lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsDuration == rhsDuration, lhsSize == rhsSize, lhsMuted == rhsMuted, lhsActive == rhsActive {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
|
|
private var content: ChatMessageInteractiveMediaBadgeContent?
|
|
public var pressed: (() -> Void)?
|
|
|
|
private var mediaDownloadState: ChatMessageInteractiveMediaDownloadState?
|
|
|
|
private var previousContentSize: CGSize?
|
|
private var backgroundNodeColor: UIColor?
|
|
private var foregroundColor: UIColor?
|
|
private var iconName: String?
|
|
|
|
private let backgroundNode: ASImageNode
|
|
private let durationNode: ASTextNode
|
|
private var sizeNode: ASTextNode?
|
|
private var measureNode: ASTextNode
|
|
private var iconNode: ASImageNode?
|
|
private var mediaDownloadStatusNode: RadialStatusNode?
|
|
|
|
override public init() {
|
|
self.backgroundNode = ASImageNode()
|
|
self.backgroundNode.clipsToBounds = true
|
|
self.durationNode = ASTextNode()
|
|
self.measureNode = ASTextNode()
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
self.backgroundNode.addSubnode(self.durationNode)
|
|
}
|
|
|
|
override public func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
|
}
|
|
|
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
self.pressed?()
|
|
}
|
|
}
|
|
|
|
private let digitsSet = CharacterSet(charactersIn: "0123456789")
|
|
private func widthForString(_ string: String) -> CGFloat {
|
|
let convertedString = string.components(separatedBy: digitsSet).joined(separator: "8")
|
|
self.measureNode.attributedText = NSMutableAttributedString(string: convertedString, attributes: [.font: font])
|
|
return self.measureNode.measure(CGSize(width: 240.0, height: 160.0)).width
|
|
}
|
|
|
|
public func update(theme: PresentationTheme?, content: ChatMessageInteractiveMediaBadgeContent?, mediaDownloadState: ChatMessageInteractiveMediaDownloadState?, alignment: NSTextAlignment = .left, animated: Bool, badgeAnimated: Bool = true) {
|
|
var transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
|
|
|
|
let previousContentSize = self.previousContentSize
|
|
var contentSize: CGSize?
|
|
|
|
if self.content != content {
|
|
let previousContent = self.content
|
|
self.content = content
|
|
var currentContentSize = CGSize()
|
|
|
|
if let content = self.content {
|
|
var previousActive: Bool?
|
|
var previousMuted: Bool?
|
|
if let previousContent = previousContent, case let .mediaDownload(_, _, _, _, muted, active) = previousContent {
|
|
previousActive = active
|
|
previousMuted = muted
|
|
}
|
|
|
|
switch content {
|
|
case let .text(inset, backgroundColor, foregroundColor, text, iconName):
|
|
transition = .immediate
|
|
|
|
if self.backgroundNodeColor != backgroundColor {
|
|
self.backgroundNodeColor = backgroundColor
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: backgroundColor)
|
|
}
|
|
var textFont = font
|
|
if iconName != nil {
|
|
textFont = boldFont
|
|
}
|
|
let convertedText = NSMutableAttributedString(string: text.string, attributes: [.font: textFont, .foregroundColor: foregroundColor])
|
|
text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: []) { attributes, range, _ in
|
|
if let _ = attributes[ChatTextInputAttributes.bold] {
|
|
convertedText.addAttribute(.font, value: boldFont, range: range)
|
|
}
|
|
}
|
|
self.durationNode.attributedText = convertedText
|
|
let durationSize = self.durationNode.measure(CGSize(width: 160.0, height: 160.0))
|
|
self.durationNode.frame = CGRect(x: 7.0 + inset, y: 2.0 + UIScreenPixel, width: durationSize.width, height: durationSize.height)
|
|
currentContentSize = CGSize(width: widthForString(text.string) + 14.0 + inset, height: 18.0)
|
|
|
|
if let iconName {
|
|
let iconNode: ASImageNode
|
|
if let current = self.iconNode {
|
|
iconNode = current
|
|
} else {
|
|
iconNode = ASImageNode()
|
|
self.iconNode = iconNode
|
|
self.backgroundNode.addSubnode(iconNode)
|
|
}
|
|
|
|
if self.foregroundColor != foregroundColor || self.iconName != iconName {
|
|
self.foregroundColor = foregroundColor
|
|
self.iconName = iconName
|
|
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: foregroundColor)
|
|
}
|
|
transition.updateAlpha(node: iconNode, alpha: 1.0)
|
|
transition.updateTransformScale(node: iconNode, scale: 1.0)
|
|
|
|
iconNode.frame = CGRect(x: 3.0, y: 2.0, width: 12.0, height: 14.0)
|
|
} else {
|
|
if let iconNode = self.iconNode {
|
|
transition.updateTransformScale(node: iconNode, scale: 0.001)
|
|
transition.updateAlpha(node: iconNode, alpha: 0.0)
|
|
}
|
|
}
|
|
case let .mediaDownload(backgroundColor, foregroundColor, duration, size, muted, active):
|
|
if self.backgroundNodeColor != backgroundColor {
|
|
self.backgroundNodeColor = backgroundColor
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: backgroundColor)
|
|
}
|
|
|
|
if previousActive == nil {
|
|
previousActive = active
|
|
}
|
|
if previousMuted == nil {
|
|
previousMuted = muted
|
|
}
|
|
|
|
let textTransition = previousActive != active ? transition : .immediate
|
|
transition = (previousMuted != muted || previousActive != active) ? transition : .immediate
|
|
|
|
let durationString = NSMutableAttributedString(string: duration, attributes: [.font: font, .foregroundColor: foregroundColor])
|
|
self.durationNode.attributedText = durationString
|
|
|
|
var sizeWidth: CGFloat = 0.0
|
|
if let size = size {
|
|
let sizeNode: ASTextNode
|
|
if let current = self.sizeNode {
|
|
sizeNode = current
|
|
} else {
|
|
sizeNode = ASTextNode()
|
|
self.sizeNode = sizeNode
|
|
self.backgroundNode.addSubnode(sizeNode)
|
|
}
|
|
|
|
let sizeString = NSMutableAttributedString(string: size, attributes: [.font: font, .foregroundColor: foregroundColor])
|
|
sizeWidth = widthForString(size)
|
|
sizeNode.attributedText = sizeString
|
|
let sizeSize = sizeNode.measure(CGSize(width: 160.0, height: 160.0))
|
|
let sizeFrame = CGRect(x: active ? 42.0 : 7.0, y: active ? 19.0 : 2.0, width: sizeSize.width, height: sizeSize.height)
|
|
sizeNode.bounds = CGRect(origin: CGPoint(), size: sizeFrame.size)
|
|
|
|
let previousFrame = sizeNode.frame
|
|
if previousFrame.midY != sizeFrame.midY {
|
|
textTransition.updatePosition(node: sizeNode, position: CGPoint(x: sizeFrame.midX, y: sizeFrame.midY))
|
|
} else {
|
|
sizeNode.layer.removeAllAnimations()
|
|
sizeNode.frame = sizeFrame
|
|
}
|
|
transition.updateAlpha(node: sizeNode, alpha: 1.0)
|
|
} else if let sizeNode = self.sizeNode {
|
|
let sizeSize = sizeNode.frame.size
|
|
let sizeFrame = CGRect(x: active ? 42.0 : 7.0, y: active ? 19.0 : 2.0, width: sizeSize.width, height: sizeSize.height)
|
|
sizeNode.bounds = CGRect(origin: CGPoint(), size: sizeFrame.size)
|
|
textTransition.updatePosition(node: sizeNode, position: CGPoint(x: sizeFrame.midX, y: sizeFrame.midY))
|
|
|
|
transition.updateAlpha(node: sizeNode, alpha: 0.0)
|
|
}
|
|
|
|
let durationSize = self.durationNode.measure(CGSize(width: 160.0, height: 160.0))
|
|
if let statusNode = self.mediaDownloadStatusNode {
|
|
transition.updateAlpha(node: statusNode, alpha: active ? 1.0 : 0.0)
|
|
}
|
|
|
|
let durationFrame = CGRect(x: active ? 42.0 : 7.0, y: active ? 6.0 : 2.0 + UIScreenPixel, width: durationSize.width, height: durationSize.height)
|
|
self.durationNode.bounds = CGRect(origin: CGPoint(), size: durationFrame.size)
|
|
textTransition.updatePosition(node: self.durationNode, position: CGPoint(x: durationFrame.midX, y: durationFrame.midY))
|
|
|
|
let iconNode: ASImageNode
|
|
if let current = self.iconNode {
|
|
iconNode = current
|
|
} else {
|
|
iconNode = ASImageNode()
|
|
iconNode.frame = CGRect(x: 0.0, y: 0.0, width: 14.0, height: 9.0)
|
|
self.iconNode = iconNode
|
|
self.backgroundNode.addSubnode(iconNode)
|
|
}
|
|
|
|
if self.foregroundColor != foregroundColor {
|
|
self.foregroundColor = foregroundColor
|
|
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/InlineVideoMute"), color: foregroundColor)
|
|
}
|
|
|
|
let durationWidth = widthForString(duration)
|
|
transition.updatePosition(node: iconNode, position: CGPoint(x: (active ? 42.0 : 7.0) + durationWidth + 4.0 + 7.0, y: (active ? 8.0 : 4.0) + 5.0))
|
|
|
|
if muted {
|
|
transition.updateAlpha(node: iconNode, alpha: 1.0)
|
|
transition.updateTransformScale(node: iconNode, scale: 1.0)
|
|
} else if let iconNode = self.iconNode {
|
|
transition.updateAlpha(node: iconNode, alpha: 0.0)
|
|
transition.updateTransformScale(node: iconNode, scale: 0.001)
|
|
}
|
|
|
|
var contentWidth: CGFloat = max(sizeWidth, durationWidth + (muted ? 17.0 : 0.0)) + 14.0
|
|
if active {
|
|
contentWidth += 36.0
|
|
}
|
|
currentContentSize = CGSize(width: contentWidth, height: active ? 38.0 : 18.0)
|
|
}
|
|
}
|
|
|
|
var originX: CGFloat = 0.0
|
|
if alignment == .right {
|
|
originX = -currentContentSize.width
|
|
}
|
|
let previousSize = self.backgroundNode.frame.size
|
|
if previousSize.height == 0 || (previousSize.height == currentContentSize.height && currentContentSize.height == 38.0) {
|
|
self.backgroundNode.frame = CGRect(x: originX, y: 0.0, width: currentContentSize.width, height: currentContentSize.height)
|
|
} else {
|
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(x: originX, y: 0.0, width: currentContentSize.width, height: currentContentSize.height))
|
|
}
|
|
|
|
contentSize = currentContentSize
|
|
self.previousContentSize = contentSize
|
|
} else {
|
|
contentSize = previousContentSize
|
|
}
|
|
|
|
if self.mediaDownloadState != mediaDownloadState || previousContentSize != contentSize {
|
|
self.mediaDownloadState = mediaDownloadState
|
|
if let mediaDownloadState = self.mediaDownloadState, let contentSize = contentSize {
|
|
let mediaDownloadStatusNode: RadialStatusNode
|
|
if let current = self.mediaDownloadStatusNode {
|
|
mediaDownloadStatusNode = current
|
|
} else {
|
|
mediaDownloadStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
|
self.mediaDownloadStatusNode = mediaDownloadStatusNode
|
|
self.addSubnode(mediaDownloadStatusNode)
|
|
}
|
|
let state: RadialStatusNodeState
|
|
var isCompact = false
|
|
var originX: CGFloat = 0.0
|
|
if alignment == .right {
|
|
originX -= contentSize.width
|
|
}
|
|
var originY: CGFloat = 5.0
|
|
switch mediaDownloadState {
|
|
case .remote:
|
|
if let theme = theme, let image = PresentationResourcesChat.chatBubbleFileCloudFetchMediaIcon(theme) {
|
|
state = .customIcon(image)
|
|
} else {
|
|
state = .none
|
|
}
|
|
case let .fetching(progress):
|
|
var cloudProgress: CGFloat?
|
|
if let progress = progress {
|
|
cloudProgress = CGFloat(progress)
|
|
}
|
|
state = .cloudProgress(color: .white, strokeBackgroundColor: UIColor(white: 1.0, alpha: 0.3), lineWidth: 2.0 - UIScreenPixel, value: cloudProgress)
|
|
case .compactRemote:
|
|
state = .download(.white)
|
|
isCompact = true
|
|
originY = -1.0 - UIScreenPixel
|
|
case .compactFetching:
|
|
state = .progress(color: .white, lineWidth: nil, value: 0.0, cancelEnabled: true, animateRotation: true)
|
|
isCompact = true
|
|
originY = -1.0
|
|
}
|
|
let mediaStatusFrame: CGRect
|
|
if isCompact {
|
|
mediaStatusFrame = CGRect(origin: CGPoint(x: 1.0 + originX, y: originY), size: CGSize(width: 20.0, height: 20.0))
|
|
} else {
|
|
mediaStatusFrame = CGRect(origin: CGPoint(x: 7.0 + originX, y: originY), size: CGSize(width: 28.0, height: 28.0))
|
|
}
|
|
mediaDownloadStatusNode.frame = mediaStatusFrame
|
|
mediaDownloadStatusNode.transitionToState(state, animated: badgeAnimated, completion: {})
|
|
} else if let mediaDownloadStatusNode = self.mediaDownloadStatusNode {
|
|
mediaDownloadStatusNode.transitionToState(.none, animated: badgeAnimated, completion: {})
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
return self.backgroundNode.frame.contains(point)
|
|
}
|
|
}
|
|
|