[WIP] View-once audio and video messages

This commit is contained in:
Ilya Laktyushin 2023-12-27 22:29:02 +04:00
parent 6dcfc09165
commit 061e2c5c21
21 changed files with 1556 additions and 703 deletions

View File

@ -29,6 +29,7 @@ swift_library(
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",

View File

@ -199,7 +199,7 @@ private final class InnerActionsContainerNode: ASDisplayNode {
if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth {
minActionsWidth = minimalWidth
}
switch widthClass {
case .compact:
minActionsWidth = max(minActionsWidth, floor(constrainedWidth / 3.0))

View File

@ -2214,12 +2214,14 @@ public final class ContextController: ViewController, StandalonePresentableContr
public let title: String
public let source: ContextContentSource
public let items: Signal<ContextController.Items, NoError>
public let closeActionTitle: String?
public init(id: AnyHashable, title: String, source: ContextContentSource, items: Signal<ContextController.Items, NoError>) {
public init(id: AnyHashable, title: String, source: ContextContentSource, items: Signal<ContextController.Items, NoError>, closeActionTitle: String? = nil) {
self.id = id
self.title = title
self.source = source
self.items = items
self.closeActionTitle = closeActionTitle
}
}

View File

@ -1636,7 +1636,12 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
topItemWidth = lastItemLayout.size.width * (1.0 - transitionFraction) + previousItemLayout.size.width * transitionFraction
}
let navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight)))
let navigationContainerFrame: CGRect
if topItemApparentHeight > 0.0 {
navigationContainerFrame = CGRect(origin: CGPoint(), size: CGSize(width: topItemWidth, height: max(14 * 2.0, topItemApparentHeight)))
} else {
navigationContainerFrame = .zero
}
let previousNavigationContainerFrame = self.navigationContainer.frame
transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame, beginWithCurrentState: true)
self.navigationContainer.update(presentationData: presentationData, presentation: presentation, size: navigationContainerFrame.size, transition: transition)

View File

