mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Reaction concept update
This commit is contained in:
parent
d9ac01d601
commit
d7a5983255
@ -19,6 +19,7 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/WebPBinding:WebPBinding",
|
||||
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
|
||||
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -10,6 +10,27 @@ import TelegramPresentationData
|
||||
import UIKit
|
||||
import WebPBinding
|
||||
import AnimatedAvatarSetNode
|
||||
import ReactionImageComponent
|
||||
|
||||
public final class ReactionIconView: PortalSourceView {
|
||||
fileprivate let imageView: UIImageView
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.imageView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.imageView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateFrame(view: self.imageView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
}
|
||||
|
||||
public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
|
||||
fileprivate final class ContainerButtonNode: HighlightTrackingButtonNode {
|
||||
@ -272,7 +293,6 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
|
||||
let spec: Spec
|
||||
|
||||
let backgroundColor: UInt32
|
||||
let clippingHeight: CGFloat
|
||||
let sideInsets: CGFloat
|
||||
|
||||
let imageFrame: CGRect
|
||||
@ -281,46 +301,37 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
|
||||
let counterFrame: CGRect?
|
||||
|
||||
let backgroundLayout: ContainerButtonNode.Layout
|
||||
//let backgroundImage: UIImage
|
||||
//let extractedBackgroundImage: UIImage
|
||||
|
||||
let size: CGSize
|
||||
|
||||
init(
|
||||
spec: Spec,
|
||||
backgroundColor: UInt32,
|
||||
clippingHeight: CGFloat,
|
||||
sideInsets: CGFloat,
|
||||
imageFrame: CGRect,
|
||||
counterLayout: CounterLayout?,
|
||||
counterFrame: CGRect?,
|
||||
backgroundLayout: ContainerButtonNode.Layout,
|
||||
//backgroundImage: UIImage,
|
||||
//extractedBackgroundImage: UIImage,
|
||||
size: CGSize
|
||||
) {
|
||||
self.spec = spec
|
||||
self.backgroundColor = backgroundColor
|
||||
self.clippingHeight = clippingHeight
|
||||
self.sideInsets = sideInsets
|
||||
self.imageFrame = imageFrame
|
||||
self.counterLayout = counterLayout
|
||||
self.counterFrame = counterFrame
|
||||
self.backgroundLayout = backgroundLayout
|
||||
//self.backgroundImage = backgroundImage
|
||||
//self.extractedBackgroundImage = extractedBackgroundImage
|
||||
self.size = size
|
||||
}
|
||||
|
||||
static func calculate(spec: Spec, currentLayout: Layout?) -> Layout {
|
||||
let clippingHeight: CGFloat = 22.0
|
||||
let sideInsets: CGFloat = 8.0
|
||||
let height: CGFloat = 30.0
|
||||
let spacing: CGFloat = 4.0
|
||||
|
||||
let defaultImageSize = CGSize(width: 22.0, height: 22.0)
|
||||
let defaultImageSize = CGSize(width: 26.0, height: 26.0)
|
||||
let imageSize: CGSize
|
||||
if let file = spec.component.reaction.iconFile {
|
||||
if let file = spec.component.reaction.centerAnimation {
|
||||
imageSize = file.dimensions?.cgSize.aspectFitted(defaultImageSize) ?? defaultImageSize
|
||||
} else {
|
||||
imageSize = defaultImageSize
|
||||
@ -386,14 +397,11 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
|
||||
return Layout(
|
||||
spec: spec,
|
||||
backgroundColor: backgroundColor,
|
||||
clippingHeight: clippingHeight,
|
||||
sideInsets: sideInsets,
|
||||
imageFrame: imageFrame,
|
||||
counterLayout: counterLayout,
|
||||
counterFrame: counterFrame,
|
||||
backgroundLayout: backgroundLayout,
|
||||
//backgroundImage: backgroundImage,
|
||||
//extractedBackgroundImage: extractedBackgroundImage,
|
||||
size: size
|
||||
)
|
||||
}
|
||||
@ -403,7 +411,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
|
||||
|
||||
public let containerNode: ContextExtractedContentContainingNode
|
||||
private let buttonNode: ContainerButtonNode
|
||||
public let iconView: UIImageView
|
||||
public let iconView: ReactionIconView
|
||||
private var avatarsView: AnimatedAvatarSetView?
|
||||
|
||||
private let iconImageDisposable = MetaDisposable()
|
||||
@ -412,7 +420,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
|
||||
self.containerNode = ContextExtractedContentContainingNode()
|
||||
self.buttonNode = ContainerButtonNode()
|
||||
|
||||
self.iconView = UIImageView()
|
||||
self.iconView = ReactionIconView()
|
||||
self.iconView.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
@ -480,18 +488,19 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
|
||||
self.buttonNode.update(layout: layout.backgroundLayout)
|
||||
|
||||
animation.animator.updateFrame(layer: self.iconView.layer, frame: layout.imageFrame, completion: nil)
|
||||
self.iconView.update(size: layout.imageFrame.size, transition: animation.transition)
|
||||
|
||||
if self.layout?.spec.component.reaction != layout.spec.component.reaction {
|
||||
if let file = layout.spec.component.reaction.iconFile {
|
||||
self.iconImageDisposable.set((layout.spec.component.context.account.postbox.mediaBox.resourceData(file.resource)
|
||||
if let file = layout.spec.component.reaction.centerAnimation {
|
||||
self.iconImageDisposable.set((reactionStaticImage(context: layout.spec.component.context, animation: file, pixelSize: CGSize(width: 72.0, height: 72.0))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||
if let image = WebP.convert(fromWebP: dataValue) {
|
||||
strongSelf.iconView.image = image
|
||||
if data.isComplete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||
if let image = UIImage(data: dataValue) {
|
||||
strongSelf.iconView.imageView.image = image
|
||||
}
|
||||
}
|
||||
}))
|
||||
@ -564,29 +573,21 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
|
||||
}
|
||||
}
|
||||
|
||||
public final class ReactionButtonComponent: Component {
|
||||
public struct ViewTag: Equatable {
|
||||
public var value: String
|
||||
|
||||
public init(value: String) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
public final class ReactionButtonComponent: Equatable {
|
||||
public struct Reaction: Equatable {
|
||||
public var value: String
|
||||
public var iconFile: TelegramMediaFile?
|
||||
public var centerAnimation: TelegramMediaFile?
|
||||
|
||||
public init(value: String, iconFile: TelegramMediaFile?) {
|
||||
public init(value: String, centerAnimation: TelegramMediaFile?) {
|
||||
self.value = value
|
||||
self.iconFile = iconFile
|
||||
self.centerAnimation = centerAnimation
|
||||
}
|
||||
|
||||
public static func ==(lhs: Reaction, rhs: Reaction) -> Bool {
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
if lhs.iconFile?.fileId != rhs.iconFile?.fileId {
|
||||
if lhs.centerAnimation?.fileId != rhs.centerAnimation?.fileId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -665,153 +666,6 @@ public final class ReactionButtonComponent: Component {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIButton, ComponentTaggedView {
|
||||
public let iconView: UIImageView
|
||||
private let textView: ComponentHostView<Empty>
|
||||
private let measureTextView: ComponentHostView<Empty>
|
||||
|
||||
private var currentComponent: ReactionButtonComponent?
|
||||
|
||||
private let iconImageDisposable = MetaDisposable()
|
||||
|
||||
init() {
|
||||
self.iconView = UIImageView()
|
||||
self.iconView.isUserInteractionEnabled = false
|
||||
|
||||
self.textView = ComponentHostView<Empty>()
|
||||
self.textView.isUserInteractionEnabled = false
|
||||
|
||||
self.measureTextView = ComponentHostView<Empty>()
|
||||
self.measureTextView.isUserInteractionEnabled = false
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.iconView)
|
||||
self.addSubview(self.textView)
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.iconImageDisposable.dispose()
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let currentComponent = self.currentComponent else {
|
||||
return
|
||||
}
|
||||
currentComponent.action(currentComponent.reaction.value)
|
||||
}
|
||||
|
||||
public func matches(tag: Any) -> Bool {
|
||||
guard let tag = tag as? ViewTag else {
|
||||
return false
|
||||
}
|
||||
guard let currentComponent = self.currentComponent else {
|
||||
return false
|
||||
}
|
||||
if currentComponent.reaction.value == tag.value {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func update(component: ReactionButtonComponent, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let sideInsets: CGFloat = 8.0
|
||||
let height: CGFloat = 30.0
|
||||
let spacing: CGFloat = 4.0
|
||||
|
||||
let defaultImageSize = CGSize(width: 22.0, height: 22.0)
|
||||
|
||||
let imageSize: CGSize
|
||||
if self.currentComponent?.reaction != component.reaction {
|
||||
if let file = component.reaction.iconFile {
|
||||
self.iconImageDisposable.set((component.context.account.postbox.mediaBox.resourceData(file.resource)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||
if let image = WebP.convert(fromWebP: dataValue) {
|
||||
strongSelf.iconView.image = image
|
||||
}
|
||||
}
|
||||
}))
|
||||
imageSize = file.dimensions?.cgSize.aspectFitted(defaultImageSize) ?? defaultImageSize
|
||||
} else {
|
||||
imageSize = defaultImageSize
|
||||
}
|
||||
} else {
|
||||
imageSize = self.iconView.bounds.size
|
||||
}
|
||||
|
||||
self.iconView.frame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize)
|
||||
|
||||
let text = countString(Int64(component.count))
|
||||
var measureText = ""
|
||||
for _ in 0 ..< text.count {
|
||||
measureText.append("0")
|
||||
}
|
||||
|
||||
let minTextWidth = self.measureTextView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: measureText,
|
||||
font: Font.regular(11.0),
|
||||
color: .black
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
).width + 2.0
|
||||
|
||||
let actualTextSize: CGSize
|
||||
if self.currentComponent?.count != component.count || self.currentComponent?.colors != component.colors || self.currentComponent?.isSelected != component.isSelected {
|
||||
actualTextSize = self.textView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: text,
|
||||
font: Font.medium(11.0),
|
||||
color: UIColor(argb: component.isSelected ? component.colors.selectedForeground : component.colors.deselectedForeground)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
} else {
|
||||
actualTextSize = self.textView.bounds.size
|
||||
}
|
||||
let layoutTextSize = CGSize(width: max(actualTextSize.width, minTextWidth), height: actualTextSize.height)
|
||||
|
||||
if self.currentComponent?.colors != component.colors || self.currentComponent?.isSelected != component.isSelected {
|
||||
if component.isSelected {
|
||||
self.backgroundColor = UIColor(argb: component.colors.selectedBackground)
|
||||
} else {
|
||||
self.backgroundColor = UIColor(argb: component.colors.deselectedBackground)
|
||||
}
|
||||
}
|
||||
|
||||
self.layer.cornerRadius = height / 2.0
|
||||
|
||||
self.textView.frame = CGRect(origin: CGPoint(x: sideInsets + imageSize.width + spacing, y: floorToScreenPixels((height - actualTextSize.height) / 2.0)), size: actualTextSize)
|
||||
|
||||
self.currentComponent = component
|
||||
|
||||
return CGSize(width: imageSize.width + spacing + layoutTextSize.width + sideInsets * 2.0, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class ReactionButtonsAsyncLayoutContainer {
|
||||
|
@ -17,6 +17,8 @@ swift_library(
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/WebPBinding:WebPBinding",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/GZip:GZip",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -9,6 +9,65 @@ import AccountContext
|
||||
import TelegramPresentationData
|
||||
import UIKit
|
||||
import WebPBinding
|
||||
import RLottieBinding
|
||||
import GZip
|
||||
|
||||
public func reactionStaticImage(context: AccountContext, animation: TelegramMediaFile, pixelSize: CGSize) -> Signal<EngineMediaResource.ResourceData, NoError> {
|
||||
return context.engine.resources.custom(id: "\(animation.resource.id.stringRepresentation):reaction-static-v7", fetch: EngineMediaResource.Fetch {
|
||||
return Signal { subscriber in
|
||||
let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: MediaResourceReference.standalone(resource: animation.resource)).start()
|
||||
let dataDisposable = context.account.postbox.mediaBox.resourceData(animation.resource).start(next: { data in
|
||||
if !data.complete {
|
||||
return
|
||||
}
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else {
|
||||
return
|
||||
}
|
||||
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
|
||||
return
|
||||
}
|
||||
guard let instance = LottieInstance(data: unpackedData, fitzModifier: .none, cacheKey: "") else {
|
||||
return
|
||||
}
|
||||
|
||||
let innerInsets = UIEdgeInsets(top: 2.4, left: 2.4, bottom: 2.4, right: 2.4)
|
||||
let renderContext = DrawingContext(size: CGSize(width: floor(pixelSize.width * (1.0 + innerInsets.left + innerInsets.right)), height: floor(pixelSize.height * (1.0 + innerInsets.bottom + innerInsets.top))), scale: 1.0, clear: true)
|
||||
|
||||
instance.renderFrame(with: Int32(instance.frameCount - 1), into: renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(renderContext.size.width * renderContext.scale), height: Int32(renderContext.size.height * renderContext.scale), bytesPerRow: Int32(renderContext.bytesPerRow))
|
||||
|
||||
guard let image = renderContext.generateImage() else {
|
||||
return
|
||||
}
|
||||
|
||||
let clippingContext = DrawingContext(size: pixelSize, scale: 1.0, clear: true)
|
||||
clippingContext.withContext { context in
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
image.draw(at: CGPoint(x: -innerInsets.left * pixelSize.width, y: -innerInsets.top * pixelSize.height))
|
||||
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
guard let pngData = clippingContext.generateImage()?.pngData() else {
|
||||
return
|
||||
}
|
||||
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "image.png")
|
||||
guard let _ = try? pngData.write(to: URL(fileURLWithPath: tempFile.path)) else {
|
||||
return
|
||||
}
|
||||
|
||||
subscriber.putNext(.moveTempFile(file: tempFile))
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
fetchDisposable.dispose()
|
||||
dataDisposable.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public final class ReactionImageNode: ASImageNode {
|
||||
private var disposable: Disposable?
|
||||
|
@ -1244,9 +1244,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
}
|
||||
|
||||
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, completion: @escaping () -> Void) {
|
||||
if let presentationNode = self.presentationNode {
|
||||
presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: completion)
|
||||
presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
@ -1264,7 +1264,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
self.reactionContextNodeIsAnimatingOut = true
|
||||
reactionContextNode.willAnimateOutToReaction(value: value)
|
||||
reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: { [weak self] in
|
||||
reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -2375,10 +2375,10 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
||||
self.dismissed?()
|
||||
}
|
||||
|
||||
public func dismissWithReaction(value: String, targetView: UIView, hideNode: Bool, completion: (() -> Void)?) {
|
||||
public func dismissWithReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, completion: (() -> Void)?) {
|
||||
if !self.wasDismissed {
|
||||
self.wasDismissed = true
|
||||
self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: { [weak self] in
|
||||
self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, completion: { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
completion?()
|
||||
})
|
||||
|
@ -675,7 +675,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
||||
}
|
||||
}
|
||||
|
||||
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, completion: @escaping () -> Void) {
|
||||
guard let reactionContextNode = self.reactionContextNode else {
|
||||
self.requestAnimateOut(.default, completion)
|
||||
return
|
||||
@ -697,7 +697,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, completion: { [weak self] in
|
||||
reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ protocol ContextControllerPresentationNode: ASDisplayNode {
|
||||
stateTransition: ContextControllerPresentationNodeStateTransition?
|
||||
)
|
||||
|
||||
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void)
|
||||
func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, completion: @escaping () -> Void)
|
||||
func cancelReactionAnimation()
|
||||
|
||||
func highlightGestureMoved(location: CGPoint)
|
||||
|
@ -57,6 +57,9 @@ private final class ChildWindowHostView: UIView, WindowHost {
|
||||
|
||||
func presentInGlobalOverlay(_ controller: ContainableController) {
|
||||
}
|
||||
|
||||
func addGlobalPortalHostView(sourceView: PortalSourceView) {
|
||||
}
|
||||
}
|
||||
|
||||
public func childWindowHostView(parent: UIView) -> WindowHostView {
|
||||
|
@ -53,6 +53,8 @@ final class GlobalOverlayPresentationContext {
|
||||
|
||||
private(set) var controllers: [ContainableController] = []
|
||||
|
||||
private var globalPortalViews: [GlobalPortalView] = []
|
||||
|
||||
private var presentationDisposables = DisposableSet()
|
||||
private var layout: ContainerViewLayout?
|
||||
|
||||
@ -184,6 +186,34 @@ final class GlobalOverlayPresentationContext {
|
||||
transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
controller.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
for globalPortalView in self.globalPortalViews {
|
||||
transition.updateFrame(view: globalPortalView.view, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func addGlobalPortalHostView(sourceView: PortalSourceView) {
|
||||
guard let globalPortalView = GlobalPortalView(wasRemoved: { [weak self] globalPortalView in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let index = strongSelf.globalPortalViews.firstIndex(where: { $0 === globalPortalView }) {
|
||||
strongSelf.globalPortalViews.remove(at: index)
|
||||
}
|
||||
globalPortalView.view.removeFromSuperview()
|
||||
}) else {
|
||||
return
|
||||
}
|
||||
|
||||
globalPortalView.view.isUserInteractionEnabled = false
|
||||
self.globalPortalViews.append(globalPortalView)
|
||||
|
||||
sourceView.setGlobalPortal(view: globalPortalView)
|
||||
|
||||
if let presentationView = self.currentPresentationView(underStatusBar: true), let initialLayout = self.layout {
|
||||
presentationView.addSubview(globalPortalView.view)
|
||||
globalPortalView.view.frame = CGRect(origin: CGPoint(), size: initialLayout.size)
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,6 +250,14 @@ final class GlobalOverlayPresentationContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.globalPortalViews.isEmpty, let view = self.currentPresentationView(underStatusBar: true) {
|
||||
for globalPortalView in self.globalPortalViews {
|
||||
view.addSubview(globalPortalView.view)
|
||||
|
||||
globalPortalView.view.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,6 +267,10 @@ final class GlobalOverlayPresentationContext {
|
||||
controller.view.removeFromSuperview()
|
||||
controller.viewDidDisappear(false)
|
||||
}
|
||||
|
||||
for globalPortalView in self.globalPortalViews {
|
||||
globalPortalView.view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
|
19
submodules/Display/Source/GlobalPortalView.swift
Normal file
19
submodules/Display/Source/GlobalPortalView.swift
Normal file
@ -0,0 +1,19 @@
|
||||
import UIKit
|
||||
|
||||
final class GlobalPortalView: PortalView {
|
||||
private let wasRemoved: (GlobalPortalView) -> Void
|
||||
|
||||
init?(wasRemoved: @escaping (GlobalPortalView) -> Void) {
|
||||
self.wasRemoved = wasRemoved
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func triggerWasRemoved() {
|
||||
self.wasRemoved(self)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AudioToolbox
|
||||
import CoreHaptics
|
||||
|
||||
public enum ImpactHapticFeedbackStyle: Hashable {
|
||||
case light
|
||||
@ -193,3 +194,38 @@ public final class HapticFeedback {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public final class ContinuousHaptic {
|
||||
private let engine: CHHapticEngine
|
||||
private let player: CHHapticPatternPlayer
|
||||
|
||||
public init(duration: Double) throws {
|
||||
self.engine = try CHHapticEngine()
|
||||
|
||||
var events: [CHHapticEvent] = []
|
||||
for i in 0 ... 10 {
|
||||
let t = CGFloat(i) / 10.0
|
||||
|
||||
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float((1.0 - t) * 0.1 + t * 1.0))
|
||||
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
|
||||
let eventDuration: Double
|
||||
if i == 10 {
|
||||
eventDuration = 100.0
|
||||
} else {
|
||||
eventDuration = duration
|
||||
}
|
||||
let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness], relativeTime: Double(i) / 10.0 * duration, duration: eventDuration)
|
||||
events.append(event)
|
||||
}
|
||||
|
||||
let pattern = try CHHapticPattern(events: events, parameters: [])
|
||||
self.player = try self.engine.makePlayer(with: pattern)
|
||||
|
||||
try self.engine.start()
|
||||
try self.player.start(atTime: 0)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.engine.stop(completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
@ -234,6 +234,7 @@ private final class NativeWindow: UIWindow, WindowHost {
|
||||
var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)?
|
||||
var presentController: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)?
|
||||
var presentControllerInGlobalOverlay: ((_ controller: ContainableController) -> Void)?
|
||||
var addGlobalPortalHostViewImpl: ((PortalSourceView) -> Void)?
|
||||
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
|
||||
var presentNativeImpl: ((UIViewController) -> Void)?
|
||||
var invalidateDeferScreenEdgeGestureImpl: (() -> Void)?
|
||||
@ -321,6 +322,10 @@ private final class NativeWindow: UIWindow, WindowHost {
|
||||
self.presentControllerInGlobalOverlay?(controller)
|
||||
}
|
||||
|
||||
func addGlobalPortalHostView(sourceView: PortalSourceView) {
|
||||
self.addGlobalPortalHostViewImpl?(sourceView)
|
||||
}
|
||||
|
||||
func presentNative(_ controller: UIViewController) {
|
||||
self.presentNativeImpl?(controller)
|
||||
}
|
||||
@ -396,6 +401,10 @@ public func nativeWindowHostView() -> (UIWindow & WindowHost, WindowHostView) {
|
||||
hostView?.presentInGlobalOverlay?(controller)
|
||||
}
|
||||
|
||||
window.addGlobalPortalHostViewImpl = { [weak hostView] sourceView in
|
||||
hostView?.addGlobalPortalHostViewImpl?(sourceView)
|
||||
}
|
||||
|
||||
window.presentNativeImpl = { [weak hostView] controller in
|
||||
hostView?.presentNative?(controller)
|
||||
}
|
||||
|
81
submodules/Display/Source/PortalSourceView.swift
Normal file
81
submodules/Display/Source/PortalSourceView.swift
Normal file
@ -0,0 +1,81 @@
|
||||
import UIKit
|
||||
|
||||
open class PortalSourceView: UIView {
|
||||
private final class PortalReference {
|
||||
weak var portalView: PortalView?
|
||||
|
||||
init(portalView: PortalView) {
|
||||
self.portalView = portalView
|
||||
}
|
||||
}
|
||||
|
||||
private var portalReferences: [PortalReference] = []
|
||||
private weak var globalPortalView: GlobalPortalView?
|
||||
|
||||
public final var needsGlobalPortal: Bool = false {
|
||||
didSet {
|
||||
if self.needsGlobalPortal != oldValue {
|
||||
if self.needsGlobalPortal {
|
||||
self.alpha = 0.0
|
||||
|
||||
if let windowHost = self.windowHost {
|
||||
windowHost.addGlobalPortalHostView(sourceView: self)
|
||||
}
|
||||
} else {
|
||||
self.alpha = 1.0
|
||||
|
||||
if let globalPortalView = self.globalPortalView {
|
||||
self.globalPortalView = nil
|
||||
|
||||
globalPortalView.triggerWasRemoved()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let globalPortalView = self.globalPortalView {
|
||||
globalPortalView.triggerWasRemoved()
|
||||
}
|
||||
}
|
||||
|
||||
public func addPortal(view: PortalView) {
|
||||
self.portalReferences.append(PortalReference(portalView: view))
|
||||
if self.window != nil {
|
||||
view.reloadPortal(sourceView: self)
|
||||
}
|
||||
}
|
||||
|
||||
func setGlobalPortal(view: GlobalPortalView?) {
|
||||
if let globalPortalView = self.globalPortalView {
|
||||
self.globalPortalView = nil
|
||||
|
||||
globalPortalView.triggerWasRemoved()
|
||||
}
|
||||
|
||||
if let view = view {
|
||||
self.globalPortalView = view
|
||||
|
||||
if self.window != nil {
|
||||
view.reloadPortal(sourceView: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override open func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
|
||||
if self.window != nil {
|
||||
for portalReference in self.portalReferences {
|
||||
if let portalView = portalReference.portalView {
|
||||
portalView.reloadPortal(sourceView: self)
|
||||
}
|
||||
}
|
||||
|
||||
if self.needsGlobalPortal, self.globalPortalView == nil, let windowHost = self.windowHost {
|
||||
windowHost.addGlobalPortalHostView(sourceView: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
submodules/Display/Source/PortalView.swift
Normal file
21
submodules/Display/Source/PortalView.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import UIKit
|
||||
import UIKitRuntimeUtils
|
||||
|
||||
public class PortalView {
|
||||
public let view: UIView & UIKitPortalViewProtocol
|
||||
|
||||
public init?() {
|
||||
guard let view = makePortalView() else {
|
||||
return nil
|
||||
}
|
||||
self.view = view
|
||||
}
|
||||
|
||||
func reloadPortal(sourceView: PortalSourceView) {
|
||||
self.view.sourceView = sourceView
|
||||
|
||||
if let portalSuperview = self.view.superview, let index = portalSuperview.subviews.firstIndex(of: self.view) {
|
||||
portalSuperview.insertSubview(self.view, at: index)
|
||||
}
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ private func findCurrentResponder(_ view: UIView) -> UIResponder? {
|
||||
}
|
||||
}
|
||||
|
||||
private func findWindow(_ view: UIView) -> WindowHost? {
|
||||
func findWindow(_ view: UIView) -> WindowHost? {
|
||||
if let view = view as? WindowHost {
|
||||
return view
|
||||
} else if let superview = view.superview {
|
||||
@ -569,6 +569,10 @@ public enum TabBarItemContextActionType {
|
||||
self.window?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
|
||||
public func addGlobalPortalHostView(sourceView: PortalSourceView) {
|
||||
self.window?.addGlobalPortalHostView(sourceView: sourceView)
|
||||
}
|
||||
|
||||
open override func viewWillDisappear(_ animated: Bool) {
|
||||
self.activeInputViewCandidate = findCurrentResponder(self.view)
|
||||
|
||||
|
@ -163,6 +163,7 @@ public final class WindowHostView {
|
||||
|
||||
var present: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)?
|
||||
var presentInGlobalOverlay: ((_ controller: ContainableController) -> Void)?
|
||||
var addGlobalPortalHostViewImpl: ((PortalSourceView) -> Void)?
|
||||
var presentNative: ((UIViewController) -> Void)?
|
||||
var nativeController: (() -> UIViewController?)?
|
||||
var updateSize: ((CGSize, Double) -> Void)?
|
||||
@ -200,12 +201,25 @@ public protocol WindowHost {
|
||||
func forEachController(_ f: (ContainableController) -> Void)
|
||||
func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void)
|
||||
func presentInGlobalOverlay(_ controller: ContainableController)
|
||||
func addGlobalPortalHostView(sourceView: PortalSourceView)
|
||||
func invalidateDeferScreenEdgeGestures()
|
||||
func invalidatePrefersOnScreenNavigationHidden()
|
||||
func invalidateSupportedOrientations()
|
||||
func cancelInteractiveKeyboardGestures()
|
||||
}
|
||||
|
||||
public extension UIView {
|
||||
var windowHost: WindowHost? {
|
||||
if let window = self.window as? WindowHost {
|
||||
return window
|
||||
} else if let result = findWindow(self) {
|
||||
return result
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func layoutMetricsForScreenSize(_ size: CGSize) -> LayoutMetrics {
|
||||
if size.width > 690.0 && size.height > 690.0 {
|
||||
return LayoutMetrics(widthClass: .regular, heightClass: .regular)
|
||||
@ -379,6 +393,10 @@ public class Window1 {
|
||||
self?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
|
||||
self.hostView.addGlobalPortalHostViewImpl = { [weak self] sourceView in
|
||||
self?.addGlobalPortalHostView(sourceView: sourceView)
|
||||
}
|
||||
|
||||
self.hostView.presentNative = { [weak self] controller in
|
||||
self?.presentNative(controller)
|
||||
}
|
||||
@ -1105,6 +1123,10 @@ public class Window1 {
|
||||
self.overlayPresentationContext.present(controller)
|
||||
}
|
||||
|
||||
public func addGlobalPortalHostView(sourceView: PortalSourceView) {
|
||||
self.overlayPresentationContext.addGlobalPortalHostView(sourceView: sourceView)
|
||||
}
|
||||
|
||||
public func presentNative(_ controller: UIViewController) {
|
||||
if let nativeController = self.hostView.nativeController?() {
|
||||
nativeController.present(controller, animated: true, completion: nil)
|
||||
|
@ -19,6 +19,7 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/Components/ReactionButtonListComponent:ReactionButtonListComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -163,11 +163,11 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
let smallCircleDuration: Double = 0.35
|
||||
let largeCircleDuration: Double = 0.35
|
||||
let largeCircleDelay: Double = 0.13
|
||||
let mainCircleDuration: Double = 0.25
|
||||
let mainCircleDelay: Double = 0.16
|
||||
let smallCircleDuration: Double = 0.4
|
||||
let largeCircleDuration: Double = 0.4
|
||||
let largeCircleDelay: Double = 0.0
|
||||
let mainCircleDuration: Double = 0.3
|
||||
let mainCircleDelay: Double = 0.0
|
||||
|
||||
self.smallCircleLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: smallCircleDuration, delay: 0.0)
|
||||
|
||||
@ -181,9 +181,9 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func animateInFromAnchorRect(size: CGSize, sourceBackgroundFrame: CGRect) {
|
||||
let springDuration: Double = 0.2
|
||||
let springDuration: Double = 0.3
|
||||
let springDamping: CGFloat = 104.0
|
||||
let springDelay: Double = 0.25
|
||||
let springDelay: Double = 0.05
|
||||
let shadowInset: CGFloat = 15.0
|
||||
|
||||
let contentBounds = self.backgroundNode.frame
|
||||
|
@ -6,6 +6,7 @@ import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TelegramAnimatedStickerNode
|
||||
import ReactionButtonListComponent
|
||||
|
||||
public final class ReactionContextItem {
|
||||
public struct Reaction: Equatable {
|
||||
@ -53,8 +54,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private let previewingItemContainer: ASDisplayNode
|
||||
private var visibleItemNodes: [Int: ReactionNode] = [:]
|
||||
|
||||
private weak var currentLongPressItemNode: ReactionNode?
|
||||
|
||||
private var isExpanded: Bool = true
|
||||
private var highlightedReaction: ReactionContextItem.Reaction?
|
||||
private var continuousHaptic: Any?
|
||||
private var validLayout: (CGSize, UIEdgeInsets, CGRect)?
|
||||
private var isLeftAligned: Bool = true
|
||||
|
||||
@ -82,6 +86,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.canCancelContentTouches = true
|
||||
self.scrollNode.clipsToBounds = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
@ -132,6 +137,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
|
||||
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))
|
||||
longPressGesture.minimumPressDuration = 0.2
|
||||
self.view.addGestureRecognizer(longPressGesture)
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, transition: ContainedViewLayoutTransition) {
|
||||
@ -142,7 +151,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.backgroundNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: transition)
|
||||
}
|
||||
|
||||
private func calculateBackgroundFrame(containerSize: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, contentSize: CGSize) -> (backgroundFrame: CGRect, isLeftAligned: Bool, cloudSourcePoint: CGFloat) {
|
||||
private func calculateBackgroundFrame(containerSize: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, contentSize: CGSize) -> (backgroundFrame: CGRect, visualBackgroundFrame: CGRect, isLeftAligned: Bool, cloudSourcePoint: CGFloat) {
|
||||
var contentSize = contentSize
|
||||
contentSize.width = max(52.0, contentSize.width)
|
||||
contentSize.height = 52.0
|
||||
@ -178,12 +187,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
cloudSourcePoint = max(rect.minX + rect.height / 2.0, anchorRect.minX)
|
||||
}
|
||||
|
||||
var visualRect = rect
|
||||
|
||||
if self.highlightedReaction != nil {
|
||||
rect.origin.x -= 2.0
|
||||
rect.size.width += 4.0
|
||||
visualRect.origin.x -= 4.0
|
||||
visualRect.size.width += 8.0
|
||||
}
|
||||
|
||||
return (rect, isLeftAligned, cloudSourcePoint)
|
||||
return (rect, visualRect, isLeftAligned, cloudSourcePoint)
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
@ -265,7 +276,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
itemNode.updateLayout(size: itemFrame.size, isExpanded: false, isPreviewing: isPreviewing, transition: transition)
|
||||
|
||||
if animateIn {
|
||||
itemNode.animateIn()
|
||||
itemNode.appear(animated: !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -305,41 +316,41 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
backgroundInsets.left += sideInset
|
||||
backgroundInsets.right += sideInset
|
||||
|
||||
let (backgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: CGSize(width: size.width, height: size.height), insets: backgroundInsets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight))
|
||||
let (actualBackgroundFrame, visualBackgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: CGSize(width: size.width, height: size.height), insets: backgroundInsets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight))
|
||||
self.isLeftAligned = isLeftAligned
|
||||
|
||||
transition.updateFrame(node: self.contentContainer, frame: backgroundFrame, beginWithCurrentState: true)
|
||||
transition.updateFrame(view: self.contentContainerMask, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size), beginWithCurrentState: true)
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size), beginWithCurrentState: true)
|
||||
transition.updateFrame(node: self.previewingItemContainer, frame: backgroundFrame, beginWithCurrentState: true)
|
||||
self.scrollNode.view.contentSize = CGSize(width: completeContentWidth, height: backgroundFrame.size.height)
|
||||
transition.updateFrame(node: self.contentContainer, frame: visualBackgroundFrame, beginWithCurrentState: true)
|
||||
transition.updateFrame(view: self.contentContainerMask, frame: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size), beginWithCurrentState: true)
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: actualBackgroundFrame.size), beginWithCurrentState: true)
|
||||
transition.updateFrame(node: self.previewingItemContainer, frame: visualBackgroundFrame, beginWithCurrentState: true)
|
||||
self.scrollNode.view.contentSize = CGSize(width: completeContentWidth, height: visualBackgroundFrame.size.height)
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame, beginWithCurrentState: true)
|
||||
transition.updateFrame(node: self.backgroundNode, frame: visualBackgroundFrame, beginWithCurrentState: true)
|
||||
self.backgroundNode.update(
|
||||
theme: self.theme,
|
||||
size: backgroundFrame.size,
|
||||
cloudSourcePoint: cloudSourcePoint - backgroundFrame.minX,
|
||||
size: visualBackgroundFrame.size,
|
||||
cloudSourcePoint: cloudSourcePoint - visualBackgroundFrame.minX,
|
||||
isLeftAligned: isLeftAligned,
|
||||
transition: transition
|
||||
)
|
||||
|
||||
if let animateInFromAnchorRect = animateInFromAnchorRect {
|
||||
let springDuration: Double = 0.42
|
||||
let springDuration: Double = 0.3
|
||||
let springDamping: CGFloat = 104.0
|
||||
let springDelay: Double = 0.22
|
||||
let springDelay: Double = 0.05
|
||||
|
||||
let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: backgroundFrame.height, height: contentHeight)).0
|
||||
let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: visualBackgroundFrame.height, height: contentHeight)).0
|
||||
|
||||
self.backgroundNode.animateInFromAnchorRect(size: backgroundFrame.size, sourceBackgroundFrame: sourceBackgroundFrame.offsetBy(dx: -backgroundFrame.minX, dy: -backgroundFrame.minY))
|
||||
self.backgroundNode.animateInFromAnchorRect(size: visualBackgroundFrame.size, sourceBackgroundFrame: sourceBackgroundFrame.offsetBy(dx: -visualBackgroundFrame.minX, dy: -visualBackgroundFrame.minY))
|
||||
|
||||
self.contentContainer.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - backgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true)
|
||||
self.contentContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: backgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping)
|
||||
self.contentContainer.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - visualBackgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true)
|
||||
self.contentContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping)
|
||||
} else if let animateOutToAnchorRect = animateOutToAnchorRect {
|
||||
let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)).0
|
||||
|
||||
let offset = CGPoint(x: -(targetBackgroundFrame.minX - backgroundFrame.minX), y: -(targetBackgroundFrame.minY - backgroundFrame.minY))
|
||||
let offset = CGPoint(x: -(targetBackgroundFrame.minX - visualBackgroundFrame.minX), y: -(targetBackgroundFrame.minY - visualBackgroundFrame.minY))
|
||||
self.position = CGPoint(x: self.position.x - offset.x, y: self.position.y - offset.y)
|
||||
self.layer.animatePosition(from: offset, to: CGPoint(), duration: 0.2, removeOnCompletion: true, additive: true)
|
||||
}
|
||||
@ -353,20 +364,29 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
//let mainCircleDuration: Double = 0.5
|
||||
let mainCircleDelay: Double = 0.1
|
||||
let mainCircleDelay: Double = 0.01
|
||||
|
||||
self.backgroundNode.animateIn()
|
||||
|
||||
self.didAnimateIn = true
|
||||
|
||||
for i in 0 ..< self.items.count {
|
||||
guard let itemNode = self.visibleItemNodes[i] else {
|
||||
continue
|
||||
if !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion {
|
||||
for i in 0 ..< self.items.count {
|
||||
guard let itemNode = self.visibleItemNodes[i] else {
|
||||
continue
|
||||
}
|
||||
let itemDelay = mainCircleDelay + Double(i) * 0.06
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay, execute: { [weak itemNode] in
|
||||
itemNode?.appear(animated: true)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for i in 0 ..< self.items.count {
|
||||
guard let itemNode = self.visibleItemNodes[i] else {
|
||||
continue
|
||||
}
|
||||
itemNode.appear(animated: false)
|
||||
}
|
||||
let itemDelay = mainCircleDelay + 0.1 + Double(i) * 0.035
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + itemDelay, execute: { [weak itemNode] in
|
||||
itemNode?.animateIn()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -386,6 +406,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
if "".isEmpty {
|
||||
if hideNode {
|
||||
targetView.alpha = 1.0
|
||||
targetView.isHidden = false
|
||||
}
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
guard let targetSnapshotView = targetView.snapshotContentTree(unhide: true) else {
|
||||
completion()
|
||||
return
|
||||
@ -419,6 +448,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
targetSnapshotView?.isHidden = true
|
||||
|
||||
if hideNode {
|
||||
targetView.alpha = 1.0
|
||||
targetView.isHidden = false
|
||||
targetSnapshotView?.isHidden = true
|
||||
targetScaleCompleted = true
|
||||
@ -441,7 +471,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
public func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, completion: @escaping () -> Void) {
|
||||
for (_, itemNode) in self.visibleItemNodes {
|
||||
if itemNode.item.reaction.rawValue != value {
|
||||
continue
|
||||
@ -450,53 +480,48 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.animationTargetView = targetView
|
||||
self.animationHideNode = hideNode
|
||||
|
||||
/*let standaloneReactionAnimation = StandaloneReactionAnimation()
|
||||
self.standaloneReactionAnimation = standaloneReactionAnimation
|
||||
standaloneReactionAnimation.frame = self.bounds
|
||||
self.addSubnode(standaloneReactionAnimation)
|
||||
standaloneReactionAnimation.animateReactionSelection(context: itemNode.context, theme: self.theme, reaction: itemNode.item, targetView: targetView, currentItemNode: itemNode, hideNode: hideNode, completion: completion)
|
||||
|
||||
return*/
|
||||
|
||||
if hideNode {
|
||||
targetView.isHidden = true
|
||||
if let animateTargetContainer = animateTargetContainer {
|
||||
animateTargetContainer.isHidden = true
|
||||
targetView.isHidden = true
|
||||
} else {
|
||||
targetView.alpha = 0.0
|
||||
targetView.layer.animateAlpha(from: targetView.alpha, to: 0.0, duration: 0.2, completion: { [weak targetView] completed in
|
||||
if completed {
|
||||
targetView?.isHidden = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let itemSize: CGFloat = 40.0
|
||||
|
||||
itemNode.isExtracted = true
|
||||
let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view)
|
||||
let selfTargetRect = self.view.convert(targetView.bounds, from: targetView)
|
||||
|
||||
let expandedScale: CGFloat = 4.0
|
||||
let expandedSize = CGSize(width: floor(itemSize * expandedScale), height: floor(itemSize * expandedScale))
|
||||
let expandedSize: CGSize
|
||||
if targetView.bounds.width < 20.0 {
|
||||
expandedSize = CGSize(width: 21.0, height: 21.0)
|
||||
} else {
|
||||
expandedSize = CGSize(width: 32.0, height: 32.0)
|
||||
}
|
||||
|
||||
var expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize)
|
||||
if expandedFrame.minX < -floor(expandedFrame.width * 0.05) {
|
||||
expandedFrame.origin.x = -floor(expandedFrame.width * 0.05)
|
||||
}
|
||||
expandedFrame.origin.y += UIScreenPixel
|
||||
|
||||
let effectFrame = expandedFrame.insetBy(dx: -60.0, dy: -60.0)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear)
|
||||
|
||||
self.addSubnode(itemNode)
|
||||
//itemNode.position = selfSourceRect.center
|
||||
itemNode.position = expandedFrame.center
|
||||
transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size))
|
||||
itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, isPreviewing: false, transition: transition)
|
||||
|
||||
transition.animatePositionWithKeyframes(node: itemNode, keyframes: generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0))
|
||||
|
||||
let additionalAnimationNode = AnimatedStickerNode()
|
||||
let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0
|
||||
let animationFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5)
|
||||
.offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0)
|
||||
|
||||
additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id)))
|
||||
additionalAnimationNode.frame = animationFrame
|
||||
if incomingMessage {
|
||||
additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
||||
}
|
||||
additionalAnimationNode.updateLayout(size: animationFrame.size)
|
||||
additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id)))
|
||||
additionalAnimationNode.frame = effectFrame
|
||||
additionalAnimationNode.updateLayout(size: effectFrame.size)
|
||||
self.addSubnode(additionalAnimationNode)
|
||||
|
||||
var mainAnimationCompleted = false
|
||||
@ -512,8 +537,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
intermediateCompletion()
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1 * UIView.animationDurationFactor(), execute: {
|
||||
transition.animatePositionWithKeyframes(node: itemNode, keyframes: generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0))
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.15 * UIView.animationDurationFactor(), execute: {
|
||||
additionalAnimationNode.visibility = true
|
||||
if let animateTargetContainer = animateTargetContainer {
|
||||
animateTargetContainer.isHidden = false
|
||||
animateTargetContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
animateTargetContainer.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
||||
}
|
||||
})
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, 2.0 * UIView.animationDurationFactor()), execute: {
|
||||
@ -536,12 +568,47 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
let point = recognizer.location(in: self.view)
|
||||
if let itemNode = self.reactionItemNode(at: point) {
|
||||
self.highlightedReaction = itemNode.item.reaction
|
||||
if #available(iOS 13.0, *) {
|
||||
self.continuousHaptic = try? ContinuousHaptic(duration: 2.5)
|
||||
}
|
||||
//itemNode.updateIsLongPressing(isLongPressing: true)
|
||||
|
||||
if self.hapticFeedback == nil {
|
||||
self.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
|
||||
if let (size, insets, anchorRect) = self.validLayout {
|
||||
self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .animated(duration: 2.5, curve: .linear), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true)
|
||||
}
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
self.continuousHaptic = nil
|
||||
if let itemNode = self.currentLongPressItemNode {
|
||||
self.currentLongPressItemNode = nil
|
||||
self.reactionSelected?(itemNode.item)
|
||||
itemNode.updateIsLongPressing(isLongPressing: false)
|
||||
}
|
||||
self.highlightGestureFinished(performAction: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
let point = recognizer.location(in: self.view)
|
||||
if let reaction = self.reaction(at: point) {
|
||||
self.reactionSelected?(reaction)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -584,7 +651,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
var closestItem: (index: Int, distance: CGFloat)?
|
||||
|
||||
for (index, itemNode) in self.visibleItemNodes {
|
||||
let intersectionItemFrame = CGRect(origin: CGPoint(x: itemNode.frame.midX - itemSize / 2.0, y: itemNode.frame.midY - 1.0), size: CGSize(width: itemSize, height: 2.0))
|
||||
let intersectionItemFrame = CGRect(origin: CGPoint(x: itemNode.position.x - itemSize / 2.0, y: itemNode.position.y - 1.0), size: CGSize(width: itemSize, height: 2.0))
|
||||
|
||||
if !self.scrollNode.bounds.contains(intersectionItemFrame) {
|
||||
continue
|
||||
@ -605,7 +672,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func reaction(at point: CGPoint) -> ReactionContextItem? {
|
||||
private func reactionItemNode(at point: CGPoint) -> ReactionNode? {
|
||||
for i in 0 ..< 2 {
|
||||
let touchInset: CGFloat = i == 0 ? 0.0 : 8.0
|
||||
for (_, itemNode) in self.visibleItemNodes {
|
||||
@ -614,13 +681,17 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
let itemPoint = self.view.convert(point, to: itemNode.view)
|
||||
if itemNode.bounds.insetBy(dx: -touchInset, dy: -touchInset).contains(itemPoint) {
|
||||
return itemNode.item
|
||||
return itemNode
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func reaction(at point: CGPoint) -> ReactionContextItem? {
|
||||
return self.reactionItemNode(at: point)?.item
|
||||
}
|
||||
|
||||
public func performReactionSelection(reaction: ReactionContextItem.Reaction) {
|
||||
for (_, itemNode) in self.visibleItemNodes {
|
||||
if itemNode.item.reaction == reaction {
|
||||
@ -634,6 +705,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.standaloneReactionAnimation?.cancel()
|
||||
|
||||
if let animationTargetView = self.animationTargetView, self.animationHideNode {
|
||||
animationTargetView.alpha = 1.0
|
||||
animationTargetView.isHidden = false
|
||||
}
|
||||
}
|
||||
@ -695,22 +767,25 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
}
|
||||
|
||||
itemNode.isExtracted = true
|
||||
let sourceItemSize: CGFloat = 40.0
|
||||
let selfTargetRect = self.view.convert(targetView.bounds, from: targetView)
|
||||
|
||||
let expandedScale: CGFloat = 3.0
|
||||
let expandedSize = CGSize(width: floor(sourceItemSize * expandedScale), height: floor(sourceItemSize * expandedScale))
|
||||
let expandedSize: CGSize
|
||||
if targetView.bounds.width < 20.0 {
|
||||
expandedSize = CGSize(width: 21.0, height: 21.0)
|
||||
} else {
|
||||
expandedSize = CGSize(width: 32.0, height: 32.0)
|
||||
}
|
||||
|
||||
var expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize)
|
||||
if expandedFrame.minX < -floor(expandedFrame.width * 0.05) {
|
||||
expandedFrame.origin.x = -floor(expandedFrame.width * 0.05)
|
||||
}
|
||||
expandedFrame.origin.y += UIScreenPixel
|
||||
|
||||
let effectFrame = expandedFrame.insetBy(dx: -60.0, dy: -60.0)
|
||||
|
||||
sourceSnapshotView.frame = selfTargetRect
|
||||
self.view.addSubview(sourceSnapshotView)
|
||||
sourceSnapshotView.alpha = 0.0
|
||||
sourceSnapshotView.layer.animateSpring(from: 1.0 as NSNumber, to: (expandedFrame.width / selfTargetRect.width) as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||
sourceSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, completion: { [weak sourceSnapshotView] _ in
|
||||
sourceSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.01, completion: { [weak sourceSnapshotView] _ in
|
||||
sourceSnapshotView?.removeFromSuperview()
|
||||
})
|
||||
|
||||
@ -719,23 +794,18 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, isPreviewing: false, transition: .immediate)
|
||||
|
||||
itemNode.layer.animateSpring(from: (selfTargetRect.width / expandedFrame.width) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04)
|
||||
|
||||
if targetView.bounds.width < 20.0 {
|
||||
itemNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.15)
|
||||
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
let additionalAnimationNode = AnimatedStickerNode()
|
||||
let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0
|
||||
let animationFrame = expandedFrame.insetBy(dx: -expandedFrame.width * 0.5, dy: -expandedFrame.height * 0.5)
|
||||
.offsetBy(dx: incomingMessage ? (expandedFrame.width - 50.0) : (-expandedFrame.width + 50.0), dy: 0.0)
|
||||
|
||||
additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(animationFrame.width * 2.0), height: Int(animationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id)))
|
||||
additionalAnimationNode.frame = animationFrame
|
||||
if incomingMessage {
|
||||
additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
||||
}
|
||||
additionalAnimationNode.updateLayout(size: animationFrame.size)
|
||||
additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: itemNode.item.applicationAnimation.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(itemNode.item.applicationAnimation.resource.id)))
|
||||
additionalAnimationNode.frame = effectFrame
|
||||
additionalAnimationNode.updateLayout(size: effectFrame.size)
|
||||
self.addSubnode(additionalAnimationNode)
|
||||
|
||||
additionalAnimationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||
|
||||
var mainAnimationCompleted = false
|
||||
var additionalAnimationCompleted = false
|
||||
let intermediateCompletion: () -> Void = {
|
||||
@ -779,6 +849,15 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
}
|
||||
|
||||
private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) {
|
||||
if "".isEmpty {
|
||||
if hideNode {
|
||||
targetView.alpha = 1.0
|
||||
targetView.isHidden = false
|
||||
}
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
guard let targetSnapshotView = targetView.snapshotContentTree(unhide: true) else {
|
||||
completion()
|
||||
return
|
||||
@ -812,6 +891,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
targetSnapshotView?.isHidden = true
|
||||
|
||||
if hideNode {
|
||||
targetView.alpha = 1.0
|
||||
targetView.isHidden = false
|
||||
targetSnapshotView?.isHidden = true
|
||||
targetScaleCompleted = true
|
||||
@ -834,6 +914,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
|
||||
self.isCancelled = true
|
||||
|
||||
if let targetView = self.targetView, self.hideNode {
|
||||
targetView.alpha = 1.0
|
||||
targetView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,9 @@ final class ReactionNode: ASDisplayNode {
|
||||
|
||||
var didSetupStillAnimation: Bool = false
|
||||
|
||||
private var isLongPressing: Bool = false
|
||||
private var longPressAnimator: DisplayLinkAnimator?
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, item: ReactionContextItem) {
|
||||
self.context = context
|
||||
self.item = item
|
||||
@ -94,8 +97,12 @@ final class ReactionNode: ASDisplayNode {
|
||||
self.fetchFullAnimationDisposable?.dispose()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.animateInAnimationNode?.visibility = true
|
||||
func appear(animated: Bool) {
|
||||
if animated {
|
||||
self.animateInAnimationNode?.visibility = true
|
||||
} else {
|
||||
self.animateInAnimationNode?.completed(true)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, isExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) {
|
||||
@ -113,14 +120,18 @@ final class ReactionNode: ASDisplayNode {
|
||||
var animationFrame = CGRect(origin: CGPoint(x: floor((intrinsicSize.width - animationDisplaySize.width) / 2.0), y: floor((intrinsicSize.height - animationDisplaySize.height) / 2.0)), size: animationDisplaySize)
|
||||
animationFrame.origin.y = floor(animationFrame.origin.y + animationFrame.height * offsetFactor)
|
||||
|
||||
let expandedInset: CGFloat = floor(size.width / 32.0 * 60.0)
|
||||
let expandedAnimationFrame = animationFrame.insetBy(dx: -expandedInset, dy: -expandedInset)
|
||||
|
||||
if isExpanded, self.animationNode == nil {
|
||||
let animationNode = AnimatedStickerNode()
|
||||
animationNode.automaticallyLoadFirstFrame = true
|
||||
self.animationNode = animationNode
|
||||
self.addSubnode(animationNode)
|
||||
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.listAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.listAnimation.resource.id)))
|
||||
animationNode.frame = animationFrame
|
||||
animationNode.updateLayout(size: animationFrame.size)
|
||||
animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.listAnimation.resource), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.listAnimation.resource.id)))
|
||||
animationNode.frame = expandedAnimationFrame
|
||||
animationNode.updateLayout(size: expandedAnimationFrame.size)
|
||||
|
||||
if transition.isAnimated {
|
||||
if let stillAnimationNode = self.stillAnimationNode, !stillAnimationNode.frame.isEmpty {
|
||||
@ -172,7 +183,9 @@ final class ReactionNode: ASDisplayNode {
|
||||
}
|
||||
self.staticAnimationNode.isHidden = true
|
||||
}
|
||||
animationNode.visibility = true
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.17, execute: {
|
||||
animationNode.visibility = true
|
||||
})
|
||||
}
|
||||
|
||||
if self.validSize != size {
|
||||
@ -262,4 +275,32 @@ final class ReactionNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsLongPressing(isLongPressing: Bool) {
|
||||
if self.isLongPressing == isLongPressing {
|
||||
return
|
||||
}
|
||||
self.isLongPressing = isLongPressing
|
||||
|
||||
if isLongPressing {
|
||||
if self.longPressAnimator == nil {
|
||||
let longPressAnimator = DisplayLinkAnimator(duration: 2.0, from: 1.0, to: 2.0, update: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let transition: ContainedViewLayoutTransition = .immediate
|
||||
transition.updateSublayerTransformScale(node: strongSelf, scale: value)
|
||||
}, completion: {
|
||||
})
|
||||
self.longPressAnimator = longPressAnimator
|
||||
}
|
||||
} else if let longPressAnimator = self.longPressAnimator {
|
||||
self.longPressAnimator = nil
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
|
||||
transition.updateSublayerTransformScale(node: self, scale: 1.0)
|
||||
|
||||
longPressAnimator.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,87 +1 @@
|
||||
/*import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
|
||||
public final class ReactionSelectionParentNode: ASDisplayNode {
|
||||
private let account: Account
|
||||
private let theme: PresentationTheme
|
||||
|
||||
private var currentNode: ReactionSelectionNode?
|
||||
private var currentLocation: (CGPoint, CGFloat, CGPoint)?
|
||||
|
||||
private var validLayout: (size: CGSize, insets: UIEdgeInsets)?
|
||||
|
||||
public init(account: Account, theme: PresentationTheme) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func displayReactions(_ reactions: [ReactionGestureItem], at point: CGPoint, touchPoint: CGPoint) {
|
||||
if let currentNode = self.currentNode {
|
||||
currentNode.removeFromSupernode()
|
||||
self.currentNode = nil
|
||||
}
|
||||
|
||||
let reactionNode = ReactionSelectionNode(account: self.account, theme: self.theme, reactions: reactions)
|
||||
self.addSubnode(reactionNode)
|
||||
self.currentNode = reactionNode
|
||||
self.currentLocation = (point, point.x, touchPoint)
|
||||
|
||||
if let (size, insets) = self.validLayout {
|
||||
self.update(size: size, insets: insets, isInitial: true)
|
||||
|
||||
reactionNode.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
func selectedReaction() -> ReactionGestureItem? {
|
||||
if let currentNode = self.currentNode {
|
||||
return currentNode.selectedReaction()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dismissReactions(into targetNode: ASDisplayNode?, hideTarget: Bool) {
|
||||
if let currentNode = self.currentNode {
|
||||
currentNode.animateOut(into: targetNode, hideTarget: hideTarget, completion: { [weak currentNode] in
|
||||
currentNode?.removeFromSupernode()
|
||||
})
|
||||
self.currentNode = nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateReactionsAnchor(point: CGPoint, touchPoint: CGPoint) {
|
||||
if let (currentPoint, _, _) = self.currentLocation {
|
||||
self.currentLocation = (currentPoint, point.x, touchPoint)
|
||||
|
||||
if let (size, insets) = self.validLayout {
|
||||
self.update(size: size, insets: insets, isInitial: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (size, insets)
|
||||
|
||||
self.update(size: size, insets: insets, isInitial: false)
|
||||
}
|
||||
|
||||
private func update(size: CGSize, insets: UIEdgeInsets, isInitial: Bool) {
|
||||
if let currentNode = self.currentNode, let (point, offset, touchPoint) = self.currentLocation {
|
||||
currentNode.updateLayout(constrainedSize: size, startingPoint: CGPoint(x: size.width - 32.0, y: point.y), offsetFromStart: offset, isInitial: isInitial, touchPoint: touchPoint)
|
||||
currentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
|
@ -92,6 +92,7 @@ swift_library(
|
||||
"//submodules/DebugSettingsUI:DebugSettingsUI",
|
||||
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
|
||||
"//submodules/WebPBinding:WebPBinding",
|
||||
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
|
||||
"//submodules/Translate:Translate",
|
||||
"//submodules/QrCodeUI:QrCodeUI",
|
||||
],
|
||||
|
@ -11,6 +11,7 @@ import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import WebPBinding
|
||||
import ReactionImageComponent
|
||||
|
||||
private final class QuickReactionSetupControllerArguments {
|
||||
let context: AccountContext
|
||||
|
@ -16,6 +16,7 @@ import ItemListPeerActionItem
|
||||
import UndoUI
|
||||
import ShareController
|
||||
import WebPBinding
|
||||
import ReactionImageComponent
|
||||
|
||||
private final class InstalledStickerPacksControllerArguments {
|
||||
let account: Account
|
||||
|
@ -792,7 +792,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[594408994] = { return Api.EmojiKeyword.parse_emojiKeywordDeleted($0) }
|
||||
dict[-290921362] = { return Api.upload.CdnFile.parse_cdnFileReuploadNeeded($0) }
|
||||
dict[-1449145777] = { return Api.upload.CdnFile.parse_cdnFile($0) }
|
||||
dict[35486795] = { return Api.AvailableReaction.parse_availableReaction($0) }
|
||||
dict[-1065882623] = { return Api.AvailableReaction.parse_availableReaction($0) }
|
||||
dict[415997816] = { return Api.help.InviteText.parse_inviteText($0) }
|
||||
dict[-1826077446] = { return Api.MessageUserReaction.parse_messageUserReaction($0) }
|
||||
dict[1984755728] = { return Api.BotInlineMessage.parse_botInlineMessageMediaAuto($0) }
|
||||
|
@ -20236,13 +20236,13 @@ public extension Api {
|
||||
|
||||
}
|
||||
public enum AvailableReaction: TypeConstructorDescription {
|
||||
case availableReaction(flags: Int32, reaction: String, title: String, staticIcon: Api.Document, appearAnimation: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document)
|
||||
case availableReaction(flags: Int32, reaction: String, title: String, staticIcon: Api.Document, appearAnimation: Api.Document, selectAnimation: Api.Document, activateAnimation: Api.Document, effectAnimation: Api.Document, aroundAnimation: Api.Document?, centerIcon: Api.Document?)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .availableReaction(let flags, let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation):
|
||||
case .availableReaction(let flags, let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation, let aroundAnimation, let centerIcon):
|
||||
if boxed {
|
||||
buffer.appendInt32(35486795)
|
||||
buffer.appendInt32(-1065882623)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeString(reaction, buffer: buffer, boxed: false)
|
||||
@ -20252,14 +20252,16 @@ public extension Api {
|
||||
selectAnimation.serialize(buffer, true)
|
||||
activateAnimation.serialize(buffer, true)
|
||||
effectAnimation.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 1) != 0 {aroundAnimation!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 1) != 0 {centerIcon!.serialize(buffer, true)}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .availableReaction(let flags, let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation):
|
||||
return ("availableReaction", [("flags", flags), ("reaction", reaction), ("title", title), ("staticIcon", staticIcon), ("appearAnimation", appearAnimation), ("selectAnimation", selectAnimation), ("activateAnimation", activateAnimation), ("effectAnimation", effectAnimation)])
|
||||
case .availableReaction(let flags, let reaction, let title, let staticIcon, let appearAnimation, let selectAnimation, let activateAnimation, let effectAnimation, let aroundAnimation, let centerIcon):
|
||||
return ("availableReaction", [("flags", flags), ("reaction", reaction), ("title", title), ("staticIcon", staticIcon), ("appearAnimation", appearAnimation), ("selectAnimation", selectAnimation), ("activateAnimation", activateAnimation), ("effectAnimation", effectAnimation), ("aroundAnimation", aroundAnimation), ("centerIcon", centerIcon)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -20290,6 +20292,14 @@ public extension Api {
|
||||
if let signature = reader.readInt32() {
|
||||
_8 = Api.parse(reader, signature: signature) as? Api.Document
|
||||
}
|
||||
var _9: Api.Document?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
|
||||
_9 = Api.parse(reader, signature: signature) as? Api.Document
|
||||
} }
|
||||
var _10: Api.Document?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
|
||||
_10 = Api.parse(reader, signature: signature) as? Api.Document
|
||||
} }
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
@ -20298,8 +20308,10 @@ public extension Api {
|
||||
let _c6 = _6 != nil
|
||||
let _c7 = _7 != nil
|
||||
let _c8 = _8 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
|
||||
return Api.AvailableReaction.availableReaction(flags: _1!, reaction: _2!, title: _3!, staticIcon: _4!, appearAnimation: _5!, selectAnimation: _6!, activateAnimation: _7!, effectAnimation: _8!)
|
||||
let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil
|
||||
let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 {
|
||||
return Api.AvailableReaction.availableReaction(flags: _1!, reaction: _2!, title: _3!, staticIcon: _4!, appearAnimation: _5!, selectAnimation: _6!, activateAnimation: _7!, effectAnimation: _8!, aroundAnimation: _9, centerIcon: _10)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
@ -14,6 +14,8 @@ public final class AvailableReactions: Equatable, Codable {
|
||||
case selectAnimation
|
||||
case activateAnimation
|
||||
case effectAnimation
|
||||
case aroundAnimation
|
||||
case centerAnimation
|
||||
}
|
||||
|
||||
public let isEnabled: Bool
|
||||
@ -24,6 +26,8 @@ public final class AvailableReactions: Equatable, Codable {
|
||||
public let selectAnimation: TelegramMediaFile
|
||||
public let activateAnimation: TelegramMediaFile
|
||||
public let effectAnimation: TelegramMediaFile
|
||||
public let aroundAnimation: TelegramMediaFile?
|
||||
public let centerAnimation: TelegramMediaFile?
|
||||
|
||||
public init(
|
||||
isEnabled: Bool,
|
||||
@ -33,7 +37,9 @@ public final class AvailableReactions: Equatable, Codable {
|
||||
appearAnimation: TelegramMediaFile,
|
||||
selectAnimation: TelegramMediaFile,
|
||||
activateAnimation: TelegramMediaFile,
|
||||
effectAnimation: TelegramMediaFile
|
||||
effectAnimation: TelegramMediaFile,
|
||||
aroundAnimation: TelegramMediaFile?,
|
||||
centerAnimation: TelegramMediaFile?
|
||||
) {
|
||||
self.isEnabled = isEnabled
|
||||
self.value = value
|
||||
@ -43,6 +49,8 @@ public final class AvailableReactions: Equatable, Codable {
|
||||
self.selectAnimation = selectAnimation
|
||||
self.activateAnimation = activateAnimation
|
||||
self.effectAnimation = effectAnimation
|
||||
self.aroundAnimation = aroundAnimation
|
||||
self.centerAnimation = centerAnimation
|
||||
}
|
||||
|
||||
public static func ==(lhs: Reaction, rhs: Reaction) -> Bool {
|
||||
@ -70,6 +78,12 @@ public final class AvailableReactions: Equatable, Codable {
|
||||
if lhs.effectAnimation != rhs.effectAnimation {
|
||||
return false
|
||||
}
|
||||
if lhs.aroundAnimation != rhs.aroundAnimation {
|
||||
return false
|
||||
}
|
||||
if lhs.centerAnimation != rhs.centerAnimation {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -95,6 +109,18 @@ public final class AvailableReactions: Equatable, Codable {
|
||||
|
||||
let effectAnimationData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .effectAnimation)
|
||||
self.effectAnimation = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: effectAnimationData.data)))
|
||||
|
||||
if let aroundAnimationData = try container.decodeIfPresent(AdaptedPostboxDecoder.RawObjectData.self, forKey: .aroundAnimation) {
|
||||
self.aroundAnimation = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: aroundAnimationData.data)))
|
||||
} else {
|
||||
self.aroundAnimation = nil
|
||||
}
|
||||
|
||||
if let centerAnimationData = try container.decodeIfPresent(AdaptedPostboxDecoder.RawObjectData.self, forKey: .centerAnimation) {
|
||||
self.centerAnimation = TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: centerAnimationData.data)))
|
||||
} else {
|
||||
self.centerAnimation = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -110,6 +136,12 @@ public final class AvailableReactions: Equatable, Codable {
|
||||
try container.encode(PostboxEncoder().encodeObjectToRawData(self.selectAnimation), forKey: .selectAnimation)
|
||||
try container.encode(PostboxEncoder().encodeObjectToRawData(self.activateAnimation), forKey: .activateAnimation)
|
||||
try container.encode(PostboxEncoder().encodeObjectToRawData(self.effectAnimation), forKey: .effectAnimation)
|
||||
if let aroundAnimation = self.aroundAnimation {
|
||||
try container.encode(PostboxEncoder().encodeObjectToRawData(aroundAnimation), forKey: .aroundAnimation)
|
||||
}
|
||||
if let centerAnimation = self.centerAnimation {
|
||||
try container.encode(PostboxEncoder().encodeObjectToRawData(centerAnimation), forKey: .centerAnimation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,7 +189,7 @@ public final class AvailableReactions: Equatable, Codable {
|
||||
private extension AvailableReactions.Reaction {
|
||||
convenience init?(apiReaction: Api.AvailableReaction) {
|
||||
switch apiReaction {
|
||||
case let .availableReaction(flags, reaction, title, staticIcon, appearAnimation, selectAnimation, activateAnimation, effectAnimation):
|
||||
case let .availableReaction(flags, reaction, title, staticIcon, appearAnimation, selectAnimation, activateAnimation, effectAnimation, aroundAnimation, centerIcon):
|
||||
guard let staticIconFile = telegramMediaFileFromApiDocument(staticIcon) else {
|
||||
return nil
|
||||
}
|
||||
@ -173,6 +205,8 @@ private extension AvailableReactions.Reaction {
|
||||
guard let effectAnimationFile = telegramMediaFileFromApiDocument(effectAnimation) else {
|
||||
return nil
|
||||
}
|
||||
let aroundAnimationFile = aroundAnimation.flatMap(telegramMediaFileFromApiDocument)
|
||||
let centerAnimationFile = centerIcon.flatMap(telegramMediaFileFromApiDocument)
|
||||
let isEnabled = (flags & (1 << 0)) == 0
|
||||
self.init(
|
||||
isEnabled: isEnabled,
|
||||
@ -182,7 +216,9 @@ private extension AvailableReactions.Reaction {
|
||||
appearAnimation: appearAnimationFile,
|
||||
selectAnimation: selectAnimationFile,
|
||||
activateAnimation: activateAnimationFile,
|
||||
effectAnimation: effectAnimationFile
|
||||
effectAnimation: effectAnimationFile,
|
||||
aroundAnimation: aroundAnimationFile,
|
||||
centerAnimation: centerAnimationFile
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -251,6 +287,12 @@ func managedSynchronizeAvailableReactions(postbox: Postbox, network: Network) ->
|
||||
resources.append(reaction.selectAnimation.resource)
|
||||
resources.append(reaction.activateAnimation.resource)
|
||||
resources.append(reaction.effectAnimation.resource)
|
||||
if let centerAnimation = reaction.centerAnimation {
|
||||
resources.append(centerAnimation.resource)
|
||||
}
|
||||
if let aroundAnimation = reaction.aroundAnimation {
|
||||
resources.append(aroundAnimation.resource)
|
||||
}
|
||||
}
|
||||
|
||||
for resource in resources {
|
||||
|
@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
|
||||
|
||||
public class Serialization: NSObject, MTSerialization {
|
||||
public func currentLayer() -> UInt {
|
||||
return 136
|
||||
return 137
|
||||
}
|
||||
|
||||
public func parseMessage(_ data: Data!) -> Any! {
|
||||
|
@ -251,6 +251,7 @@ swift_library(
|
||||
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
|
||||
"//submodules/QrCodeUI:QrCodeUI",
|
||||
"//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent",
|
||||
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
|
||||
"//submodules/Translate:Translate",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||
|
@ -1008,6 +1008,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if !reaction.isEnabled {
|
||||
continue
|
||||
}
|
||||
guard let centerAnimation = reaction.centerAnimation else {
|
||||
continue
|
||||
}
|
||||
guard let aroundAnimation = reaction.aroundAnimation else {
|
||||
continue
|
||||
}
|
||||
|
||||
switch allowedReactions {
|
||||
case let .set(set):
|
||||
@ -1021,8 +1027,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
|
||||
appearAnimation: reaction.appearAnimation,
|
||||
stillAnimation: reaction.selectAnimation,
|
||||
listAnimation: reaction.activateAnimation,
|
||||
applicationAnimation: reaction.effectAnimation
|
||||
listAnimation: centerAnimation,
|
||||
applicationAnimation: aroundAnimation
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -1038,6 +1044,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
var updatedReaction: String? = value.reaction.rawValue
|
||||
var isFirst = true
|
||||
for attribute in topMessage.attributes {
|
||||
if let attribute = attribute as? ReactionsMessageAttribute {
|
||||
for reaction in attribute.reactions {
|
||||
@ -1045,6 +1052,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if reaction.isSelected {
|
||||
updatedReaction = nil
|
||||
}
|
||||
isFirst = false
|
||||
}
|
||||
}
|
||||
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
|
||||
@ -1065,7 +1073,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let itemNode = itemNode, let targetView = itemNode.targetReactionView(value: updatedReaction) {
|
||||
strongSelf.chatDisplayNode.messageTransitionNode.addMessageContextController(messageId: item.message.id, contextController: controller)
|
||||
|
||||
controller.dismissWithReaction(value: updatedReaction, targetView: targetView, hideNode: true, completion: { [weak itemNode, weak targetView] in
|
||||
var hideTargetButton: UIView?
|
||||
if isFirst {
|
||||
hideTargetButton = targetView.superview
|
||||
}
|
||||
|
||||
controller.dismissWithReaction(value: updatedReaction, targetView: targetView, hideNode: true, animateTargetContainer: hideTargetButton, completion: { [weak itemNode, weak targetView] in
|
||||
guard let strongSelf = self, let itemNode = itemNode, let targetView = targetView else {
|
||||
return
|
||||
}
|
||||
@ -1242,6 +1255,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
if let itemNode = itemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) {
|
||||
for reaction in availableReactions.reactions {
|
||||
guard let centerAnimation = reaction.centerAnimation else {
|
||||
continue
|
||||
}
|
||||
guard let aroundAnimation = reaction.aroundAnimation else {
|
||||
continue
|
||||
}
|
||||
|
||||
if reaction.value == updatedReaction {
|
||||
let standaloneReactionAnimation = StandaloneReactionAnimation()
|
||||
|
||||
@ -1256,8 +1276,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
|
||||
appearAnimation: reaction.appearAnimation,
|
||||
stillAnimation: reaction.selectAnimation,
|
||||
listAnimation: reaction.activateAnimation,
|
||||
applicationAnimation: reaction.effectAnimation
|
||||
listAnimation: centerAnimation,
|
||||
applicationAnimation: aroundAnimation
|
||||
),
|
||||
targetView: targetView,
|
||||
hideNode: true,
|
||||
|
@ -192,7 +192,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
|
||||
animation.animator.updatePosition(layer: titleNode.layer, position: titleFrame.center, completion: nil)
|
||||
|
||||
if let buttonView = node.buttonView {
|
||||
animation.animator.updateFrame(layer: buttonView.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)), completion: nil)
|
||||
buttonView.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0))
|
||||
}
|
||||
if let iconNode = node.iconNode {
|
||||
animation.animator.updateFrame(layer: iconNode.layer, frame: CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0), completion: nil)
|
||||
@ -324,10 +324,11 @@ final class ChatMessageActionButtonsNode: ASDisplayNode {
|
||||
let buttonNode = buttonApply(animation)
|
||||
updatedButtons.append(buttonNode)
|
||||
if buttonNode.supernode == nil {
|
||||
node.addSubnode(buttonNode)
|
||||
buttonNode.pressed = node.buttonPressedWrapper
|
||||
buttonNode.longTapped = node.buttonLongTappedWrapper
|
||||
buttonNode.frame = buttonFrame
|
||||
|
||||
node.addSubnode(buttonNode)
|
||||
} else {
|
||||
animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil)
|
||||
}
|
||||
|
@ -807,6 +807,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
return .fail
|
||||
}
|
||||
|
||||
if let actionButtonsNode = strongSelf.actionButtonsNode {
|
||||
if let _ = actionButtonsNode.hitTest(strongSelf.view.convert(point, to: actionButtonsNode.view), with: nil) {
|
||||
return .fail
|
||||
}
|
||||
}
|
||||
|
||||
if let reactionButtonsNode = strongSelf.reactionButtonsNode {
|
||||
if let _ = reactionButtonsNode.hitTest(strongSelf.view.convert(point, to: reactionButtonsNode.view), with: nil) {
|
||||
return .fail
|
||||
@ -1182,6 +1188,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
if isAd {
|
||||
needsShareButton = false
|
||||
}
|
||||
for attribute in item.content.firstMessage.attributes {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
|
||||
needsShareButton = false
|
||||
}
|
||||
}
|
||||
|
||||
var tmpWidth: CGFloat
|
||||
if allowFullWidth {
|
||||
@ -3435,7 +3446,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
|
||||
if !self.backgroundNode.frame.contains(point) {
|
||||
if self.actionButtonsNode == nil || !self.actionButtonsNode!.frame.contains(point) {
|
||||
if let actionButtonsNode = self.actionButtonsNode, let result = actionButtonsNode.hitTest(self.view.convert(point, to: actionButtonsNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import AccountContext
|
||||
import AppBundle
|
||||
import ReactionButtonListComponent
|
||||
import WebPBinding
|
||||
import ReactionImageComponent
|
||||
|
||||
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
|
||||
if let _ = layer.animation(forKey: "clockFrameAnimation") {
|
||||
@ -73,14 +74,14 @@ private final class StatusReactionNode: ASDisplayNode {
|
||||
let defaultImageSize = CGSize(width: 17.0, height: 17.0)
|
||||
let imageSize: CGSize
|
||||
if let file = file {
|
||||
self.iconImageDisposable.set((context.account.postbox.mediaBox.resourceData(file.resource)
|
||||
self.iconImageDisposable.set((reactionStaticImage(context: context, animation: file, pixelSize: CGSize(width: 72.0, height: 72.0))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||
if let image = WebP.convert(fromWebP: dataValue) {
|
||||
if data.isComplete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||
if let image = UIImage(data: dataValue) {
|
||||
strongSelf.iconView.image = image
|
||||
}
|
||||
}
|
||||
@ -667,12 +668,12 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
strongSelf.reactionSelected?(value)
|
||||
},
|
||||
reactions: arguments.reactions.map { reaction in
|
||||
var iconFile: TelegramMediaFile?
|
||||
var centerAnimation: TelegramMediaFile?
|
||||
|
||||
if let availableReactions = arguments.availableReactions {
|
||||
for availableReaction in availableReactions.reactions {
|
||||
if availableReaction.value == reaction.value {
|
||||
iconFile = availableReaction.staticIcon
|
||||
centerAnimation = availableReaction.centerAnimation
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -691,7 +692,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
return ReactionButtonsAsyncLayoutContainer.Reaction(
|
||||
reaction: ReactionButtonComponent.Reaction(
|
||||
value: reaction.value,
|
||||
iconFile: iconFile
|
||||
centerAnimation: centerAnimation
|
||||
),
|
||||
count: Int(reaction.count),
|
||||
peers: peers,
|
||||
@ -1027,18 +1028,18 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||
}
|
||||
validReactions.insert(reaction.value)
|
||||
|
||||
var iconFile: TelegramMediaFile?
|
||||
var centerAnimation: TelegramMediaFile?
|
||||
|
||||
if let availableReactions = arguments.availableReactions {
|
||||
for availableReaction in availableReactions.reactions {
|
||||
if availableReaction.value == reaction.value {
|
||||
iconFile = availableReaction.staticIcon
|
||||
centerAnimation = availableReaction.centerAnimation
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.update(context: arguments.context, type: arguments.type, value: reaction.value, file: iconFile, isSelected: reaction.isSelected, count: Int(reaction.count), theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, animated: false)
|
||||
node.update(context: arguments.context, type: arguments.type, value: reaction.value, file: centerAnimation, isSelected: reaction.isSelected, count: Int(reaction.count), theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper, animated: false)
|
||||
if node.supernode == nil {
|
||||
strongSelf.addSubnode(node)
|
||||
if animation.isAnimated {
|
||||
|
@ -140,12 +140,12 @@ final class MessageReactionButtonsNode: ASDisplayNode {
|
||||
strongSelf.reactionSelected?(value)
|
||||
},
|
||||
reactions: reactions.reactions.map { reaction in
|
||||
var iconFile: TelegramMediaFile?
|
||||
var centerAnimation: TelegramMediaFile?
|
||||
|
||||
if let availableReactions = availableReactions {
|
||||
for availableReaction in availableReactions.reactions {
|
||||
if availableReaction.value == reaction.value {
|
||||
iconFile = availableReaction.staticIcon
|
||||
centerAnimation = availableReaction.centerAnimation
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -170,7 +170,7 @@ final class MessageReactionButtonsNode: ASDisplayNode {
|
||||
return ReactionButtonsAsyncLayoutContainer.Reaction(
|
||||
reaction: ReactionButtonComponent.Reaction(
|
||||
value: reaction.value,
|
||||
iconFile: iconFile
|
||||
centerAnimation: centerAnimation
|
||||
),
|
||||
count: Int(reaction.count),
|
||||
peers: peers,
|
||||
|
@ -181,7 +181,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.textNode.frame = textFrame
|
||||
|
||||
if let statusSizeAndApply = statusSizeAndApply {
|
||||
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0)
|
||||
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.maxX - statusSizeAndApply.0.width, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0)
|
||||
if strongSelf.statusNode.supernode == nil {
|
||||
strongSelf.addSubnode(strongSelf.statusNode)
|
||||
statusSizeAndApply.1(.None)
|
||||
|
@ -9,3 +9,18 @@ CGFloat springAnimationValueAtImpl(CABasicAnimation * _Nonnull animation, CGFloa
|
||||
|
||||
UIBlurEffect * _Nonnull makeCustomZoomBlurEffectImpl(bool isLight);
|
||||
void applySmoothRoundedCornersImpl(CALayer * _Nonnull layer);
|
||||
|
||||
@protocol UIKitPortalViewProtocol <NSObject>
|
||||
|
||||
@property(nonatomic) __weak UIView * _Nullable sourceView;
|
||||
@property(nonatomic) _Bool forwardsClientHitTestingToSourceView;
|
||||
@property(nonatomic) _Bool allowsHitTesting; // @dynamic allowsHitTesting;
|
||||
@property(nonatomic) _Bool allowsBackdropGroups; // @dynamic allowsBackdropGroups;
|
||||
@property(nonatomic) _Bool matchesPosition; // @dynamic matchesPosition;
|
||||
@property(nonatomic) _Bool matchesTransform; // @dynamic matchesTransform;
|
||||
@property(nonatomic) _Bool matchesAlpha; // @dynamic matchesAlpha;
|
||||
@property(nonatomic) _Bool hidesSourceView; // @dynamic hidesSourceView;
|
||||
|
||||
@end
|
||||
|
||||
UIView<UIKitPortalViewProtocol> * _Nullable makePortalView();
|
||||
|
@ -168,3 +168,47 @@ void applySmoothRoundedCornersImpl(CALayer * _Nonnull layer) {
|
||||
setBoolField(layer, [@[@"set", @"Continuous", @"Corners", @":"] componentsJoinedByString:@""], true);
|
||||
}
|
||||
}
|
||||
|
||||
/*@interface _UIPortalView : UIView
|
||||
|
||||
@property(nonatomic, getter=_isGeometryFrozen, setter=_setGeometryFrozen:) _Bool _geometryFrozen; // @synthesize _geometryFrozen=__geometryFrozen;
|
||||
@property(nonatomic) _Bool forwardsClientHitTestingToSourceView; // @synthesize forwardsClientHitTestingToSourceView=_forwardsClientHitTestingToSourceView;
|
||||
@property(copy, nonatomic) NSString * _Nullable name; // @synthesize name=_name;
|
||||
@property(nonatomic) __weak UIView * _Nullable sourceView; // @synthesize sourceView=_sourceView;
|
||||
- (void)setCenter:(struct CGPoint)arg1;
|
||||
- (void)setBounds:(struct CGRect)arg1;
|
||||
- (void)setFrame:(struct CGRect)arg1;
|
||||
- (void)setHidden:(_Bool)arg1;
|
||||
@property(nonatomic) _Bool allowsHitTesting; // @dynamic allowsHitTesting;
|
||||
@property(nonatomic) _Bool allowsBackdropGroups; // @dynamic allowsBackdropGroups;
|
||||
@property(nonatomic) _Bool matchesPosition; // @dynamic matchesPosition;
|
||||
@property(nonatomic) _Bool matchesTransform; // @dynamic matchesTransform;
|
||||
@property(nonatomic) _Bool matchesAlpha; // @dynamic matchesAlpha;
|
||||
@property(nonatomic) _Bool hidesSourceView; // @dynamic hidesSourceView;
|
||||
- (instancetype _Nonnull)initWithFrame:(struct CGRect)arg1;
|
||||
- (instancetype _Nonnull)initWithSourceView:(UIView * _Nullable)arg1;
|
||||
|
||||
@end*/
|
||||
|
||||
UIView<UIKitPortalViewProtocol> * _Nullable makePortalView() {
|
||||
static Class portalViewClass = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
portalViewClass = NSClassFromString([@[@"_", @"UI", @"Portal", @"View"] componentsJoinedByString:@""]);
|
||||
});
|
||||
if (!portalViewClass) {
|
||||
return nil;
|
||||
}
|
||||
UIView<UIKitPortalViewProtocol> *view = [[portalViewClass alloc] init];
|
||||
if (!view) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
view.forwardsClientHitTestingToSourceView = false;
|
||||
view.matchesPosition = true;
|
||||
view.matchesTransform = true;
|
||||
view.matchesAlpha = false;
|
||||
view.allowsHitTesting = false;
|
||||
|
||||
return view;
|
||||
}
|
||||
|
@ -48,17 +48,3 @@ void applyKeyboardAutocorrection(UITextView * _Nonnull textView);
|
||||
@property (nonatomic, copy) UIInterfaceOrientationMask (^ _Nullable supportedOrientations)(void);
|
||||
|
||||
@end
|
||||
|
||||
/*@interface _UIPortalView : UIView
|
||||
|
||||
- (void)setSourceView:(UIView * _Nullable)sourceView;
|
||||
- (bool)hidesSourceView;
|
||||
- (void)setHidesSourceView:(bool)arg1;
|
||||
- (void)setMatchesAlpha:(bool)arg1;
|
||||
- (void)setMatchesPosition:(bool)arg1;
|
||||
- (void)setMatchesTransform:(bool)arg1;
|
||||
- (bool)matchesTransform;
|
||||
- (bool)matchesPosition;
|
||||
- (bool)matchesAlpha;
|
||||
|
||||
@end*/
|
||||
|
@ -105,22 +105,6 @@ static bool notyfyingShiftState = false;
|
||||
[RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentingViewController) newSelector:@selector(_65087dc8_presentingViewController)];
|
||||
[RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentViewController:animated:completion:) newSelector:@selector(_65087dc8_presentViewController:animated:completion:)];
|
||||
[RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(setNeedsStatusBarAppearanceUpdate) newSelector:@selector(_65087dc8_setNeedsStatusBarAppearanceUpdate)];
|
||||
|
||||
/*#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
if (@available(iOS 13, *)) {
|
||||
Class UIUndoGestureInteractionClass = NSClassFromString(@"UIUndoGestureInteraction");
|
||||
SEL addGestureRecognizersSelector = @selector(_addGestureRecognizers);
|
||||
IMP doNothing = imp_implementationWithBlock(^void(__unused id _self) {
|
||||
return;
|
||||
});
|
||||
|
||||
method_setImplementation(class_getInstanceMethod(UIUndoGestureInteractionClass, addGestureRecognizersSelector), doNothing);
|
||||
}
|
||||
#pragma clang diagnostic pop*/
|
||||
|
||||
//[RuntimeUtils swizzleInstanceMethodOfClass:NSClassFromString(@"UIKeyboardImpl") currentSelector:@selector(notifyShiftState) withAnotherClass:[UIKeyboardImpl_65087dc8 class] newSelector:@selector(notifyShiftState)];
|
||||
//[RuntimeUtils swizzleInstanceMethodOfClass:NSClassFromString(@"UIInputWindowController") currentSelector:@selector(updateViewConstraints) withAnotherClass:[UIInputWindowController_65087dc8 class] newSelector:@selector(updateViewConstraints)];
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
||||
break;
|
||||
}
|
||||
|
||||
_animation = rlottie::Animation::loadFromData(std::string(reinterpret_cast<const char *>(data.bytes), data.length), std::string([cacheKey UTF8String]), "", true, {}, modifier);
|
||||
_animation = rlottie::Animation::loadFromData(std::string(reinterpret_cast<const char *>(data.bytes), data.length), std::string([cacheKey UTF8String]), "", false, {}, modifier);
|
||||
if (_animation == nullptr) {
|
||||
return nil;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user