mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-05 22:11:22 +00:00
[WIP] View-once audio and video messages
This commit is contained in:
parent
6dcfc09165
commit
061e2c5c21
@ -29,6 +29,7 @@ swift_library(
|
|||||||
"//submodules/Components/ComponentDisplayAdapters",
|
"//submodules/Components/ComponentDisplayAdapters",
|
||||||
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
"//submodules/TelegramUI/Components/TabSelectorComponent",
|
||||||
"//submodules/TelegramUI/Components/LottieComponent",
|
"//submodules/TelegramUI/Components/LottieComponent",
|
||||||
|
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -199,7 +199,7 @@ private final class InnerActionsContainerNode: ASDisplayNode {
|
|||||||
if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth {
|
if let minimalWidth = minimalWidth, minimalWidth > minActionsWidth {
|
||||||
minActionsWidth = minimalWidth
|
minActionsWidth = minimalWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
switch widthClass {
|
switch widthClass {
|
||||||
case .compact:
|
case .compact:
|
||||||
minActionsWidth = max(minActionsWidth, floor(constrainedWidth / 3.0))
|
minActionsWidth = max(minActionsWidth, floor(constrainedWidth / 3.0))
|
||||||
|
|||||||
@ -2214,12 +2214,14 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
public let title: String
|
public let title: String
|
||||||
public let source: ContextContentSource
|
public let source: ContextContentSource
|
||||||
public let items: Signal<ContextController.Items, NoError>
|
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.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
self.source = source
|
self.source = source
|
||||||
self.items = items
|
self.items = items
|
||||||
|
self.closeActionTitle = closeActionTitle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1636,7 +1636,12 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
|
|||||||
topItemWidth = lastItemLayout.size.width * (1.0 - transitionFraction) + previousItemLayout.size.width * transitionFraction
|
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
|
let previousNavigationContainerFrame = self.navigationContainer.frame
|
||||||
transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame, beginWithCurrentState: true)
|
transition.updateFrame(node: self.navigationContainer, frame: navigationContainerFrame, beginWithCurrentState: true)
|
||||||
self.navigationContainer.update(presentationData: presentationData, presentation: presentation, size: navigationContainerFrame.size, transition: transition)
|
self.navigationContainer.update(presentationData: presentationData, presentation: presentation, size: navigationContainerFrame.size, transition: transition)
|
||||||
|
|||||||
@ -1026,16 +1026,26 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
|
|
||||||
if let contentNode = itemContentNode {
|
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)
|
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 {
|
if case let .extracted(extracted) = self.source {
|
||||||
contentFrame.origin.x = layout.size.width - contentFrame.maxX
|
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)
|
contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true)
|
||||||
}
|
}
|
||||||
if let contentNode = controllerContentNode {
|
if let contentNode = controllerContentNode {
|
||||||
//TODO:
|
//TODO:
|
||||||
var contentFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentRect.size)
|
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 {
|
if case let .extracted(extracted) = self.source, extracted.centerVertically {
|
||||||
contentFrame.origin.x = layout.size.width - contentFrame.maxX
|
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)
|
contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true)
|
||||||
|
|
||||||
@ -1086,6 +1096,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
|
|
||||||
switch stateTransition {
|
switch stateTransition {
|
||||||
case .animateIn:
|
case .animateIn:
|
||||||
|
let actionsSize = self.actionsContainerNode.bounds.size
|
||||||
|
|
||||||
if let contentNode = itemContentNode {
|
if let contentNode = itemContentNode {
|
||||||
contentNode.takeContainingNode()
|
contentNode.takeContainingNode()
|
||||||
}
|
}
|
||||||
@ -1095,7 +1107,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
|
|
||||||
self.scroller.contentOffset = CGPoint(x: 0.0, y: defaultScrollY)
|
self.scroller.contentOffset = CGPoint(x: 0.0, y: defaultScrollY)
|
||||||
|
|
||||||
let animationInContentYDistance: CGFloat
|
var animationInContentYDistance: CGFloat
|
||||||
let currentContentScreenFrame: CGRect
|
let currentContentScreenFrame: CGRect
|
||||||
if let contentNode = itemContentNode {
|
if let contentNode = itemContentNode {
|
||||||
if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace {
|
if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace {
|
||||||
@ -1109,20 +1121,27 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
|
|
||||||
var animationInContentXDistance: CGFloat = 0.0
|
var animationInContentXDistance: CGFloat = 0.0
|
||||||
let contentX = contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX
|
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
|
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 contentHeight = contentNode.containingItem.view.bounds.size.height
|
||||||
let fixedContentX = layout.size.width - (contentX + contentWidth)
|
if case let .extracted(extracted) = self.source, extracted.centerVertically {
|
||||||
animationInContentXDistance = fixedContentX - contentX
|
if actionsSize.height.isZero {
|
||||||
|
let fixedContentY = floorToScreenPixels((layout.size.height - contentHeight) / 2.0)
|
||||||
contentNode.layer.animateSpring(
|
animationInContentYDistance = fixedContentY - contentY
|
||||||
from: -animationInContentXDistance as NSNumber, to: 0.0 as NSNumber,
|
} else if contentX + contentWidth > layout.size.width / 2.0, actionsSize.height > 0.0 {
|
||||||
keyPath: "position.x",
|
let fixedContentX = layout.size.width - (contentX + contentWidth)
|
||||||
duration: duration,
|
animationInContentXDistance = fixedContentX - contentX
|
||||||
delay: 0.0,
|
|
||||||
initialVelocity: 0.0,
|
contentNode.layer.animateSpring(
|
||||||
damping: springDamping,
|
from: -animationInContentXDistance as NSNumber, to: 0.0 as NSNumber,
|
||||||
additive: true
|
keyPath: "position.x",
|
||||||
)
|
duration: duration,
|
||||||
|
delay: 0.0,
|
||||||
|
initialVelocity: 0.0,
|
||||||
|
damping: springDamping,
|
||||||
|
additive: true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contentNode.layer.animateSpring(
|
contentNode.layer.animateSpring(
|
||||||
@ -1178,9 +1197,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
damping: springDamping,
|
damping: springDamping,
|
||||||
additive: false
|
additive: false
|
||||||
)
|
)
|
||||||
|
|
||||||
let actionsSize = self.actionsContainerNode.bounds.size
|
|
||||||
|
|
||||||
var actionsPositionDeltaXDistance: CGFloat = 0.0
|
var actionsPositionDeltaXDistance: CGFloat = 0.0
|
||||||
if case .center = actionsHorizontalAlignment {
|
if case .center = actionsHorizontalAlignment {
|
||||||
actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX
|
actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX
|
||||||
@ -1262,6 +1279,8 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .animateOut(result, completion):
|
case let .animateOut(result, completion):
|
||||||
|
let actionsSize = self.actionsContainerNode.bounds.size
|
||||||
|
|
||||||
let duration: Double
|
let duration: Double
|
||||||
let timingFunction: String
|
let timingFunction: String
|
||||||
switch result {
|
switch result {
|
||||||
@ -1367,19 +1386,24 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||||||
var animationInContentXDistance: CGFloat = 0.0
|
var animationInContentXDistance: CGFloat = 0.0
|
||||||
let contentX = contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX
|
let contentX = contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX
|
||||||
let contentWidth = contentNode.containingItem.view.bounds.size.width
|
let contentWidth = contentNode.containingItem.view.bounds.size.width
|
||||||
if case let .extracted(extracted) = self.source, extracted.centerVertically, contentX + contentWidth > layout.size.width / 2.0 {
|
if case let .extracted(extracted) = self.source, extracted.centerVertically {
|
||||||
let fixedContentX = layout.size.width - (contentX + contentWidth)
|
if actionsSize.height.isZero {
|
||||||
animationInContentXDistance = contentX - fixedContentX
|
// let fixedContentY = floorToScreenPixels((layout.size.height - contentHeight) / 2.0)
|
||||||
|
animationInContentYDistance = 0.0 //contentY - fixedContentY
|
||||||
contentNode.offsetContainerNode.layer.animate(
|
} else if contentX + contentWidth > layout.size.width / 2.0{
|
||||||
from: -animationInContentXDistance as NSNumber,
|
let fixedContentX = layout.size.width - (contentX + contentWidth)
|
||||||
to: 0.0 as NSNumber,
|
animationInContentXDistance = contentX - fixedContentX
|
||||||
keyPath: "position.x",
|
|
||||||
timingFunction: timingFunction,
|
contentNode.offsetContainerNode.layer.animate(
|
||||||
duration: duration,
|
from: -animationInContentXDistance as NSNumber,
|
||||||
delay: 0.0,
|
to: 0.0 as NSNumber,
|
||||||
additive: true
|
keyPath: "position.x",
|
||||||
)
|
timingFunction: timingFunction,
|
||||||
|
duration: duration,
|
||||||
|
delay: 0.0,
|
||||||
|
additive: true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: animationInContentXDistance, dy: -animationInContentYDistance)
|
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
|
var actionsPositionDeltaXDistance: CGFloat = 0.0
|
||||||
if case .center = actionsHorizontalAlignment {
|
if case .center = actionsHorizontalAlignment {
|
||||||
actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX
|
actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import TelegramCore
|
|||||||
import ReactionSelectionNode
|
import ReactionSelectionNode
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import TabSelectorComponent
|
import TabSelectorComponent
|
||||||
|
import PlainButtonComponent
|
||||||
import ComponentDisplayAdapters
|
import ComponentDisplayAdapters
|
||||||
|
|
||||||
final class ContextSourceContainer: ASDisplayNode {
|
final class ContextSourceContainer: ASDisplayNode {
|
||||||
@ -16,6 +17,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
let id: AnyHashable
|
let id: AnyHashable
|
||||||
let title: String
|
let title: String
|
||||||
let source: ContextContentSource
|
let source: ContextContentSource
|
||||||
|
let closeActionTitle: String?
|
||||||
|
|
||||||
private var _presentationNode: ContextControllerPresentationNode?
|
private var _presentationNode: ContextControllerPresentationNode?
|
||||||
var presentationNode: ContextControllerPresentationNode {
|
var presentationNode: ContextControllerPresentationNode {
|
||||||
@ -40,12 +42,14 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
id: AnyHashable,
|
id: AnyHashable,
|
||||||
title: String,
|
title: String,
|
||||||
source: ContextContentSource,
|
source: ContextContentSource,
|
||||||
items: Signal<ContextController.Items, NoError>
|
items: Signal<ContextController.Items, NoError>,
|
||||||
|
closeActionTitle: String? = nil
|
||||||
) {
|
) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
self.source = source
|
self.source = source
|
||||||
|
self.closeActionTitle = closeActionTitle
|
||||||
|
|
||||||
self.ready.set(combineLatest(queue: .mainQueue(), self.contentReady.get(), self.actionsReady.get())
|
self.ready.set(combineLatest(queue: .mainQueue(), self.contentReady.get(), self.actionsReady.get())
|
||||||
|> map { a, b -> Bool in
|
|> map { a, b -> Bool in
|
||||||
@ -162,8 +166,11 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
guard let self, let controller = self.controller else {
|
guard let self, let controller = self.controller else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
controller.controllerNode.dismissedForCancel?()
|
if let _ = self.closeActionTitle {
|
||||||
controller.controllerNode.beginDismiss(result)
|
} else {
|
||||||
|
controller.controllerNode.dismissedForCancel?()
|
||||||
|
controller.controllerNode.beginDismiss(result)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
requestAnimateOut: { [weak self] result, completion in
|
requestAnimateOut: { [weak self] result, completion in
|
||||||
guard let self, let controller = self.controller else {
|
guard let self, let controller = self.controller else {
|
||||||
@ -341,6 +348,7 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
var activeIndex: Int = 0
|
var activeIndex: Int = 0
|
||||||
|
|
||||||
private var tabSelector: ComponentView<Empty>?
|
private var tabSelector: ComponentView<Empty>?
|
||||||
|
private var closeButton: ComponentView<Empty>?
|
||||||
|
|
||||||
private var presentationData: PresentationData?
|
private var presentationData: PresentationData?
|
||||||
private var validLayout: ContainerViewLayout?
|
private var validLayout: ContainerViewLayout?
|
||||||
@ -376,7 +384,8 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
id: source.id,
|
id: source.id,
|
||||||
title: source.title,
|
title: source.title,
|
||||||
source: source.source,
|
source: source.source,
|
||||||
items: source.items
|
items: source.items,
|
||||||
|
closeActionTitle: source.closeActionTitle
|
||||||
)
|
)
|
||||||
self.sources.append(mappedSource)
|
self.sources.append(mappedSource)
|
||||||
self.addSubnode(mappedSource.presentationNode)
|
self.addSubnode(mappedSource.presentationNode)
|
||||||
@ -457,6 +466,9 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
if let tabSelectorView = self.tabSelector?.view {
|
if let tabSelectorView = self.tabSelector?.view {
|
||||||
tabSelectorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
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) {
|
func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) {
|
||||||
@ -465,6 +477,9 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
if let tabSelectorView = self.tabSelector?.view {
|
if let tabSelectorView = self.tabSelector?.view {
|
||||||
tabSelectorView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
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 {
|
if let activeSource = self.activeSource {
|
||||||
activeSource.animateOut(result: result, completion: completion)
|
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))
|
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 {
|
} else if let tabSelector = self.tabSelector {
|
||||||
self.tabSelector = nil
|
self.tabSelector = nil
|
||||||
tabSelector.view?.removeFromSuperview()
|
tabSelector.view?.removeFromSuperview()
|
||||||
@ -664,6 +716,11 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
return result
|
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 {
|
guard let activeSource = self.activeSource else {
|
||||||
return nil
|
return nil
|
||||||
@ -671,3 +728,61 @@ final class ContextSourceContainer: ASDisplayNode {
|
|||||||
return activeSource.presentationNode.view.hitTest(point, with: event)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import SwiftSignalKit
|
|||||||
import RLottieBinding
|
import RLottieBinding
|
||||||
import GZip
|
import GZip
|
||||||
import AppBundle
|
import AppBundle
|
||||||
import ManagedAnimationNode
|
|
||||||
|
|
||||||
public enum SemanticStatusNodeState: Equatable {
|
public enum SemanticStatusNodeState: Equatable {
|
||||||
public struct ProgressAppearance: Equatable {
|
public struct ProgressAppearance: Equatable {
|
||||||
@ -33,25 +32,27 @@ public enum SemanticStatusNodeState: Equatable {
|
|||||||
case pause
|
case pause
|
||||||
case check(appearance: CheckAppearance?)
|
case check(appearance: CheckAppearance?)
|
||||||
case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?)
|
case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?)
|
||||||
|
case secretTimeout(position: Double, duration: Double, generationTimestamp: Double, appearance: ProgressAppearance?)
|
||||||
case customIcon(UIImage)
|
case customIcon(UIImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
private protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol {
|
protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol {
|
||||||
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor)
|
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private protocol SemanticStatusNodeStateContext: AnyObject {
|
protocol SemanticStatusNodeStateContext: AnyObject {
|
||||||
var isAnimating: Bool { get }
|
var isAnimating: Bool { get }
|
||||||
var requestUpdate: () -> Void { get set }
|
var requestUpdate: () -> Void { get set }
|
||||||
|
|
||||||
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState
|
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum SemanticStatusNodeIcon: Equatable {
|
enum SemanticStatusNodeIcon: Equatable {
|
||||||
case none
|
case none
|
||||||
case download
|
case download
|
||||||
case play
|
case play
|
||||||
case pause
|
case pause
|
||||||
|
case secretTimeout
|
||||||
case custom(UIImage)
|
case custom(UIImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,535 +89,6 @@ private func svgPath(_ path: StaticString, scale: CGPoint = CGPoint(x: 1.0, y: 1
|
|||||||
return path
|
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 {
|
private extension SemanticStatusNodeState {
|
||||||
func context(current: SemanticStatusNodeStateContext?) -> SemanticStatusNodeStateContext {
|
func context(current: SemanticStatusNodeStateContext?) -> SemanticStatusNodeStateContext {
|
||||||
switch self {
|
switch self {
|
||||||
@ -633,6 +105,8 @@ private extension SemanticStatusNodeState {
|
|||||||
icon = .pause
|
icon = .pause
|
||||||
case let .customIcon(image):
|
case let .customIcon(image):
|
||||||
icon = .custom(image)
|
icon = .custom(image)
|
||||||
|
case .secretTimeout:
|
||||||
|
icon = .none
|
||||||
default:
|
default:
|
||||||
preconditionFailure()
|
preconditionFailure()
|
||||||
}
|
}
|
||||||
@ -654,6 +128,13 @@ private extension SemanticStatusNodeState {
|
|||||||
} else {
|
} else {
|
||||||
return SemanticStatusNodeCheckContext(value: 0.0, appearance: appearance)
|
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):
|
case let .progress(value, cancelEnabled, appearance):
|
||||||
if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled {
|
if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled {
|
||||||
current.updateValue(value: value)
|
current.updateValue(value: value)
|
||||||
@ -1048,53 +529,3 @@ public final class SemanticStatusNode: ASControlNode {
|
|||||||
parameters.appearanceState.drawForeground(context: context, size: bounds.size)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -185,6 +185,8 @@ public final class PrincipalThemeEssentialGraphics {
|
|||||||
public let radialIndicatorFileIconIncoming: UIImage
|
public let radialIndicatorFileIconIncoming: UIImage
|
||||||
public let radialIndicatorFileIconOutgoing: UIImage
|
public let radialIndicatorFileIconOutgoing: UIImage
|
||||||
|
|
||||||
|
public let radialIndicatorViewOnceIcon: UIImage
|
||||||
|
|
||||||
public let incomingBubbleGradientImage: UIImage?
|
public let incomingBubbleGradientImage: UIImage?
|
||||||
public let outgoingBubbleGradientImage: 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.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)!
|
||||||
self.radialIndicatorFileIconOutgoing = 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 {
|
} 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.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)
|
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.radialIndicatorFileIconIncoming = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocument"), color: .black)!
|
||||||
self.radialIndicatorFileIconOutgoing = 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
|
let chatDateSize: CGFloat = 20.0
|
||||||
|
|||||||
@ -42,6 +42,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
|
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||||
|
"//submodules/AnimatedCountLabelNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import ChatMessageDateAndStatusNode
|
|||||||
import ChatHistoryEntry
|
import ChatHistoryEntry
|
||||||
import ChatMessageItemCommon
|
import ChatMessageItemCommon
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
|
import AnimatedCountLabelNode
|
||||||
|
|
||||||
private struct FetchControls {
|
private struct FetchControls {
|
||||||
let fetch: (Bool) -> Void
|
let fetch: (Bool) -> Void
|
||||||
@ -120,6 +121,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
private let descriptionMeasuringNode: TextNode
|
private let descriptionMeasuringNode: TextNode
|
||||||
public let fetchingTextNode: ImmediateTextNode
|
public let fetchingTextNode: ImmediateTextNode
|
||||||
public let fetchingCompactTextNode: ImmediateTextNode
|
public let fetchingCompactTextNode: ImmediateTextNode
|
||||||
|
private let countNode: ImmediateAnimatedCountLabelNode
|
||||||
|
|
||||||
public var waveformView: ComponentHostView<Empty>?
|
public var waveformView: ComponentHostView<Empty>?
|
||||||
|
|
||||||
@ -194,6 +196,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
private var progressFrame: CGRect?
|
private var progressFrame: CGRect?
|
||||||
private var streamingCacheStatusFrame: CGRect?
|
private var streamingCacheStatusFrame: CGRect?
|
||||||
private var fileIconImage: UIImage?
|
private var fileIconImage: UIImage?
|
||||||
|
private var viewOnceIconImage: UIImage?
|
||||||
|
|
||||||
public var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed
|
public var audioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState = .collapsed
|
||||||
public var forcedAudioTranscriptionText: TranscribedText?
|
public var forcedAudioTranscriptionText: TranscribedText?
|
||||||
@ -218,6 +221,9 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
self.descriptionNode.displaysAsynchronously = false
|
self.descriptionNode.displaysAsynchronously = false
|
||||||
self.descriptionNode.isUserInteractionEnabled = false
|
self.descriptionNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.countNode = ImmediateAnimatedCountLabelNode()
|
||||||
|
self.countNode.alwaysOneDirection = true
|
||||||
|
|
||||||
self.descriptionMeasuringNode = TextNode()
|
self.descriptionMeasuringNode = TextNode()
|
||||||
|
|
||||||
self.fetchingTextNode = ImmediateTextNode()
|
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 (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 (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
|
let fileSizeString: String
|
||||||
if let _ = arguments.file.size {
|
if let _ = arguments.file.size {
|
||||||
fileSizeString = "000.0 MB"
|
fileSizeString = "000.0 MB"
|
||||||
@ -747,7 +755,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState?
|
var updatedAudioTranscriptionState: AudioTranscriptionButtonComponent.TranscriptionState?
|
||||||
|
|
||||||
var displayTranscribe = false
|
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 })
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
|
||||||
if arguments.associatedData.isPremium {
|
if arguments.associatedData.isPremium {
|
||||||
displayTranscribe = true
|
displayTranscribe = true
|
||||||
@ -965,12 +973,14 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
minLayoutWidth = max(minLayoutWidth, textLayout.size.width + horizontalInset)
|
minLayoutWidth = max(minLayoutWidth, textLayout.size.width + horizontalInset)
|
||||||
|
|
||||||
let fileIconImage: UIImage?
|
let fileIconImage: UIImage?
|
||||||
|
var viewOnceIconImage: UIImage?
|
||||||
if hasThumbnail {
|
if hasThumbnail {
|
||||||
fileIconImage = nil
|
fileIconImage = nil
|
||||||
} else {
|
} else {
|
||||||
let principalGraphics = PresentationResourcesChat.principalGraphics(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, bubbleCorners: arguments.presentationData.chatBubbleCorners)
|
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
|
fileIconImage = arguments.incoming ? principalGraphics.radialIndicatorFileIconIncoming : principalGraphics.radialIndicatorFileIconOutgoing
|
||||||
|
viewOnceIconImage = principalGraphics.radialIndicatorViewOnceIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
return (minLayoutWidth, { boundingWidth in
|
return (minLayoutWidth, { boundingWidth in
|
||||||
@ -1050,7 +1060,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
strongSelf.titleNode.frame = titleFrame
|
strongSelf.titleNode.frame = titleFrame
|
||||||
strongSelf.descriptionNode.frame = descriptionFrame
|
strongSelf.descriptionNode.frame = descriptionFrame
|
||||||
strongSelf.descriptionMeasuringNode.frame = CGRect(origin: CGPoint(), size: descriptionMeasuringLayout.size)
|
strongSelf.descriptionMeasuringNode.frame = CGRect(origin: CGPoint(), size: descriptionMeasuringLayout.size)
|
||||||
|
|
||||||
if let updatedAudioTranscriptionState = updatedAudioTranscriptionState {
|
if let updatedAudioTranscriptionState = updatedAudioTranscriptionState {
|
||||||
strongSelf.audioTranscriptionState = updatedAudioTranscriptionState
|
strongSelf.audioTranscriptionState = updatedAudioTranscriptionState
|
||||||
}
|
}
|
||||||
@ -1432,7 +1442,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
strongSelf.progressFrame = progressFrame
|
strongSelf.progressFrame = progressFrame
|
||||||
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
|
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
|
||||||
strongSelf.fileIconImage = fileIconImage
|
strongSelf.fileIconImage = fileIconImage
|
||||||
|
strongSelf.viewOnceIconImage = viewOnceIconImage
|
||||||
|
|
||||||
if let updatedFetchControls = updatedFetchControls {
|
if let updatedFetchControls = updatedFetchControls {
|
||||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||||
if arguments.automaticDownload {
|
if arguments.automaticDownload {
|
||||||
@ -1548,6 +1559,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let isViewOnceMessage = "".isEmpty || (isVoice && message.autoremoveAttribute?.timeout == viewOnceTimeout)
|
||||||
|
|
||||||
var state: SemanticStatusNodeState
|
var state: SemanticStatusNodeState
|
||||||
var streamingState: SemanticStatusNodeState = .none
|
var streamingState: SemanticStatusNodeState = .none
|
||||||
@ -1556,6 +1568,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
|
|
||||||
var downloadingStrings: (String, String, UIFont)?
|
var downloadingStrings: (String, String, UIFont)?
|
||||||
|
|
||||||
|
var playbackState: (position: Double, duration: Double, generationTimestamp: Double) = (0.0, 0.0, 0.0)
|
||||||
if !isAudio {
|
if !isAudio {
|
||||||
var fetchStatus: MediaResourceStatus?
|
var fetchStatus: MediaResourceStatus?
|
||||||
if let actualFetchStatus = self.actualFetchStatus, message.forwardInfo != nil {
|
if let actualFetchStatus = self.actualFetchStatus, message.forwardInfo != nil {
|
||||||
@ -1579,75 +1592,84 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
} else if isVoice {
|
} else if isVoice {
|
||||||
if let playerStatus = self.playerStatus {
|
if let playerStatus = self.playerStatus {
|
||||||
var playerPosition: Int32?
|
var playerPosition: Double?
|
||||||
var playerDuration: Int32 = 0
|
var playerDuration: Double = 0.0
|
||||||
if !playerStatus.generationTimestamp.isZero, case .playing = playerStatus.status {
|
if !playerStatus.generationTimestamp.isZero, case .playing = playerStatus.status {
|
||||||
playerPosition = Int32(playerStatus.timestamp + (CACurrentMediaTime() - playerStatus.generationTimestamp))
|
playerPosition = playerStatus.timestamp + (CACurrentMediaTime() - playerStatus.generationTimestamp)
|
||||||
} else {
|
} 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))
|
let durationFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 11.0 / 17.0))
|
||||||
downloadingStrings = (durationString, durationString, durationFont)
|
downloadingStrings = (durationString, durationString, durationFont)
|
||||||
|
|
||||||
|
playbackState = (playerStatus.timestamp, playerDuration, playerStatus.generationTimestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch resourceStatus.mediaStatus {
|
switch resourceStatus.mediaStatus {
|
||||||
case var .fetchStatus(fetchStatus):
|
case var .fetchStatus(fetchStatus):
|
||||||
if self.message?.forwardInfo != nil {
|
if self.message?.forwardInfo != nil {
|
||||||
fetchStatus = resourceStatus.fetchStatus
|
fetchStatus = resourceStatus.fetchStatus
|
||||||
}
|
}
|
||||||
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false
|
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = false
|
||||||
//self.waveformScrubbingNode?.enableScrubbing = false
|
|
||||||
|
|
||||||
switch fetchStatus {
|
switch fetchStatus {
|
||||||
case let .Fetching(_, progress):
|
case let .Fetching(_, progress):
|
||||||
let adjustedProgress = max(progress, 0.027)
|
let adjustedProgress = max(progress, 0.027)
|
||||||
var wasCheck = false
|
var wasCheck = false
|
||||||
if let statusNode = self.statusNode, case .check = statusNode.state {
|
if let statusNode = self.statusNode, case .check = statusNode.state {
|
||||||
wasCheck = true
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case let .playbackStatus(playbackStatus):
|
|
||||||
(self.waveformView?.componentView as? AudioWaveformComponent.View)?.enableScrubbing = true
|
if isAudio && !isVoice && !isSending {
|
||||||
//self.waveformScrubbingNode?.enableScrubbing = true
|
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 {
|
switch playbackStatus {
|
||||||
case .playing:
|
case .playing:
|
||||||
state = .pause
|
state = .pause
|
||||||
case .paused:
|
case .paused:
|
||||||
state = .play
|
state = .play
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAudio && !isVoice && !isSending && state != .pause {
|
if isViewOnceMessage, let viewOnceIconImage = self.viewOnceIconImage, state == .play {
|
||||||
switch resourceStatus.fetchStatus {
|
streamingState = .customIcon(viewOnceIconImage)
|
||||||
|
} else {
|
||||||
|
if isAudio && !isVoice && !isSending && state != .pause {
|
||||||
|
switch resourceStatus.fetchStatus {
|
||||||
case let .Fetching(_, progress):
|
case let .Fetching(_, progress):
|
||||||
let adjustedProgress = max(progress, 0.027)
|
let adjustedProgress = max(progress, 0.027)
|
||||||
streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0))
|
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
|
streamingState = .none
|
||||||
case .Remote, .Paused:
|
case .Remote, .Paused:
|
||||||
streamingState = .download
|
streamingState = .download
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
streamingState = .none
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
streamingState = .none
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSending {
|
if isSending {
|
||||||
@ -1721,7 +1744,13 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let effectsEnabled = self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true
|
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 blobFrame = progressFrame.insetBy(dx: -12.0, dy: -12.0)
|
||||||
let playbackAudioLevelNode = VoiceBlobNode(
|
let playbackAudioLevelNode = VoiceBlobNode(
|
||||||
maxLevel: 0.3,
|
maxLevel: 0.3,
|
||||||
@ -1801,9 +1830,28 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
statusNode.setCutout(cutoutFrame, animated: true)
|
statusNode.setCutout(cutoutFrame, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var displayingCountdown = false
|
||||||
if let (expandedString, compactString, font) = downloadingStrings {
|
if let (expandedString, compactString, font) = downloadingStrings {
|
||||||
self.fetchingTextNode.attributedText = NSAttributedString(string: expandedString, font: font, textColor: messageTheme.fileDurationColor)
|
self.fetchingTextNode.attributedText = NSAttributedString(string: expandedString, font: font, textColor: messageTheme.fileDurationColor)
|
||||||
self.fetchingCompactTextNode.attributedText = NSAttributedString(string: compactString, 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 {
|
} else {
|
||||||
self.fetchingTextNode.attributedText = nil
|
self.fetchingTextNode.attributedText = nil
|
||||||
self.fetchingCompactTextNode.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 maxFetchingStatusWidth = max(self.titleNode.frame.width, self.descriptionMeasuringNode.frame.width) + 2.0
|
||||||
let fetchingInfo = self.fetchingTextNode.updateLayoutInfo(CGSize(width: maxFetchingStatusWidth, height: CGFloat.greatestFiniteMagnitude))
|
let fetchingInfo = self.fetchingTextNode.updateLayoutInfo(CGSize(width: maxFetchingStatusWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||||
let fetchingCompactSize = self.fetchingCompactTextNode.updateLayout(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 {
|
if displayingCountdown {
|
||||||
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.fetchingTextNode.isHidden = true
|
||||||
self.fetchingCompactTextNode.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.fetchingTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingInfo.size)
|
||||||
self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize)
|
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
|
public typealias Apply = (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode
|
||||||
|
|||||||
@ -806,8 +806,10 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isViewOnceMessage = "".isEmpty
|
||||||
|
|
||||||
var displayTranscribe = false
|
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 })
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
|
||||||
if item.associatedData.isPremium {
|
if item.associatedData.isPremium {
|
||||||
displayTranscribe = true
|
displayTranscribe = true
|
||||||
|
|||||||
@ -434,7 +434,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
|
|
||||||
private var internallyVisible = true
|
private var internallyVisible = true
|
||||||
private func updateVisibility() {
|
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 let videoNode = self.videoNode {
|
||||||
if visibility {
|
if visibility {
|
||||||
@ -1254,7 +1255,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
uploading = true
|
uploading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !isStory && !uploading && !presentationData.isPreview {
|
if file.isVideo && !file.isVideoSticker && !isSecretMedia && automaticPlayback && !isStory && !uploading {
|
||||||
updateVideoFile = file
|
updateVideoFile = file
|
||||||
if hasCurrentVideoNode {
|
if hasCurrentVideoNode {
|
||||||
if let currentFile = currentMedia as? TelegramMediaFile {
|
if let currentFile = currentMedia as? TelegramMediaFile {
|
||||||
@ -1453,6 +1454,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
if let statusApply = statusApply {
|
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)
|
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 {
|
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||||
|
strongSelf.dateAndStatusNode.view.tag = 0xFACE
|
||||||
strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode)
|
strongSelf.pinchContainerNode.contentNode.addSubnode(strongSelf.dateAndStatusNode)
|
||||||
statusApply(.None)
|
statusApply(.None)
|
||||||
strongSelf.dateAndStatusNode.frame = dateAndStatusFrame
|
strongSelf.dateAndStatusNode.frame = dateAndStatusFrame
|
||||||
@ -1508,6 +1510,11 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
strongSelf.videoContent = videoContent
|
strongSelf.videoContent = videoContent
|
||||||
strongSelf.videoNode = videoNode
|
strongSelf.videoNode = videoNode
|
||||||
|
|
||||||
|
if presentationData.isPreview {
|
||||||
|
videoNode.isHidden = true
|
||||||
|
strongSelf.pinchContainerNode.contentNode.insertSubnode(videoNode, aboveSubnode: strongSelf.imageNode)
|
||||||
|
}
|
||||||
|
|
||||||
updatedVideoNodeReadySignal = videoNode.ready
|
updatedVideoNodeReadySignal = videoNode.ready
|
||||||
updatedPlayerStatusSignal = videoNode.status
|
updatedPlayerStatusSignal = videoNode.status
|
||||||
|> mapToSignal { status -> Signal<MediaPlayerStatus?, NoError> in
|
|> mapToSignal { status -> Signal<MediaPlayerStatus?, NoError> in
|
||||||
@ -1558,7 +1565,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate)
|
videoNode.updateLayout(size: arguments.drawingSize, transition: .immediate)
|
||||||
videoNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
|
videoNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
|
||||||
|
|
||||||
if strongSelf.visibility && strongSelf.internallyVisible {
|
if strongSelf.visibility && strongSelf.internallyVisible && !presentationData.isPreview {
|
||||||
if !videoNode.canAttachContent {
|
if !videoNode.canAttachContent {
|
||||||
videoNode.canAttachContent = true
|
videoNode.canAttachContent = true
|
||||||
if videoNode.hasAttachedContext {
|
if videoNode.hasAttachedContext {
|
||||||
@ -2147,6 +2154,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
|||||||
if var badgeContent = badgeContent {
|
if var badgeContent = badgeContent {
|
||||||
if self.badgeNode == nil {
|
if self.badgeNode == nil {
|
||||||
let badgeNode = ChatMessageInteractiveMediaBadge()
|
let badgeNode = ChatMessageInteractiveMediaBadge()
|
||||||
|
badgeNode.view.tag = 0xFACE
|
||||||
if isPreview {
|
if isPreview {
|
||||||
badgeNode.durationNode.displaysAsynchronously = false
|
badgeNode.durationNode.displaysAsynchronously = false
|
||||||
}
|
}
|
||||||
|
|||||||
12
submodules/TelegramUI/Images.xcassets/Chat/Message/ViewOnce.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/ViewOnce.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "viewonceonplay_20 (2).pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
344
submodules/TelegramUI/Images.xcassets/Chat/Message/ViewOnce.imageset/viewonceonplay_20 (2).pdf
vendored
Normal file
344
submodules/TelegramUI/Images.xcassets/Chat/Message/ViewOnce.imageset/viewonceonplay_20 (2).pdf
vendored
Normal 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
|
||||||
@ -733,6 +733,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
if !displayVoiceMessageDiscardAlert() {
|
if !displayVoiceMessageDiscardAlert() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (file.isVoice || file.isInstantVideo) && "".isEmpty {
|
||||||
|
// strongSelf.openViewOnceMediaMessage(message)
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia {
|
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]) {
|
func openStorySharing(messages: [Message]) {
|
||||||
let context = self.context
|
let context = self.context
|
||||||
let subject: Signal<MediaEditorScreen.Subject?, NoError> = .single(.message(messages.map { $0.id }))
|
let subject: Signal<MediaEditorScreen.Subject?, NoError> = .single(.message(messages.map { $0.id }))
|
||||||
|
|||||||
@ -25,6 +25,7 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|
|||||||
let keepInPlace: Bool = false
|
let keepInPlace: Bool = false
|
||||||
let ignoreContentTouches: Bool = false
|
let ignoreContentTouches: Bool = false
|
||||||
let blurBackground: Bool = true
|
let blurBackground: Bool = true
|
||||||
|
let centerVertically: Bool
|
||||||
|
|
||||||
private weak var chatNode: ChatControllerNode?
|
private weak var chatNode: ChatControllerNode?
|
||||||
private let engine: TelegramEngine
|
private let engine: TelegramEngine
|
||||||
@ -47,11 +48,12 @@ final class ChatMessageContextExtractedContentSource: ContextExtractedContentSou
|
|||||||
|> distinctUntilChanged
|
|> 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.chatNode = chatNode
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
self.message = message
|
self.message = message
|
||||||
self.selectAll = selectAll
|
self.selectAll = selectAll
|
||||||
|
self.centerVertically = centerVertically
|
||||||
}
|
}
|
||||||
|
|
||||||
func takeView() -> ContextControllerTakeViewInfo? {
|
func takeView() -> ContextControllerTakeViewInfo? {
|
||||||
|
|||||||
@ -168,8 +168,6 @@ final class ManagedAudioRecorderContext {
|
|||||||
private var micLevelPeakCount: Int = 0
|
private var micLevelPeakCount: Int = 0
|
||||||
private var audioLevelPeakUpdate: Double = 0.0
|
private var audioLevelPeakUpdate: Double = 0.0
|
||||||
|
|
||||||
fileprivate var isPaused = false
|
|
||||||
|
|
||||||
private var recordingStateUpdateTimestamp: Double?
|
private var recordingStateUpdateTimestamp: Double?
|
||||||
|
|
||||||
private var hasAudioSession = false
|
private var hasAudioSession = false
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user