@ -1026,16 +1026,26 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
if let contentNode = itemContentNode {
var contentFrame = CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX, y: contentRect.minY - contentNode.containingItem.contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentNode.containingItem.view.bounds.size)
if case let .extracted(extracted) = self.source, extracted.centerVertically, contentFrame.midX > layout.size.width / 2.0 {
contentFrame.origin.x = layout.size.width - contentFrame.maxX
if case let .extracted(extracted) = self.source {
if extracted.centerVertically {
if combinedActionsFrame.height.isZero {
contentFrame.origin.y = floorToScreenPixels((layout.size.height - contentFrame.height) / 2.0)
} else if contentFrame.midX > layout.size.width / 2.0 {
contentFrame.origin.x = layout.size.width - contentFrame.maxX
}
}
}
contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true)
}
if let contentNode = controllerContentNode {
//TODO:
var contentFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentRect.size)
if case let .extracted(extracted) = self.source, extracted.centerVertically, contentFrame.midX > layout.size.width / 2.0 {
contentFrame.origin.x = layout.size.width - contentFrame.maxX
if case let .extracted(extracted) = self.source, extracted.centerVertically {
if combinedActionsFrame.height.isZero {
contentFrame.origin.y = floorToScreenPixels((layout.size.height - contentFrame.height) / 2.0)
} else if contentFrame.midX > layout.size.width / 2.0 {
contentFrame.origin.x = layout.size.width - contentFrame.maxX
}
}
contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true)
@ -1086,6 +1096,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
switch stateTransition {
case .animateIn:
let actionsSize = self.actionsContainerNode.bounds.size
if let contentNode = itemContentNode {
contentNode.takeContainingNode()
}
@ -1095,7 +1107,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.scroller.contentOffset = CGPoint(x: 0.0, y: defaultScrollY)
let animationInContentYDistance: CGFloat
var animationInContentYDistance: CGFloat
let currentContentScreenFrame: CGRect
if let contentNode = itemContentNode {
if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace {
@ -1109,20 +1121,27 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
var animationInContentXDistance: CGFloat = 0.0
let contentX = contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX
let contentY = contentParentGlobalFrame.minY + contentRect.minY - contentNode.containingItem.contentRect.minY
let contentWidth = contentNode.containingItem.view.bounds.size.width
if case let .extracted(extracted) = self.source, extracted.centerVertically, contentX + contentWidth > layout.size.width / 2.0 {
let fixedContentX = layout.size.width - (contentX + contentWidth)
animationInContentXDistance = fixedContentX - contentX
contentNode.layer.animateSpring(
from: -animationInContentXDistance as NSNumber, to: 0.0 as NSNumber,
keyPath: "position.x",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: true
)
let contentHeight = contentNode.containingItem.view.bounds.size.height
if case let .extracted(extracted) = self.source, extracted.centerVertically {
if actionsSize.height.isZero {
let fixedContentY = floorToScreenPixels((layout.size.height - contentHeight) / 2.0)
animationInContentYDistance = fixedContentY - contentY
} else if contentX + contentWidth > layout.size.width / 2.0, actionsSize.height > 0.0 {
let fixedContentX = layout.size.width - (contentX + contentWidth)
animationInContentXDistance = fixedContentX - contentX
contentNode.layer.animateSpring(
from: -animationInContentXDistance as NSNumber, to: 0.0 as NSNumber,
keyPath: "position.x",
duration: duration,
delay: 0.0,
initialVelocity: 0.0,
damping: springDamping,
additive: true
)
}
}
contentNode.layer.animateSpring(
@ -1178,9 +1197,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
damping: springDamping,
additive: false
)
let actionsSize = self.actionsContainerNode.bounds.size
var actionsPositionDeltaXDistance: CGFloat = 0.0
if case .center = actionsHorizontalAlignment {
actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX
@ -1262,6 +1279,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
}
case let .animateOut(result, completion):
let actionsSize = self.actionsContainerNode.bounds.size
let duration: Double
let timingFunction: String
switch result {
@ -1367,19 +1386,24 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
var animationInContentXDistance: CGFloat = 0.0
let contentX = contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX
let contentWidth = contentNode.containingItem.view.bounds.size.width
if case let .extracted(extracted) = self.source, extracted.centerVertically, contentX + contentWidth > layout.size.width / 2.0 {
let fixedContentX = layout.size.width - (contentX + contentWidth)
animationInContentXDistance = contentX - fixedContentX
contentNode.offsetContainerNode.layer.animate(
from: -animationInContentXDistance as NSNumber,
to: 0.0 as NSNumber,
keyPath: "position.x",
timingFunction: timingFunction,
duration: duration,
delay: 0.0,
additive: true
)
if case let .extracted(extracted) = self.source, extracted.centerVertically {
if actionsSize.height.isZero {
// let fixedContentY = floorToScreenPixels((layout.size.height - contentHeight) / 2.0)
animationInContentYDistance = 0.0 //contentY - fixedContentY
} else if contentX + contentWidth > layout.size.width / 2.0{
let fixedContentX = layout.size.width - (contentX + contentWidth)
animationInContentXDistance = contentX - fixedContentX
contentNode.offsetContainerNode.layer.animate(
from: -animationInContentXDistance as NSNumber,
to: 0.0 as NSNumber,
keyPath: "position.x",
timingFunction: timingFunction,
duration: duration,
delay: 0.0,
additive: true
)
}
}
contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: animationInContentXDistance, dy: -animationInContentYDistance)
@ -1459,9 +1483,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
}
)
let actionsSize = self.actionsContainerNode.bounds.size
var actionsPositionDeltaXDistance: CGFloat = 0.0
if case .center = actionsHorizontalAlignment {
actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX

View File

@ -7,6 +7,7 @@ import TelegramCore
import ReactionSelectionNode
import ComponentFlow
import TabSelectorComponent
import PlainButtonComponent
import ComponentDisplayAdapters
final class ContextSourceContainer: ASDisplayNode {
@ -16,6 +17,7 @@ final class ContextSourceContainer: ASDisplayNode {
let id: AnyHashable
let title: String
let source: ContextContentSource
let closeActionTitle: String?
private var _presentationNode: ContextControllerPresentationNode?
var presentationNode: ContextControllerPresentationNode {
@ -40,12 +42,14 @@ final class ContextSourceContainer: ASDisplayNode {
id: AnyHashable,
title: String,
source: ContextContentSource,
items: Signal<ContextController.Items, NoError>
items: Signal<ContextController.Items, NoError>,
closeActionTitle: String? = nil
) {
self.controller = controller
self.id = id
self.title = title
self.source = source
self.closeActionTitle = closeActionTitle
self.ready.set(combineLatest(queue: .mainQueue(), self.contentReady.get(), self.actionsReady.get())
|> map { a, b -> Bool in
@ -162,8 +166,11 @@ final class ContextSourceContainer: ASDisplayNode {
guard let self, let controller = self.controller else {
return
}
controller.controllerNode.dismissedForCancel?()
controller.controllerNode.beginDismiss(result)
if let _ = self.closeActionTitle {
} else {
controller.controllerNode.dismissedForCancel?()
controller.controllerNode.beginDismiss(result)
}
},
requestAnimateOut: { [weak self] result, completion in
guard let self, let controller = self.controller else {
@ -341,6 +348,7 @@ final class ContextSourceContainer: ASDisplayNode {
var activeIndex: Int = 0
private var tabSelector: ComponentView<Empty>?
private var closeButton: ComponentView<Empty>?
private var presentationData: PresentationData?
private var validLayout: ContainerViewLayout?
@ -376,7 +384,8 @@ final class ContextSourceContainer: ASDisplayNode {
id: source.id,
title: source.title,
source: source.source,
items: source.items
items: source.items,
closeActionTitle: source.closeActionTitle
)
self.sources.append(mappedSource)
self.addSubnode(mappedSource.presentationNode)
@ -457,6 +466,9 @@ final class ContextSourceContainer: ASDisplayNode {
if let tabSelectorView = self.tabSelector?.view {
tabSelectorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
if let closeButtonView = self.closeButton?.view {
closeButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) {
@ -465,6 +477,9 @@ final class ContextSourceContainer: ASDisplayNode {
if let tabSelectorView = self.tabSelector?.view {
tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
if let closeButtonView = self.closeButton?.view {
closeButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
if let activeSource = self.activeSource {
activeSource.animateOut(result: result, completion: completion)
@ -636,6 +651,43 @@ final class ContextSourceContainer: ASDisplayNode {
}
transition.updateFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - tabSelectorSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - tabSelectorSize.height), size: tabSelectorSize))
}
} else if let source = self.sources.first, let closeActionTitle = source.closeActionTitle {
let closeButton: ComponentView<Empty>
if let current = self.closeButton {
closeButton = current
} else {
closeButton = ComponentView()
self.closeButton = closeButton
}
let closeButtonSize = closeButton.update(
transition: Transition(transition),
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(
CloseButtonComponent(
backgroundColor: presentationData.theme.contextMenu.primaryColor.withMultipliedAlpha(0.1),
text: closeActionTitle
)
),
effectAlignment: .center,
action: { [weak self] in
guard let self else {
return
}
self.controller?.dismiss(result: .dismissWithoutContent, completion: nil)
})
),
environment: {},
containerSize: CGSize(width: layout.size.width, height: 44.0)
)
childLayout.intrinsicInsets.bottom += 30.0
if let closeButtonView = closeButton.view {
if closeButtonView.superview == nil {
self.view.addSubview(closeButtonView)
}
transition.updateFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - closeButtonSize.width) * 0.5), y: layout.size.height - layout.intrinsicInsets.bottom - closeButtonSize.height - 10.0), size: closeButtonSize))
}
} else if let tabSelector = self.tabSelector {
self.tabSelector = nil
tabSelector.view?.removeFromSuperview()
@ -664,6 +716,11 @@ final class ContextSourceContainer: ASDisplayNode {
return result
}
}
if let closeButtonView = self.closeButton?.view {
if let result = closeButtonView.hitTest(self.view.convert(point, to: closeButtonView), with: event) {
return result
}
}
guard let activeSource = self.activeSource else {
return nil
@ -671,3 +728,61 @@ final class ContextSourceContainer: ASDisplayNode {
return activeSource.presentationNode.view.hitTest(point, with: event)
}
}
private final class CloseButtonComponent: CombinedComponent {
let backgroundColor: UIColor
let text: String
init(
backgroundColor: UIColor,
text: String
) {
self.backgroundColor = backgroundColor
self.text = text
}
static func ==(lhs: CloseButtonComponent, rhs: CloseButtonComponent) -> Bool {
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.text != rhs.text {
return false
}
return true
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let text = Child(Text.self)
return { context in
let text = text.update(
component: Text(
text: "\(context.component.text)",
font: Font.regular(17.0),
color: .white
),
availableSize: CGSize(width: 200.0, height: 100.0),
transition: .immediate
)
let backgroundSize = CGSize(width: text.size.width + 34.0, height: 36.0)
let background = background.update(
component: RoundedRectangle(color: context.component.backgroundColor, cornerRadius: 18.0),
availableSize: backgroundSize,
transition: .immediate
)
context.add(background
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
)
context.add(text
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
)
return backgroundSize
}
}
}

View File

@ -6,7 +6,6 @@ import SwiftSignalKit
import RLottieBinding
import GZip
import AppBundle
import ManagedAnimationNode
public enum SemanticStatusNodeState: Equatable {
public struct ProgressAppearance: Equatable {
@ -33,25 +32,27 @@ public enum SemanticStatusNodeState: Equatable {
case pause
case check(appearance: CheckAppearance?)
case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?)
case secretTimeout(position: Double, duration: Double, generationTimestamp: Double, appearance: ProgressAppearance?)
case customIcon(UIImage)
}
private protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol {
protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol {
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor)
}
private protocol SemanticStatusNodeStateContext: AnyObject {
protocol SemanticStatusNodeStateContext: AnyObject {
var isAnimating: Bool { get }
var requestUpdate: () -> Void { get set }
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState
}
private enum SemanticStatusNodeIcon: Equatable {
enum SemanticStatusNodeIcon: Equatable {
case none
case download
case play
case pause
case secretTimeout
case custom(UIImage)
}
@ -88,535 +89,6 @@ private func svgPath(_ path: StaticString, scale: CGPoint = CGPoint(x: 1.0, y: 1
return path
}
private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContext {
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
let transitionFraction: CGFloat
let icon: SemanticStatusNodeIcon
let iconImage: UIImage?
let iconOffset: CGFloat
init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon, iconImage: UIImage?, iconOffset: CGFloat) {
self.transitionFraction = transitionFraction
self.icon = icon
self.iconImage = iconImage
self.iconOffset = iconOffset
super.init()
}
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) {
context.saveGState()
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction))
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
if foregroundColor.alpha.isZero {
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
} else {
context.setBlendMode(.normal)
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
}
switch self.icon {
case .none:
break
case .play:
let diameter = size.width
let factor = diameter / 50.0
let size: CGSize
var offset: CGFloat = 0.0
if let iconImage = self.iconImage {
size = iconImage.size
offset = self.iconOffset
} else {
offset = 1.5
size = CGSize(width: 15.0, height: 18.0)
}
context.translateBy(x: (diameter - size.width) / 2.0 + offset, y: (diameter - size.height) / 2.0)
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: factor, y: factor)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
if let iconImage = self.iconImage {
context.saveGState()
let iconRect = CGRect(origin: CGPoint(), size: iconImage.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, y: -size.height / 2.0)
context.clip(to: iconRect, mask: iconImage.cgImage!)
context.fill(iconRect)
context.restoreGState()
} else {
let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ")
context.fillPath()
}
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.translateBy(x: -(diameter - size.width) / 2.0 - offset, y: -(diameter - size.height) / 2.0)
case .pause:
let diameter = size.width
let factor = diameter / 50.0
let size: CGSize
let offset: CGFloat
if let iconImage = self.iconImage {
size = iconImage.size
offset = self.iconOffset
} else {
size = CGSize(width: 15.0, height: 16.0)
offset = 0.0
}
context.translateBy(x: (diameter - size.width) / 2.0 + offset, y: (diameter - size.height) / 2.0)
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: factor, y: factor)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
if let iconImage = self.iconImage {
context.saveGState()
let iconRect = CGRect(origin: CGPoint(), size: iconImage.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, y: -size.height / 2.0)
context.clip(to: iconRect, mask: iconImage.cgImage!)
context.fill(iconRect)
context.restoreGState()
} else {
let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ")
context.fillPath()
}
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0)
case let .custom(image):
let diameter = size.width
let imageRect = CGRect(origin: CGPoint(x: floor((diameter - image.size.width) / 2.0), y: floor((diameter - image.size.height) / 2.0)), size: image.size)
context.saveGState()
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.clip(to: imageRect, mask: image.cgImage!)
context.fill(imageRect)
context.restoreGState()
case .download:
let diameter = size.width
let factor = diameter / 50.0
let lineWidth: CGFloat = max(1.6, 2.25 * factor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
let arrowHeadSize: CGFloat = 15.0 * factor
let arrowLength: CGFloat = 18.0 * factor
let arrowHeadOffset: CGFloat = 1.0 * factor
let leftPath = UIBezierPath()
leftPath.lineWidth = lineWidth
leftPath.lineCapStyle = .round
leftPath.lineJoinStyle = .round
leftPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
leftPath.addLine(to: CGPoint(x: diameter / 2.0 - arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
leftPath.stroke()
let rightPath = UIBezierPath()
rightPath.lineWidth = lineWidth
rightPath.lineCapStyle = .round
rightPath.lineJoinStyle = .round
rightPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
rightPath.addLine(to: CGPoint(x: diameter / 2.0 + arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
rightPath.stroke()
let bodyPath = UIBezierPath()
bodyPath.lineWidth = lineWidth
bodyPath.lineCapStyle = .round
bodyPath.lineJoinStyle = .round
bodyPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 - arrowLength / 2.0))
bodyPath.addLine(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0))
bodyPath.stroke()
}
context.restoreGState()
}
}
var icon: SemanticStatusNodeIcon {
didSet {
self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: self.iconImage != nil)
}
}
var animationNode: PlayPauseIconNode?
var iconImage: UIImage?
var iconOffset: CGFloat = 0.0
init(icon: SemanticStatusNodeIcon) {
self.icon = icon
if [.play, .pause].contains(icon) {
self.animationNode = PlayPauseIconNode()
self.animationNode?.imageUpdated = { [weak self] image in
if let strongSelf = self {
strongSelf.iconImage = image
if var position = strongSelf.animationNode?.state?.position {
position = position * 2.0
if position > 1.0 {
position = 2.0 - position
}
strongSelf.iconOffset = (1.0 - position) * 1.5
}
strongSelf.requestUpdate()
}
}
self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: false)
self.iconImage = self.animationNode?.image
self.iconOffset = 1.5
}
}
var isAnimating: Bool {
return false
}
var requestUpdate: () -> Void = {}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
return DrawingState(transitionFraction: transitionFraction, icon: self.icon, iconImage: self.iconImage, iconOffset: self.iconOffset)
}
}
private final class SemanticStatusNodeProgressTransition {
let beginTime: Double
let initialValue: CGFloat
init(beginTime: Double, initialValue: CGFloat) {
self.beginTime = beginTime
self.initialValue = initialValue
}
func valueAt(timestamp: Double, actualValue: CGFloat) -> (CGFloat, Bool) {
let duration = 0.2
var t = CGFloat((timestamp - self.beginTime) / duration)
t = min(1.0, max(0.0, t))
return (t * actualValue + (1.0 - t) * self.initialValue, t >= 1.0 - 0.001)
}
}
private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext {
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
let transitionFraction: CGFloat
let value: CGFloat?
let displayCancel: Bool
let appearance: SemanticStatusNodeState.ProgressAppearance?
let timestamp: Double
init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, timestamp: Double) {
self.transitionFraction = transitionFraction
self.value = value
self.displayCancel = displayCancel
self.appearance = appearance
self.timestamp = timestamp
super.init()
}
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) {
let diameter = size.width
let factor = diameter / 50.0
context.saveGState()
if foregroundColor.alpha.isZero {
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
} else {
context.setBlendMode(.normal)
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
}
var progress: CGFloat
var startAngle: CGFloat
var endAngle: CGFloat
if let value = self.value {
progress = value
startAngle = -CGFloat.pi / 2.0
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
} else {
progress = CGFloat(1.0 + self.timestamp.remainder(dividingBy: 2.0))
startAngle = -CGFloat.pi / 2.0
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
}
let lineWidth: CGFloat
if let appearance = self.appearance {
lineWidth = appearance.lineWidth
} else {
lineWidth = max(1.6, 2.25 * factor)
}
let pathDiameter: CGFloat
if let appearance = self.appearance {
pathDiameter = diameter - lineWidth - appearance.inset * 2.0
} else {
pathDiameter = diameter - lineWidth - 2.5 * 2.0
}
var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0)
angle *= 4.0
context.translateBy(x: diameter / 2.0, y: diameter / 2.0)
context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0)))
context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0)
let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
context.restoreGState()
if self.displayCancel {
if foregroundColor.alpha.isZero {
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
} else {
context.setBlendMode(.normal)
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
}
context.saveGState()
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction))
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.setLineWidth(max(1.3, 2.0 * factor))
context.setLineCap(.round)
let crossSize: CGFloat = 14.0 * factor
context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0))
context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0))
context.strokePath()
context.move(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0))
context.addLine(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0))
context.strokePath()
context.restoreGState()
}
}
}
var value: CGFloat?
let displayCancel: Bool
let appearance: SemanticStatusNodeState.ProgressAppearance?
var transition: SemanticStatusNodeProgressTransition?
var isAnimating: Bool {
return true
}
var requestUpdate: () -> Void = {}
init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) {
self.value = value
self.displayCancel = displayCancel
self.appearance = appearance
}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
let timestamp = CACurrentMediaTime()
let resolvedValue: CGFloat?
if let value = self.value {
if let transition = self.transition {
let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value)
resolvedValue = v
if isCompleted {
self.transition = nil
}
} else {
resolvedValue = value
}
} else {
resolvedValue = nil
}
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp)
}
func maskView() -> UIView? {
return nil
}
func updateValue(value: CGFloat?) {
if value != self.value {
let previousValue = self.value
self.value = value
let timestamp = CACurrentMediaTime()
if let _ = value, let previousValue = previousValue {
if let transition = self.transition {
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: transition.valueAt(timestamp: timestamp, actualValue: previousValue).0)
} else {
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: previousValue)
}
} else {
self.transition = nil
}
}
}
}
private final class SemanticStatusNodeCheckContext: SemanticStatusNodeStateContext {
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
let transitionFraction: CGFloat
let value: CGFloat
let appearance: SemanticStatusNodeState.CheckAppearance?
init(transitionFraction: CGFloat, value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) {
self.transitionFraction = transitionFraction
self.value = value
self.appearance = appearance
super.init()
}
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) {
let diameter = size.width
let factor = diameter / 50.0
context.saveGState()
if foregroundColor.alpha.isZero {
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
} else {
context.setBlendMode(.normal)
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
}
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
let lineWidth: CGFloat
if let appearance = self.appearance {
lineWidth = appearance.lineWidth
} else {
lineWidth = max(1.6, 2.25 * factor)
}
context.setLineWidth(max(1.7, lineWidth * factor))
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
let progress = self.value
let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor)
var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor)
var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor)
if diameter < 36.0 {
s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor)
p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor)
p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor)
}
if !firstSegment.isZero {
if firstSegment < 1.0 {
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
context.addLine(to: s)
} else {
let secondSegment = (progress - 0.33) * 1.5
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
context.addLine(to: s)
}
}
context.strokePath()
}
}
var value: CGFloat
let appearance: SemanticStatusNodeState.CheckAppearance?
var transition: SemanticStatusNodeProgressTransition?
var isAnimating: Bool {
return true
}
var requestUpdate: () -> Void = {}
init(value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) {
self.value = value
self.appearance = appearance
self.animate()
}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
let timestamp = CACurrentMediaTime()
let resolvedValue: CGFloat
if let transition = self.transition {
let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value)
resolvedValue = v
if isCompleted {
self.transition = nil
}
} else {
resolvedValue = value
}
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, appearance: self.appearance)
}
func maskView() -> UIView? {
return nil
}
func animate() {
guard self.value < 1.0 else {
return
}
let timestamp = CACurrentMediaTime()
self.value = 1.0
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: 0.0)
}
}
private extension SemanticStatusNodeState {
func context(current: SemanticStatusNodeStateContext?) -> SemanticStatusNodeStateContext {
switch self {
@ -633,6 +105,8 @@ private extension SemanticStatusNodeState {
icon = .pause
case let .customIcon(image):
icon = .custom(image)
case .secretTimeout:
icon = .none
default:
preconditionFailure()
}
@ -654,6 +128,13 @@ private extension SemanticStatusNodeState {
} else {
return SemanticStatusNodeCheckContext(value: 0.0, appearance: appearance)
}
case let .secretTimeout(position, duration, generationTimestamp, appearance):
if let current = current as? SemanticStatusNodeSecretTimeoutContext {
current.updateValue(position: position, duration: duration, generationTimestamp: generationTimestamp)
return current
} else {
return SemanticStatusNodeSecretTimeoutContext(position: position, duration: duration, generationTimestamp: generationTimestamp, appearance: appearance)
}
case let .progress(value, cancelEnabled, appearance):
if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled {
current.updateValue(value: value)
@ -1048,53 +529,3 @@ public final class SemanticStatusNode: ASControlNode {
parameters.appearanceState.drawForeground(context: context, size: bounds.size)
}
}
private enum PlayPauseIconNodeState: Equatable {
case play
case pause
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.35
private var iconState: PlayPauseIconNodeState = .play
init() {
super.init(size: CGSize(width: 36.0, height: 36.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .play:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .play:
switch state {
case .pause:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
case .play:
break
}
}
}
}

View File

@ -0,0 +1,123 @@
import Foundation
import UIKit
import Display
final class SemanticStatusNodeCheckContext: SemanticStatusNodeStateContext {
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
let transitionFraction: CGFloat
let value: CGFloat
let appearance: SemanticStatusNodeState.CheckAppearance?
init(transitionFraction: CGFloat, value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) {
self.transitionFraction = transitionFraction
self.value = value
self.appearance = appearance
super.init()
}
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) {
let diameter = size.width
let factor = diameter / 50.0
context.saveGState()
if foregroundColor.alpha.isZero {
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
} else {
context.setBlendMode(.normal)
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
}
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
let lineWidth: CGFloat
if let appearance = self.appearance {
lineWidth = appearance.lineWidth
} else {
lineWidth = max(1.6, 2.25 * factor)
}
context.setLineWidth(max(1.7, lineWidth * factor))
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
let progress = self.value
let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor)
var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor)
var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor)
if diameter < 36.0 {
s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor)
p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor)
p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor)
}
if !firstSegment.isZero {
if firstSegment < 1.0 {
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
context.addLine(to: s)
} else {
let secondSegment = (progress - 0.33) * 1.5
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
context.addLine(to: s)
}
}
context.strokePath()
}
}
var value: CGFloat
let appearance: SemanticStatusNodeState.CheckAppearance?
var transition: SemanticStatusNodeProgressTransition?
var isAnimating: Bool {
return true
}
var requestUpdate: () -> Void = {}
init(value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) {
self.value = value
self.appearance = appearance
self.animate()
}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
let timestamp = CACurrentMediaTime()
let resolvedValue: CGFloat
if let transition = self.transition {
let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value)
resolvedValue = v
if isCompleted {
self.transition = nil
}
} else {
resolvedValue = value
}
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, appearance: self.appearance)
}
func maskView() -> UIView? {
return nil
}
func animate() {
guard self.value < 1.0 else {
return
}
let timestamp = CACurrentMediaTime()
self.value = 1.0
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: 0.0)
}
}

