mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
294 lines
13 KiB
Swift
294 lines
13 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import AppBundle
|
|
import ContextUI
|
|
import TelegramStringFormatting
|
|
|
|
func generateStartRecordingIcon(color: UIColor) -> UIImage? {
|
|
return generateImage(CGSize(width: 18.0, height: 18.0), opaque: false, rotatedContext: { size, context in
|
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
|
context.clear(bounds)
|
|
context.setLineWidth(1.0 + UIScreenPixel)
|
|
context.setStrokeColor(color.cgColor)
|
|
context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0))
|
|
context.setFillColor(color.cgColor)
|
|
context.fillEllipse(in: bounds.insetBy(dx: 5.0, dy: 5.0))
|
|
})
|
|
}
|
|
|
|
final class VoiceChatRecordingContextItem: ContextMenuCustomItem {
|
|
fileprivate let timestamp: Int32
|
|
fileprivate let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void
|
|
|
|
init(timestamp: Int32, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) {
|
|
self.timestamp = timestamp
|
|
self.action = action
|
|
}
|
|
|
|
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
|
|
return VoiceChatRecordingContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected)
|
|
}
|
|
}
|
|
|
|
private let textFont = Font.regular(17.0)
|
|
|
|
class VoiceChatRecordingIconNode: ASDisplayNode {
|
|
private let backgroundNode: ASImageNode
|
|
private let dotNode: ASImageNode
|
|
|
|
init(hasBackground: Bool) {
|
|
let iconSize = 16.0 + (1.0 + UIScreenPixel) * 2.0
|
|
self.backgroundNode = ASImageNode()
|
|
self.backgroundNode.displaysAsynchronously = false
|
|
self.backgroundNode.displayWithoutProcessing = true
|
|
self.backgroundNode.image = generateCircleImage(diameter: iconSize, lineWidth: 1.0 + UIScreenPixel, color: UIColor.white, backgroundColor: nil)
|
|
self.backgroundNode.isLayerBacked = true
|
|
|
|
self.dotNode = ASImageNode()
|
|
self.dotNode.displaysAsynchronously = false
|
|
self.dotNode.displayWithoutProcessing = true
|
|
self.dotNode.image = generateFilledCircleImage(diameter: 8.0, color: UIColor(rgb: 0xff3b30))
|
|
self.dotNode.isLayerBacked = true
|
|
|
|
super.init()
|
|
|
|
self.isLayerBacked = true
|
|
|
|
if hasBackground {
|
|
self.addSubnode(self.backgroundNode)
|
|
}
|
|
self.addSubnode(self.dotNode)
|
|
}
|
|
|
|
override func didEnterHierarchy() {
|
|
self.setupAnimation()
|
|
}
|
|
|
|
override func didExitHierarchy() {
|
|
self.dotNode.layer.removeAllAnimations()
|
|
}
|
|
|
|
private func setupAnimation() {
|
|
let animation = CAKeyframeAnimation(keyPath: "opacity")
|
|
animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.55 as NSNumber]
|
|
animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
|
|
animation.duration = 0.7
|
|
animation.autoreverses = true
|
|
animation.repeatCount = Float.infinity
|
|
self.dotNode.layer.add(animation, forKey: "recording")
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
|
|
self.backgroundNode.frame = self.bounds
|
|
let dotSize = CGSize(width: 8.0, height: 8.0)
|
|
self.dotNode.frame = CGRect(origin: CGPoint(x: (self.bounds.width - dotSize.width) / 2.0, y: (self.bounds.height - dotSize.height) / 2.0), size: dotSize)
|
|
}
|
|
}
|
|
|
|
private final class VoiceChatRecordingContextItemNode: ASDisplayNode, ContextMenuCustomNode {
|
|
private let item: VoiceChatRecordingContextItem
|
|
private let presentationData: PresentationData
|
|
private let getController: () -> ContextControllerProtocol?
|
|
private let actionSelected: (ContextMenuActionResult) -> Void
|
|
|
|
private let backgroundNode: ASDisplayNode
|
|
private let highlightedBackgroundNode: ASDisplayNode
|
|
private let textNode: ImmediateTextNode
|
|
private let statusNode: ImmediateTextNode
|
|
private let iconNode: VoiceChatRecordingIconNode
|
|
private let buttonNode: HighlightTrackingButtonNode
|
|
|
|
private var timer: SwiftSignalKit.Timer?
|
|
|
|
private var pointerInteraction: PointerInteraction?
|
|
|
|
init(presentationData: PresentationData, item: VoiceChatRecordingContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
|
self.item = item
|
|
self.presentationData = presentationData
|
|
self.getController = getController
|
|
self.actionSelected = actionSelected
|
|
|
|
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
|
let subtextFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)
|
|
|
|
self.backgroundNode = ASDisplayNode()
|
|
self.backgroundNode.isAccessibilityElement = false
|
|
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
|
self.highlightedBackgroundNode = ASDisplayNode()
|
|
self.highlightedBackgroundNode.isAccessibilityElement = false
|
|
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
|
self.highlightedBackgroundNode.alpha = 0.0
|
|
|
|
self.textNode = ImmediateTextNode()
|
|
self.textNode.isAccessibilityElement = false
|
|
self.textNode.isUserInteractionEnabled = false
|
|
self.textNode.displaysAsynchronously = false
|
|
self.textNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_StopRecording, font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
|
|
|
|
self.textNode.maximumNumberOfLines = 1
|
|
let statusNode = ImmediateTextNode()
|
|
statusNode.isAccessibilityElement = false
|
|
statusNode.isUserInteractionEnabled = false
|
|
statusNode.displaysAsynchronously = false
|
|
statusNode.attributedText = NSAttributedString(string: "0:00", font: subtextFont, textColor: presentationData.theme.contextMenu.secondaryColor)
|
|
statusNode.maximumNumberOfLines = 1
|
|
self.statusNode = statusNode
|
|
|
|
self.buttonNode = HighlightTrackingButtonNode()
|
|
self.buttonNode.isAccessibilityElement = true
|
|
self.buttonNode.accessibilityLabel = presentationData.strings.VoiceChat_StopRecording
|
|
|
|
self.iconNode = VoiceChatRecordingIconNode(hasBackground: true)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
self.addSubnode(self.highlightedBackgroundNode)
|
|
self.addSubnode(self.textNode)
|
|
self.addSubnode(self.statusNode)
|
|
self.addSubnode(self.iconNode)
|
|
self.addSubnode(self.buttonNode)
|
|
|
|
self.buttonNode.highligthedChanged = { [weak self] highligted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if highligted {
|
|
strongSelf.highlightedBackgroundNode.alpha = 1.0
|
|
} else {
|
|
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
|
strongSelf.highlightedBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
|
}
|
|
}
|
|
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
|
}
|
|
|
|
deinit {
|
|
self.timer?.invalidate()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.pointerInteraction = PointerInteraction(node: self.buttonNode, style: .hover, willEnter: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.highlightedBackgroundNode.alpha = 0.75
|
|
}
|
|
}, willExit: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.highlightedBackgroundNode.alpha = 0.0
|
|
}
|
|
})
|
|
|
|
let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
|
|
self?.updateTime(transition: .immediate)
|
|
}, queue: Queue.mainQueue())
|
|
self.timer = timer
|
|
timer.start()
|
|
}
|
|
|
|
private var validLayout: CGSize?
|
|
func updateTime(transition: ContainedViewLayoutTransition) {
|
|
guard let size = self.validLayout else {
|
|
return
|
|
}
|
|
|
|
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
|
let duration = max(0, timestamp - item.timestamp)
|
|
|
|
let subtextFont = Font.regular(self.presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)
|
|
self.statusNode.attributedText = NSAttributedString(string: stringForDuration(Int32(duration)), font: subtextFont, textColor: presentationData.theme.contextMenu.secondaryColor)
|
|
|
|
let sideInset: CGFloat = 16.0
|
|
let statusSize = self.statusNode.updateLayout(CGSize(width: size.width - sideInset - 32.0, height: .greatestFiniteMagnitude))
|
|
transition.updateFrameAdditive(node: self.statusNode, frame: CGRect(origin: CGPoint(x: sideInset, y: self.statusNode.frame.minY), size: statusSize))
|
|
}
|
|
|
|
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
|
let sideInset: CGFloat = 16.0
|
|
let iconSideInset: CGFloat = 12.0
|
|
let verticalInset: CGFloat = 12.0
|
|
|
|
let iconSide = 16.0 + (1.0 + UIScreenPixel) * 2.0
|
|
let iconSize: CGSize = CGSize(width: iconSide, height: iconSide)
|
|
|
|
let standardIconWidth: CGFloat = 32.0
|
|
var rightTextInset: CGFloat = sideInset
|
|
if !iconSize.width.isZero {
|
|
rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset
|
|
}
|
|
|
|
let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude))
|
|
let statusSize = self.statusNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude))
|
|
|
|
let verticalSpacing: CGFloat = 2.0
|
|
let combinedTextHeight = textSize.height + verticalSpacing + statusSize.height
|
|
return (CGSize(width: max(textSize.width, statusSize.width) + sideInset + rightTextInset, height: verticalInset * 2.0 + combinedTextHeight), { size, transition in
|
|
let hadLayout = self.validLayout != nil
|
|
self.validLayout = size
|
|
|
|
if !hadLayout {
|
|
self.updateTime(transition: .immediate)
|
|
}
|
|
let verticalOrigin = floor((size.height - combinedTextHeight) / 2.0)
|
|
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin), size: textSize)
|
|
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
|
|
transition.updateFrameAdditive(node: self.statusNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalOrigin + verticalSpacing + textSize.height), size: textSize))
|
|
|
|
if !iconSize.width.isZero {
|
|
transition.updateFrameAdditive(node: self.iconNode, frame: CGRect(origin: CGPoint(x: size.width - standardIconWidth - iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize))
|
|
}
|
|
|
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
|
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
|
transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
|
})
|
|
}
|
|
|
|
func updateTheme(presentationData: PresentationData) {
|
|
self.backgroundNode.backgroundColor = presentationData.theme.contextMenu.itemBackgroundColor
|
|
self.highlightedBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
|
|
|
let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize)
|
|
let subtextFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)
|
|
|
|
self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: textFont, textColor: presentationData.theme.contextMenu.primaryColor)
|
|
self.statusNode.attributedText = NSAttributedString(string: self.statusNode.attributedText?.string ?? "", font: subtextFont, textColor: presentationData.theme.contextMenu.secondaryColor)
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
self.performAction()
|
|
}
|
|
|
|
func canBeHighlighted() -> Bool {
|
|
return true
|
|
}
|
|
|
|
func updateIsHighlighted(isHighlighted: Bool) {
|
|
self.setIsHighlighted(isHighlighted)
|
|
}
|
|
|
|
func performAction() {
|
|
guard let controller = self.getController() else {
|
|
return
|
|
}
|
|
self.item.action(controller, { [weak self] result in
|
|
self?.actionSelected(result)
|
|
})
|
|
}
|
|
|
|
func setIsHighlighted(_ value: Bool) {
|
|
if value {
|
|
self.highlightedBackgroundNode.alpha = 1.0
|
|
} else {
|
|
self.highlightedBackgroundNode.alpha = 0.0
|
|
}
|
|
}
|
|
}
|