View File

@ -0,0 +1,262 @@
import Foundation
import UIKit
import Display
import ManagedAnimationNode
final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContext {
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
let transitionFraction: CGFloat
let icon: SemanticStatusNodeIcon
let iconImage: UIImage?
let iconOffset: CGFloat
init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon, iconImage: UIImage?, iconOffset: CGFloat) {
self.transitionFraction = transitionFraction
self.icon = icon
self.iconImage = iconImage
self.iconOffset = iconOffset
super.init()
}
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) {
context.saveGState()
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction))
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
if foregroundColor.alpha.isZero {
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
} else {
context.setBlendMode(.normal)
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
}
switch self.icon {
case .none, .secretTimeout:
break
case .play:
let diameter = size.width
let factor = diameter / 50.0
let size: CGSize
var offset: CGFloat = 0.0
if let iconImage = self.iconImage {
size = iconImage.size
offset = self.iconOffset
} else {
offset = 1.5
size = CGSize(width: 15.0, height: 18.0)
}
context.translateBy(x: (diameter - size.width) / 2.0 + offset, y: (diameter - size.height) / 2.0)
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: factor, y: factor)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
if let iconImage = self.iconImage {
context.saveGState()
let iconRect = CGRect(origin: CGPoint(), size: iconImage.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, y: -size.height / 2.0)
context.clip(to: iconRect, mask: iconImage.cgImage!)
context.fill(iconRect)
context.restoreGState()
} else {
let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ")
context.fillPath()
}
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.translateBy(x: -(diameter - size.width) / 2.0 - offset, y: -(diameter - size.height) / 2.0)
case .pause:
let diameter = size.width
let factor = diameter / 50.0
let size: CGSize
let offset: CGFloat
if let iconImage = self.iconImage {
size = iconImage.size
offset = self.iconOffset
} else {
size = CGSize(width: 15.0, height: 16.0)
offset = 0.0
}
context.translateBy(x: (diameter - size.width) / 2.0 + offset, y: (diameter - size.height) / 2.0)
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: factor, y: factor)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
if let iconImage = self.iconImage {
context.saveGState()
let iconRect = CGRect(origin: CGPoint(), size: iconImage.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, y: -size.height / 2.0)
context.clip(to: iconRect, mask: iconImage.cgImage!)
context.fill(iconRect)
context.restoreGState()
} else {
let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ")
context.fillPath()
}
if (diameter < 40.0) {
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
}
context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0)
case let .custom(image):
let diameter = size.width
let imageRect = CGRect(origin: CGPoint(x: floor((diameter - image.size.width) / 2.0), y: floor((diameter - image.size.height) / 2.0)), size: image.size)
context.saveGState()
context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.clip(to: imageRect, mask: image.cgImage!)
context.fill(imageRect)
context.restoreGState()
case .download:
let diameter = size.width
let factor = diameter / 50.0
let lineWidth: CGFloat = max(1.6, 2.25 * factor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
context.setLineJoin(.round)
let arrowHeadSize: CGFloat = 15.0 * factor
let arrowLength: CGFloat = 18.0 * factor
let arrowHeadOffset: CGFloat = 1.0 * factor
let leftPath = UIBezierPath()
leftPath.lineWidth = lineWidth
leftPath.lineCapStyle = .round
leftPath.lineJoinStyle = .round
leftPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
leftPath.addLine(to: CGPoint(x: diameter / 2.0 - arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
leftPath.stroke()
let rightPath = UIBezierPath()
rightPath.lineWidth = lineWidth
rightPath.lineCapStyle = .round
rightPath.lineJoinStyle = .round
rightPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
rightPath.addLine(to: CGPoint(x: diameter / 2.0 + arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
rightPath.stroke()
let bodyPath = UIBezierPath()
bodyPath.lineWidth = lineWidth
bodyPath.lineCapStyle = .round
bodyPath.lineJoinStyle = .round
bodyPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 - arrowLength / 2.0))
bodyPath.addLine(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0))
bodyPath.stroke()
}
context.restoreGState()
}
}
var icon: SemanticStatusNodeIcon {
didSet {
self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: self.iconImage != nil)
}
}
private var animationNode: PlayPauseIconNode?
private var iconImage: UIImage?
private var iconOffset: CGFloat = 0.0
init(icon: SemanticStatusNodeIcon) {
self.icon = icon
if [.play, .pause].contains(icon) {
self.animationNode = PlayPauseIconNode()
self.animationNode?.imageUpdated = { [weak self] image in
if let strongSelf = self {
strongSelf.iconImage = image
if var position = strongSelf.animationNode?.state?.position {
position = position * 2.0
if position > 1.0 {
position = 2.0 - position
}
strongSelf.iconOffset = (1.0 - position) * 1.5
}
strongSelf.requestUpdate()
}
}
self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: false)
self.iconImage = self.animationNode?.image
self.iconOffset = 1.5
}
}
var isAnimating: Bool {
return false
}
var requestUpdate: () -> Void = {}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
return DrawingState(transitionFraction: transitionFraction, icon: self.icon, iconImage: self.iconImage, iconOffset: self.iconOffset)
}
}
private enum PlayPauseIconNodeState: Equatable {
case play
case pause
}
private final class PlayPauseIconNode: ManagedAnimationNode {
private let duration: Double = 0.35
private var iconState: PlayPauseIconNodeState = .play
init() {
super.init(size: CGSize(width: 36.0, height: 36.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
switch previousState {
case .pause:
switch state {
case .play:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
}
case .pause:
break
}
case .play:
switch state {
case .pause:
if animated {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
} else {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
}
case .play:
break
}
}
}
}

View File

@ -0,0 +1,204 @@
import Foundation
import UIKit
import Display
final class SemanticStatusNodeProgressTransition {
let beginTime: Double
let initialValue: CGFloat
init(beginTime: Double, initialValue: CGFloat) {
self.beginTime = beginTime
self.initialValue = initialValue
}
func valueAt(timestamp: Double, actualValue: CGFloat) -> (CGFloat, Bool) {
let duration = 0.2
var t = CGFloat((timestamp - self.beginTime) / duration)
t = min(1.0, max(0.0, t))
return (t * actualValue + (1.0 - t) * self.initialValue, t >= 1.0 - 0.001)
}
}
final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext {
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
let transitionFraction: CGFloat
let value: CGFloat?
let displayCancel: Bool
let appearance: SemanticStatusNodeState.ProgressAppearance?
let timestamp: Double
init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, timestamp: Double) {
self.transitionFraction = transitionFraction
self.value = value
self.displayCancel = displayCancel
self.appearance = appearance
self.timestamp = timestamp
super.init()
}
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) {
let diameter = size.width
let factor = diameter / 50.0
context.saveGState()
if foregroundColor.alpha.isZero {
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
} else {
context.setBlendMode(.normal)
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
}
var progress: CGFloat
var startAngle: CGFloat
var endAngle: CGFloat
if let value = self.value {
progress = value
startAngle = -CGFloat.pi / 2.0
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
} else {
progress = CGFloat(1.0 + self.timestamp.remainder(dividingBy: 2.0))
startAngle = -CGFloat.pi / 2.0
endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
}
let lineWidth: CGFloat
if let appearance = self.appearance {
lineWidth = appearance.lineWidth
} else {
lineWidth = max(1.6, 2.25 * factor)
}
let pathDiameter: CGFloat
if let appearance = self.appearance {
pathDiameter = diameter - lineWidth - appearance.inset * 2.0
} else {
pathDiameter = diameter - lineWidth - 2.5 * 2.0
}
var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0)
angle *= 4.0
context.translateBy(x: diameter / 2.0, y: diameter / 2.0)
context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0)))
context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0)
let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
context.restoreGState()
if self.displayCancel {
if foregroundColor.alpha.isZero {
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
} else {
context.setBlendMode(.normal)
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
}
context.saveGState()
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction))
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.setLineWidth(max(1.3, 2.0 * factor))
context.setLineCap(.round)
let crossSize: CGFloat = 14.0 * factor
context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0))
context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0))
context.strokePath()
context.move(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0))
context.addLine(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0))
context.strokePath()
context.restoreGState()
}
}
}
var value: CGFloat?
let displayCancel: Bool
let appearance: SemanticStatusNodeState.ProgressAppearance?
var transition: SemanticStatusNodeProgressTransition?
var isAnimating: Bool {
return true
}
var requestUpdate: () -> Void = {}
init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) {
self.value = value
self.displayCancel = displayCancel
self.appearance = appearance
}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
let timestamp = CACurrentMediaTime()
let resolvedValue: CGFloat?
if let value = self.value {
if let transition = self.transition {
let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value)
resolvedValue = v
if isCompleted {
self.transition = nil
}
} else {
resolvedValue = value
}
} else {
resolvedValue = nil
}
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp)
}
func maskView() -> UIView? {
return nil
}
func updateValue(value: CGFloat?) {
if value != self.value {
let previousValue = self.value
self.value = value
let timestamp = CACurrentMediaTime()
if let _ = value, let previousValue = previousValue {
if let transition = self.transition {
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: transition.valueAt(timestamp: timestamp, actualValue: previousValue).0)
} else {
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: previousValue)
}
} else {
self.transition = nil
}
}
}
}

View File

@ -0,0 +1,226 @@
import Foundation
import UIKit
import Display
import ManagedAnimationNode
final class SemanticStatusNodeSecretTimeoutContext: SemanticStatusNodeStateContext {
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
let transitionFraction: CGFloat
let value: CGFloat
let appearance: SemanticStatusNodeState.ProgressAppearance?
let iconImage: UIImage?
fileprivate let particles: [ContentParticle]
fileprivate init(transitionFraction: CGFloat, value: CGFloat, appearance: SemanticStatusNodeState.ProgressAppearance?, iconImage: UIImage?, particles: [ContentParticle]) {
self.transitionFraction = transitionFraction
self.value = value
self.appearance = appearance
self.iconImage = iconImage
self.particles = particles
super.init()
}
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) {
let diameter = size.width
let factor = diameter / 50.0
context.saveGState()
if foregroundColor.alpha.isZero {
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
} else {
context.setBlendMode(.normal)
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
}
var progress = self.value
progress = min(1.0, progress)
let endAngle = -CGFloat.pi / 2.0
let startAngle = CGFloat(progress) * 2.0 * CGFloat.pi + endAngle
let lineWidth: CGFloat
if let appearance = self.appearance {
lineWidth = appearance.lineWidth
} else {
lineWidth = max(1.6, 2.25 * factor)
}
let pathDiameter: CGFloat
if let appearance = self.appearance {
pathDiameter = diameter - lineWidth - appearance.inset * 2.0
} else {
pathDiameter = diameter - lineWidth - 2.5 * 2.0
}
let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
if let iconImage = self.iconImage {
context.saveGState()
let iconRect = CGRect(origin: CGPoint(), size: iconImage.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, y: -size.height / 2.0)
context.translateBy(x: 4.0, y: 7.0)
context.clip(to: iconRect, mask: iconImage.cgImage!)
context.fill(iconRect)
context.restoreGState()
}
for particle in self.particles {
let size: CGFloat = 1.3
context.setAlpha(particle.alpha)
context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size)))
}
context.restoreGState()
}
}
var position: Double
var duration: Double
var generationTimestamp: Double
let appearance: SemanticStatusNodeState.ProgressAppearance?
fileprivate var particles: [ContentParticle] = []
private var animationNode: FireIconNode?
private var iconImage: UIImage?
var isAnimating: Bool {
return true
}
var requestUpdate: () -> Void = {}
init(position: Double, duration: Double, generationTimestamp: Double, appearance: SemanticStatusNodeState.ProgressAppearance?) {
self.position = position
self.duration = duration
self.generationTimestamp = generationTimestamp
self.appearance = appearance
self.animationNode = FireIconNode()
self.animationNode?.imageUpdated = { [weak self] image in
if let strongSelf = self {
strongSelf.iconImage = image
strongSelf.requestUpdate()
}
}
self.iconImage = self.animationNode?.image
}
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
let timestamp = CACurrentMediaTime()
let position = self.position + (timestamp - self.generationTimestamp)
let resolvedValue: CGFloat
if self.duration > 0.0 {
resolvedValue = position / self.duration
} else {
resolvedValue = 0.0
}
let size = CGSize(width: 44.0, height: 44.0)
let lineWidth: CGFloat
let lineInset: CGFloat
if let appearance = self.appearance {
lineWidth = appearance.lineWidth
lineInset = appearance.inset
} else {
lineWidth = 2.0
lineInset = 1.0
}
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
let radius: CGFloat = (size.width - lineWidth - lineInset * 2.0) * 0.5
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * resolvedValue
let v = CGPoint(x: sin(endAngle), y: -cos(endAngle))
let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y)
let dt: CGFloat = 1.0 / 60.0
var removeIndices: [Int] = []
for i in 0 ..< self.particles.count {
let currentTime = timestamp - self.particles[i].beginTime
if currentTime > self.particles[i].lifetime {
removeIndices.append(i)
} else {
let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime)
let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input))
self.particles[i].alpha = 1.0 - decelerated
var p = self.particles[i].position
let d = self.particles[i].direction
let v = self.particles[i].velocity
p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt)
self.particles[i].position = p
}
}
for i in removeIndices.reversed() {
self.particles.remove(at: i)
}
let newParticleCount = 1
for _ in 0 ..< newParticleCount {
let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 70.0
let angle: CGFloat = degrees * CGFloat.pi / 180.0
let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle))
let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.5
let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01)
let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp)
self.particles.append(particle)
}
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, appearance: self.appearance, iconImage: self.iconImage, particles: self.particles)
}
func maskView() -> UIView? {
return nil
}
func updateValue(position: Double, duration: Double, generationTimestamp: Double) {
self.position = position
self.duration = duration
self.generationTimestamp = generationTimestamp
}
}
private struct ContentParticle {
var position: CGPoint
var direction: CGPoint
var velocity: CGFloat
var alpha: CGFloat
var lifetime: Double
var beginTime: Double
init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) {
self.position = position
self.direction = direction
self.velocity = velocity
self.alpha = alpha
self.lifetime = lifetime
self.beginTime = beginTime
}
}
private final class FireIconNode: ManagedAnimationNode {
init() {
super.init(size: CGSize(width: 36.0, height: 36.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 0, endFrame: 80), duration: 2.5))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 80, endFrame: 115), duration: 0.85, loop: true))
}
}

View File

@ -185,6 +185,8 @@ public final class PrincipalThemeEssentialGraphics {
public let radialIndicatorFileIconIncoming: UIImage
public let radialIndicatorFileIconOutgoing: UIImage
public let radialIndicatorViewOnceIcon: UIImage
public let incomingBubbleGradientImage: UIImage?
public let outgoingBubbleGradientImage: UIImage?
@ -370,6 +372,8 @@ public final class PrincipalThemeEssentialGraphics {
self.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)!
self.radialIndicatorFileIconOutgoing = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)!
self.radialIndicatorViewOnceIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ViewOnce"), color: .black)!
} else {
self.chatMessageBackgroundIncomingMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .none, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
self.chatMessageBackgroundIncomingExtractedMaskImage = messageBubbleImage(maxCornerRadius: maxCornerRadius, minCornerRadius: minCornerRadius, incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .extracted, theme: theme, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
@ -489,6 +493,8 @@ public final class PrincipalThemeEssentialGraphics {
self.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)!
self.radialIndicatorFileIconOutgoing = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)!
self.radialIndicatorViewOnceIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ViewOnce"), color: .black)!
}
let chatDateSize: CGFloat = 20.0

View File

@ -42,6 +42,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/AnimatedCountLabelNode",
],
visibility = [
"//visibility:public",

View File

@ -33,6 +33,7 @@ import ChatMessageDateAndStatusNode
import ChatHistoryEntry
import ChatMessageItemCommon
import TelegramStringFormatting
import AnimatedCountLabelNode
private struct FetchControls {
let fetch: (Bool) -> Void
@ -120,6 +121,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
private let descriptionMeasuringNode: TextNode
public let fetchingTextNode: ImmediateTextNode
public let fetchingCompactTextNode: ImmediateTextNode
private let countNode: ImmediateAnimatedCountLabelNode
public var waveformView: ComponentHostView<Empty>?
@ -194,6 +196,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var progressFrame: CGRect?
private var streamingCacheStatusFrame: CGRect?
private var fileIconImage: UIImage?
private var viewOnceIconImage: UIImage?
public var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed
public var forcedAudioTranscriptionText: TranscribedText?
@ -218,6 +221,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.descriptionNode.displaysAsynchronously = false
self.descriptionNode.isUserInteractionEnabled = false
self.countNode = ImmediateAnimatedCountLabelNode()
self.countNode.alwaysOneDirection = true
self.descriptionMeasuringNode = TextNode()
self.fetchingTextNode = ImmediateTextNode()
@ -733,6 +739,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: hasThumbnail ? 2 : 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let isViewOnceMessage = "".isEmpty || arguments.message.autoremoveAttribute?.timeout == viewOnceTimeout
let fileSizeString: String
if let _ = arguments.file.size {
fileSizeString = "000.0 MB"
@ -747,7 +755,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState?
var displayTranscribe = false
if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !arguments.presentationData.isPreview {
if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
if arguments.associatedData.isPremium {
displayTranscribe = true
@ -965,12 +973,14 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
minLayoutWidth = max(minLayoutWidth, textLayout.size.width + horizontalInset)
let fileIconImage: UIImage?
var viewOnceIconImage: UIImage?
if hasThumbnail {
fileIconImage = nil
} else {
let principalGraphics = PresentationResourcesChat.principalGraphics(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners)
fileIconImage = arguments.incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing
viewOnceIconImage = principalGraphics.radialIndicatorViewOnceIcon
}
return (minLayoutWidth, { boundingWidth in
@ -1050,7 +1060,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
strongSelf.titleNode.frame = titleFrame
strongSelf.descriptionNode.frame = descriptionFrame
strongSelf.descriptionMeasuringNode.frame = CGRect(origin: CGPoint(), size: descriptionMeasuringLayout.size)
if let updatedAudioTranscriptionState = updatedAudioTranscriptionState {
strongSelf.audioTranscriptionState = updatedAudioTranscriptionState
}
@ -1432,7 +1442,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
strongSelf.progressFrame = progressFrame
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
strongSelf.fileIconImage = fileIconImage
strongSelf.viewOnceIconImage = viewOnceIconImage
if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
if arguments.automaticDownload {
@ -1548,6 +1559,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}
}
let isViewOnceMessage = "".isEmpty || (isVoice && message.autoremoveAttribute?.timeout == viewOnceTimeout)
var state: SemanticStatusNodeState
var streamingState: SemanticStatusNodeState = .none
@ -1556,6 +1568,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
var downloadingStrings: (String, String, UIFont)?
var playbackState: (position: Double, duration: Double, generationTimestamp: Double) = (0.0, 0.0, 0.0)
if !isAudio {
var fetchStatus: MediaResourceStatus?
if let actualFetchStatus = self.actualFetchStatus, message.forwardInfo != nil {
@ -1579,75 +1592,84 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
} else if isVoice {
if let playerStatus = self.playerStatus {
var playerPosition: Int32?
var playerDuration: Int32 = 0
var playerPosition: Double?
var playerDuration: Double = 0.0
if !playerStatus.generationTimestamp.isZero, case .playing = playerStatus.status {
playerPosition = Int32(playerStatus.timestamp + (CACurrentMediaTime() - playerStatus.generationTimestamp))
playerPosition = playerStatus.timestamp + (CACurrentMediaTime() - playerStatus.generationTimestamp)
} else {
playerPosition = Int32(playerStatus.timestamp)
playerPosition = playerStatus.timestamp
}
playerDuration = Int32(playerStatus.duration)
playerDuration = playerStatus.duration
let durationString = stringForDuration(playerDuration > 0 ? playerDuration : (audioDuration ?? 0), position: playerPosition)
let effectiveDuration = playerDuration > 0 ? playerDuration : Double(audioDuration ?? 0)
let durationString = stringForDuration(Int32(effectiveDuration), position: playerPosition.flatMap { Int32($0) })
let durationFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0))
downloadingStrings = (durationString, durationString, durationFont)
playbackState = (playerStatus.timestamp, playerDuration, playerStatus.generationTimestamp)
}
}
switch resourceStatus.mediaStatus {
case var .fetchStatus(fetchStatus):
if self.message?.forwardInfo != nil {
fetchStatus = resourceStatus.fetchStatus
}
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false
//self.waveformScrubbingNode?.enableScrubbing = false
case var .fetchStatus(fetchStatus):
if self.message?.forwardInfo != nil {
fetchStatus = resourceStatus.fetchStatus
}
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false
switch fetchStatus {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
var wasCheck = false
if let statusNode = self.statusNode, case .check = statusNode.state {
wasCheck = true
}
if isAudio && !isVoice && !isSending {
state = .play
} else {
if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) {
state = .check(appearance: nil)
} else {
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
}
}
case .Local:
if isAudio {
state = .play
} else if let fileIconImage = self.fileIconImage {
state = .customIcon(fileIconImage)
} else {
state = .none
}
case .Remote, .Paused:
if isAudio && !isVoice {
state = .play
} else {
state = .download
}
switch fetchStatus {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
var wasCheck = false
if let statusNode = self.statusNode, case .check = statusNode.state {
wasCheck = true
}
case let .playbackStatus(playbackStatus):
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = true
//self.waveformScrubbingNode?.enableScrubbing = true
if isAudio && !isVoice && !isSending {
state = .play
} else {
if message.groupingKey != nil, adjustedProgress.isEqual(to: 1.0), (message.flags.contains(.Unsent) || wasCheck) {
state = .check(appearance: nil)
} else {
state = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: nil)
}
}
case .Local:
if isAudio {
state = .play
} else if let fileIconImage = self.fileIconImage {
state = .customIcon(fileIconImage)
} else {
state = .none
}
case .Remote, .Paused:
if isAudio && !isVoice {
state = .play
} else {
state = .download
}
}
case let .playbackStatus(playbackStatus):
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = !isViewOnceMessage
if isViewOnceMessage && playbackStatus == .playing {
state = .secretTimeout(position: playbackState.position, duration: playbackState.duration, generationTimestamp: playbackState.generationTimestamp, appearance: .init(inset: 1.0 + UIScreenPixel, lineWidth: 2.0 - UIScreenPixel))
} else {
switch playbackStatus {
case .playing:
state = .pause
case .paused:
state = .play
case .playing:
state = .pause
case .paused:
state = .play
}
}
}
if isAudio && !isVoice && !isSending && state != .pause {
switch resourceStatus.fetchStatus {
if isViewOnceMessage, let viewOnceIconImage = self.viewOnceIconImage, state == .play {
streamingState = .customIcon(viewOnceIconImage)
} else {
if isAudio && !isVoice && !isSending && state != .pause {
switch resourceStatus.fetchStatus {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0))
@ -1655,9 +1677,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
streamingState = .none
case .Remote, .Paused:
streamingState = .download
}
} else {
streamingState = .none
}
} else {
streamingState = .none
}
if isSending {
@ -1721,7 +1744,13 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
let effectsEnabled = self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true
if case .pause = state, isVoice, self.playbackAudioLevelNode == nil, effectsEnabled {
var showBlobs = false
if case .pause = state {
showBlobs = true
} else if case .secretTimeout = state {
showBlobs = true
}
if showBlobs, isVoice, self.playbackAudioLevelNode == nil, effectsEnabled {
let blobFrame = progressFrame.insetBy(dx: -12.0, dy: -12.0)
let playbackAudioLevelNode = VoiceBlobNode(
maxLevel: 0.3,
@ -1801,9 +1830,28 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
statusNode.setCutout(cutoutFrame, animated: true)
}
var displayingCountdown = false
if let (expandedString, compactString, font) = downloadingStrings {
self.fetchingTextNode.attributedText = NSAttributedString(string: expandedString, font: font, textColor: messageTheme.fileDurationColor)
self.fetchingCompactTextNode.attributedText = NSAttributedString(string: compactString, font: font, textColor: messageTheme.fileDurationColor)
if isViewOnceMessage {
var segments: [AnimatedCountLabelNode.Segment] = []
var textCount = 0
for char in expandedString {
if let intValue = Int(String(char)) {
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: messageTheme.fileDurationColor)))
} else {
segments.append(.text(textCount, NSAttributedString(string: String(char), font: font, textColor: messageTheme.fileDurationColor)))
textCount += 1
}
}
if self.countNode.supernode == nil {
self.addSubnode(self.countNode)
}
self.countNode.segments = segments
displayingCountdown = true
}
} else {
self.fetchingTextNode.attributedText = nil
self.fetchingCompactTextNode.attributedText = nil
@ -1812,24 +1860,32 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
let maxFetchingStatusWidth = max(self.titleNode.frame.width, self.descriptionMeasuringNode.frame.width) + 2.0
let fetchingInfo = self.fetchingTextNode.updateLayoutInfo(CGSize(width: maxFetchingStatusWidth, height: CGFloat.greatestFiniteMagnitude))
let fetchingCompactSize = self.fetchingCompactTextNode.updateLayout(CGSize(width: maxFetchingStatusWidth, height: CGFloat.greatestFiniteMagnitude))
let countSize = self.countNode.updateLayout(size: CGSize(width: maxFetchingStatusWidth, height: CGFloat.greatestFiniteMagnitude), animated: true)
if downloadingStrings != nil {
self.descriptionNode.isHidden = true
if fetchingInfo.truncated {
self.fetchingTextNode.isHidden = true
self.fetchingCompactTextNode.isHidden = false
} else {
self.fetchingTextNode.isHidden = false
self.fetchingCompactTextNode.isHidden = true
}
} else {
self.descriptionNode.isHidden = false
if displayingCountdown {
self.fetchingTextNode.isHidden = true
self.fetchingCompactTextNode.isHidden = true
self.descriptionNode.isHidden = true
} else {
if downloadingStrings != nil {
self.descriptionNode.isHidden = true
if fetchingInfo.truncated {
self.fetchingTextNode.isHidden = true
self.fetchingCompactTextNode.isHidden = false
} else {
self.fetchingTextNode.isHidden = false
self.fetchingCompactTextNode.isHidden = true
}
} else {
self.descriptionNode.isHidden = false
self.fetchingTextNode.isHidden = true
self.fetchingCompactTextNode.isHidden = true
}
}
self.fetchingTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingInfo.size)
self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize)
self.countNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: countSize)
}
public typealias Apply = (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode

View File

@ -806,8 +806,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
}))
}
let isViewOnceMessage = "".isEmpty
var displayTranscribe = false
if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free && !item.presentationData.isPreview {
if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free && !isViewOnceMessage && !item.presentationData.isPreview {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
if item.associatedData.isPremium {
displayTranscribe = true

View File

@ -434,7 +434,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
private var internallyVisible = true
private func updateVisibility() {
let visibility = self.visibility && self.internallyVisible
let isPreview = self.themeAndStrings?.3 ?? false
let visibility = self.visibility && self.internallyVisible && !isPreview
if let videoNode = self.videoNode {
if visibility {
@ -1254,7 +1255,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
uploading = true
}
if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !isStory && !uploading && !presentationData.isPreview {
if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !isStory && !uploading {
updateVideoFile = file
if hasCurrentVideoNode {
if let currentFile = currentMedia as? TelegramMediaFile {
@ -1453,6 +1454,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
if let statusApply = statusApply {
let dateAndStatusFrame = CGRect(origin: CGPoint(x: cleanImageFrame.width - layoutConstants.image.statusInsets.right - statusSize.width, y: cleanImageFrame.height - layoutConstants.image.statusInsets.bottom - statusSize.height), size: statusSize)
if strongSelf.dateAndStatusNode.supernode == nil {
strongSelf.dateAndStatusNode.view.tag = 0xFACE
strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode)
statusApply(.None)
strongSelf.dateAndStatusNode.frame = dateAndStatusFrame
@ -1508,6 +1510,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
strongSelf.videoContent = videoContent
strongSelf.videoNode = videoNode
if presentationData.isPreview {
videoNode.isHidden = true
strongSelf.pinchContainerNode.contentNode.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode)
}
updatedVideoNodeReadySignal = videoNode.ready
updatedPlayerStatusSignal = videoNode.status
|> mapToSignal { status -> Signal<MediaPlayerStatus?, NoError> in
@ -1558,7 +1565,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate)
videoNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
if strongSelf.visibility && strongSelf.internallyVisible {
if strongSelf.visibility && strongSelf.internallyVisible && !presentationData.isPreview {
if !videoNode.canAttachContent {
videoNode.canAttachContent = true
if videoNode.hasAttachedContext {
@ -2147,6 +2154,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
if var badgeContent = badgeContent {
if self.badgeNode == nil {
let badgeNode = ChatMessageInteractiveMediaBadge()
badgeNode.view.tag = 0xFACE
if isPreview {
badgeNode.durationNode.displaysAsynchronously = false
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "viewonceonplay_20 (2).pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,344 @@
%PDF-1.7
1 0 obj
<< /Length 2 0 R >>
stream
0.479980 0 0.028809 -0.010254 0.371582 0.754395 d1
endstream
endobj
2 0 obj
51
endobj
3 0 obj
[ 0.479980 ]
endobj
4 0 obj
<< /Length 5 0 R >>
stream
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo
<< /Registry (FigmaPDF)
/Ordering (FigmaPDF)
/Supplement 0
>> def
/CMapName /A-B-C def
/CMapType 2 def
1 begincodespacerange
<00> <FF>
endcodespacerange
1 beginbfchar
<00> <0031>
endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end
endstream
endobj
5 0 obj
332
endobj
6 0 obj
<< /Subtype /Type3
/CharProcs << /C0 1 0 R >>
/Encoding << /Type /Encoding
/Differences [ 0 /C0 ]
>>
/Widths 3 0 R
/FontBBox [ 0.000000 0.000000 0.000000 0.000000 ]
/FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ]
/Type /Font
/ToUnicode 4 0 R
/FirstChar 0
/LastChar 0
/Resources << >>
>>
endobj
7 0 obj
<< /Font << /F1 6 0 R >> >>
endobj
8 0 obj
<< /Length 9 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.500000 0.840088 cm
0.000000 0.000000 0.000000 scn
7.500000 0.829912 m
7.958396 0.829912 8.330000 1.201516 8.330000 1.659912 c
8.330000 2.118309 7.958396 2.489912 7.500000 2.489912 c
7.500000 0.829912 l
h
7.500000 15.829912 m
7.958396 15.829912 8.330000 16.201515 8.330000 16.659912 c
8.330000 17.118309 7.958396 17.489912 7.500000 17.489912 c
7.500000 15.829912 l
h
7.500000 2.489912 m
3.816261 2.489912 0.830000 5.476173 0.830000 9.159912 c
-0.830000 9.159912 l
-0.830000 4.559381 2.899468 0.829912 7.500000 0.829912 c
7.500000 2.489912 l
h
0.830000 9.159912 m
0.830000 12.843652 3.816261 15.829912 7.500000 15.829912 c
7.500000 17.489912 l
2.899468 17.489912 -0.830000 13.760445 -0.830000 9.159912 c
0.830000 9.159912 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 2.500000 0.840088 cm
0.000000 0.000000 0.000000 scn
15.795376 9.923953 m
15.753811 10.380461 15.350042 10.716839 14.893535 10.675274 c
14.437027 10.633709 14.100649 10.229940 14.142214 9.773433 c
15.795376 9.923953 l
h
12.629841 13.423267 m
12.923046 13.070907 13.446381 13.022952 13.798741 13.316157 c
14.151102 13.609363 14.199057 14.132696 13.905851 14.485057 c
12.629841 13.423267 l
h
12.825145 15.565763 m
12.472785 15.858969 11.949450 15.811014 11.656245 15.458653 c
11.363040 15.106293 11.410995 14.582958 11.763355 14.289753 c
12.825145 15.565763 l
h
8.113522 15.802126 m
8.570029 15.760561 8.973797 16.096939 9.015362 16.553446 c
9.056927 17.009954 8.720550 17.413723 8.264041 17.455288 c
8.113522 15.802126 l
h
6.735959 17.455288 m
6.279451 17.413723 5.943073 17.009954 5.984638 16.553446 c
6.026203 16.096939 6.429971 15.760561 6.886479 15.802126 c
6.735959 17.455288 l
h
3.236645 14.289753 m
3.589005 14.582958 3.636960 15.106293 3.343755 15.458653 c
3.050550 15.811014 2.527215 15.858969 2.174855 15.565763 c
3.236645 14.289753 l
h
1.094149 14.485057 m
0.800944 14.132696 0.848898 13.609363 1.201259 13.316157 c
1.553619 13.022952 2.076954 13.070907 2.370159 13.423267 c
1.094149 14.485057 l
h
0.857786 9.773434 m
0.899351 10.229941 0.562974 10.633709 0.106466 10.675274 c
-0.350042 10.716839 -0.753810 10.380461 -0.795375 9.923954 c
0.857786 9.773434 l
h
-0.795375 8.395871 m
-0.753810 7.939363 -0.350043 7.602985 0.106465 7.644550 c
0.562973 7.686115 0.899351 8.089883 0.857786 8.546391 c
-0.795375 8.395871 l
h
2.370159 4.896557 m
2.076954 5.248918 1.553619 5.296872 1.201259 5.003667 c
0.848898 4.710462 0.800944 4.187127 1.094149 3.834767 c
2.370159 4.896557 l
h
2.174855 2.754061 m
2.527215 2.460855 3.050550 2.508810 3.343755 2.861171 c
3.636960 3.213531 3.589005 3.736866 3.236645 4.030071 c
2.174855 2.754061 l
h
6.886479 2.517698 m
6.429971 2.559263 6.026203 2.222886 5.984638 1.766377 c
5.943073 1.309870 6.279451 0.906102 6.735959 0.864537 c
6.886479 2.517698 l
h
8.264041 0.864536 m
8.720549 0.906101 9.056927 1.309870 9.015362 1.766377 c
8.973797 2.222885 8.570029 2.559263 8.113521 2.517698 c
8.264041 0.864536 l
h
11.763355 4.030071 m
11.410995 3.736866 11.363040 3.213531 11.656245 2.861171 c
11.949450 2.508810 12.472785 2.460855 12.825145 2.754061 c
11.763355 4.030071 l
h
13.905851 3.834767 m
14.199057 4.187127 14.151102 4.710462 13.798741 5.003667 c
13.446381 5.296872 12.923046 5.248918 12.629841 4.896557 c
13.905851 3.834767 l
h
14.142214 8.546391 m
14.100649 8.089883 14.437026 7.686115 14.893535 7.644550 c
15.350042 7.602985 15.753810 7.939363 15.795375 8.395871 c
14.142214 8.546391 l
h
15.830000 9.159912 m
15.830000 9.417282 15.818303 9.672139 15.795376 9.923953 c
14.142214 9.773433 l
14.160591 9.571594 14.170000 9.366961 14.170000 9.159912 c
15.830000 9.159912 l
h
13.905851 14.485057 m
13.579361 14.877418 13.217505 15.239273 12.825145 15.565763 c
11.763355 14.289753 l
12.077929 14.027991 12.368079 13.737841 12.629841 13.423267 c
13.905851 14.485057 l
h
8.264041 17.455288 m
8.012227 17.478214 7.757370 17.489912 7.500000 17.489912 c
7.500000 15.829912 l
7.707049 15.829912 7.911682 15.820503 8.113522 15.802126 c
8.264041 17.455288 l
h
7.500000 17.489912 m
7.242630 17.489912 6.987773 17.478214 6.735959 17.455288 c
6.886479 15.802126 l
7.088318 15.820503 7.292951 15.829912 7.500000 15.829912 c
7.500000 17.489912 l
h
2.174855 15.565763 m
1.782495 15.239273 1.420639 14.877418 1.094149 14.485057 c
2.370159 13.423267 l
2.631921 13.737841 2.922071 14.027991 3.236645 14.289753 c
2.174855 15.565763 l
h
-0.795375 9.923954 m
-0.818303 9.672140 -0.830000 9.417282 -0.830000 9.159912 c
0.830000 9.159912 l
0.830000 9.366961 0.839409 9.571594 0.857786 9.773434 c
-0.795375 9.923954 l
h
-0.830000 9.159912 m
-0.830000 8.902542 -0.818303 8.647685 -0.795375 8.395871 c
0.857786 8.546391 l
0.839409 8.748230 0.830000 8.952863 0.830000 9.159912 c
-0.830000 9.159912 l
h
1.094149 3.834767 m
1.420639 3.442407 1.782495 3.080551 2.174855 2.754061 c
3.236645 4.030071 l
2.922071 4.291833 2.631921 4.581984 2.370159 4.896557 c
1.094149 3.834767 l
h
6.735959 0.864537 m
6.987772 0.841609 7.242630 0.829912 7.500000 0.829912 c
7.500000 2.489912 l
7.292951 2.489912 7.088318 2.499321 6.886479 2.517698 c
6.735959 0.864537 l
h
7.500000 0.829912 m
7.757370 0.829912 8.012227 0.841609 8.264041 0.864536 c
8.113521 2.517698 l
7.911682 2.499321 7.707049 2.489912 7.500000 2.489912 c
7.500000 0.829912 l
h
12.825145 2.754061 m
13.217505 3.080551 13.579361 3.442407 13.905851 3.834767 c
12.629841 4.896557 l
12.368079 4.581984 12.077929 4.291833 11.763355 4.030071 c
12.825145 2.754061 l
h
15.795375 8.395871 m
15.818303 8.647685 15.830000 8.902542 15.830000 9.159912 c
14.170000 9.159912 l
14.170000 8.952863 14.160591 8.748230 14.142214 8.546391 c
15.795375 8.395871 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 6.750000 8.042969 cm
0.000000 0.000000 0.000000 scn
0.117188 -2.292969 m
h
3.703125 -2.416016 m
3.181641 -2.416016 2.824219 -2.064453 2.824219 -1.531250 c
2.824219 4.521484 l
2.789062 4.521484 l
1.552734 3.660156 l
1.388672 3.542969 1.265625 3.501953 1.083984 3.501953 c
0.726562 3.501953 0.462891 3.759766 0.462891 4.134766 c
0.462891 4.404297 0.568359 4.603516 0.843750 4.796875 c
2.519531 5.957031 l
2.929688 6.238281 3.210938 6.291016 3.574219 6.291016 c
4.201172 6.291016 4.576172 5.910156 4.576172 5.300781 c
4.576172 -1.531250 l
4.576172 -2.064453 4.224609 -2.416016 3.703125 -2.416016 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 6.750000 8.042969 cm
BT
12.000000 0.000000 0.000000 12.000000 0.117188 -2.292969 Tm
/F1 1.000000 Tf
[ (\000) ] TJ
ET
Q
endstream
endobj
9 0 obj
6443
endobj
10 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 20.000000 20.000000 ]
/Resources 7 0 R
/Contents 8 0 R
/Parent 11 0 R
>>
endobj
11 0 obj
<< /Kids [ 10 0 R ]
/Count 1
/Type /Pages
>>
endobj
12 0 obj
<< /Pages 11 0 R
/Type /Catalog
>>
endobj
xref
0 13
0000000000 65535 f
0000000010 00000 n
0000000117 00000 n
0000000138 00000 n
0000000169 00000 n
0000000557 00000 n
0000000579 00000 n
0000000991 00000 n
0000001037 00000 n
0000007536 00000 n
0000007559 00000 n
0000007734 00000 n
0000007810 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 12 0 R
/Size 13
>>
startxref
7871
%%EOF

View File

@ -733,6 +733,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if !displayVoiceMessageDiscardAlert() {
return false
}
// if (file.isVoice || file.isInstantVideo) && "".isEmpty {
// strongSelf.openViewOnceMediaMessage(message)
// return false
// }
}
}
if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia {
@ -18825,6 +18830,34 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
func openViewOnceMediaMessage(_ message: Message) {
let source: ContextContentSource = .extracted(ChatMessageContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, selectAll: false, centerVertically: true))
let configuration = ContextController.Configuration(
sources: [
ContextController.Source(
id: 0,
title: "",
source: source,
items: .single(ContextController.Items(content: .list([]))),
closeActionTitle: "Delete and Close"
)
], initialId: 0
)
let contextController = ContextController(presentationData: self.presentationData, configuration: configuration)
contextController.getOverlayViews = { [weak self] in
guard let self else {
return []
}
return [self.chatDisplayNode.navigateButtons.view]
}
self.currentContextController = contextController
self.presentInGlobalOverlay(contextController)
let _ = self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: nil, dismissInput: { }, present: { _, _ in }, transitionNode: { _, _, _ in return nil }, addToTransitionSurface: { _ in }, openUrl: { _ in }, openPeer: { _, _ in }, callPeer: { _, _ in }, enqueueMessage: { _ in }, sendSticker: nil, sendEmoji: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in }, playlistLocation: nil))
}
func openStorySharing(messages: [Message]) {
let context = self.context
let subject: Signal<MediaEditorScreen.Subject?, NoError> = .single(.message(messages.map { $0.id }))

View File

@ -25,6 +25,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
let keepInPlace: Bool = false
let ignoreContentTouches: Bool = false
let blurBackground: Bool = true
let centerVertically: Bool
private weak var chatNode: ChatControllerNode?
private let engine: TelegramEngine
@ -47,11 +48,12 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|> distinctUntilChanged
}
init(chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool) {
init(chatNode: ChatControllerNode, engine: TelegramEngine, message: Message, selectAll: Bool, centerVertically: Bool = false) {
self.chatNode = chatNode
self.engine = engine
self.message = message
self.selectAll = selectAll
self.centerVertically = centerVertically
}
func takeView() -> ContextControllerTakeViewInfo? {

View File

@ -168,8 +168,6 @@ final class ManagedAudioRecorderContext {
private var micLevelPeakCount: Int = 0
private var audioLevelPeakUpdate: Double = 0.0
fileprivate var isPaused = false
private var recordingStateUpdateTimestamp: Double?
private var hasAudioSession = false