Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2022-01-05 00:52:14 +03:00
commit 3c1afe4014
53 changed files with 1132 additions and 646 deletions

View File

@ -10,6 +10,7 @@ public protocol LiveLocationSummaryManager {
public protocol LiveLocationManager {
var summaryManager: LiveLocationSummaryManager { get }
var isPolling: Signal<Bool, NoError> { get }
var hasBackgroundTasks: Signal<Bool, NoError> { get }
func cancelLiveLocation(peerId: EnginePeer.Id)
func pollOnce()

View File

@ -19,6 +19,7 @@ swift_library(
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/WebPBinding:WebPBinding",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
],
visibility = [
"//visibility:public",

View File

@ -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 {
@ -31,16 +52,60 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
var counter: Counter?
}
private struct AnimationState {
var fromCounter: Counter
var startTime: Double
var duration: Double
}
private var isExtracted: Bool = false
private var currentLayout: Layout?
private var animationState: AnimationState?
private var animator: ConstantDisplayLinkAnimator?
init() {
super.init(pointerStyle: nil)
}
func update(layout: Layout) {
if self.currentLayout != layout {
if let currentLayout = self.currentLayout, let counter = currentLayout.counter {
self.animationState = AnimationState(fromCounter: counter, startTime: CACurrentMediaTime(), duration: 0.15 * UIView.animationDurationFactor())
}
self.currentLayout = layout
self.updateBackgroundImage(animated: false)
self.updateAnimation()
}
}
private func updateAnimation() {
if let animationState = self.animationState {
let timestamp = CACurrentMediaTime()
if timestamp >= animationState.startTime + animationState.duration {
self.animationState = nil
}
}
if self.animationState != nil {
if self.animator == nil {
let animator = ConstantDisplayLinkAnimator(update: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateBackgroundImage(animated: false)
strongSelf.updateAnimation()
})
self.animator = animator
animator.isPaused = false
}
} else if let animator = self.animator {
animator.invalidate()
self.animator = nil
self.updateBackgroundImage(animated: false)
}
}
@ -87,10 +152,52 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
}
var textOrigin: CGFloat = size.width - counter.frame.width - 8.0 + floorToScreenPixels((counter.frame.width - totalComponentWidth) / 2.0)
for component in counter.components {
let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: foregroundColor)
string.draw(at: component.bounds.origin.offsetBy(dx: textOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0))
textOrigin += component.bounds.width
textOrigin = max(textOrigin, layout.baseSize.height / 2.0 + UIScreenPixel)
var rightTextOrigin = textOrigin + totalComponentWidth
let animationFraction: CGFloat
if let animationState = self.animationState {
animationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration))
} else {
animationFraction = 1.0
}
for i in (0 ..< counter.components.count).reversed() {
let component = counter.components[i]
var componentAlpha: CGFloat = 1.0
var componentVerticalOffset: CGFloat = 0.0
if let animationState = self.animationState {
let reverseIndex = counter.components.count - 1 - i
if reverseIndex < animationState.fromCounter.components.count {
let previousComponent = animationState.fromCounter.components[animationState.fromCounter.components.count - 1 - reverseIndex]
if previousComponent != component {
componentAlpha = animationFraction
componentVerticalOffset = (1.0 - animationFraction) * 8.0
if previousComponent.string < component.string {
componentVerticalOffset = -componentVerticalOffset
}
let previousComponentAlpha = 1.0 - componentAlpha
var previousComponentVerticalOffset = -animationFraction * 8.0
if previousComponent.string < component.string {
previousComponentVerticalOffset = -previousComponentVerticalOffset
}
let componentOrigin = rightTextOrigin - previousComponent.bounds.width
let string = NSAttributedString(string: previousComponent.string, font: Font.medium(11.0), textColor: foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - previousComponentAlpha))
string.draw(at: previousComponent.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - previousComponent.bounds.height) / 2.0 + previousComponentVerticalOffset))
}
}
}
let componentOrigin = rightTextOrigin - component.bounds.width
let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - componentAlpha))
string.draw(at: component.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0 + componentVerticalOffset))
rightTextOrigin -= component.bounds.width
}
}
@ -120,7 +227,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
private static let maxDigitWidth: CGFloat = {
var maxWidth: CGFloat = 0.0
for i in 0 ..< 9 {
for i in 0 ... 9 {
let string = NSAttributedString(string: "\(i)", font: Font.medium(11.0), textColor: .black)
let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
maxWidth = max(maxWidth, boundingRect.width)
@ -151,13 +258,19 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
} else {
var resultSize = CGSize()
var resultComponents: [Component] = []
for component in spec.stringComponents {
for i in 0 ..< spec.stringComponents.count {
let component = spec.stringComponents[i]
let string = NSAttributedString(string: component, font: Font.medium(11.0), textColor: .black)
let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
resultComponents.append(Component(string: component, bounds: boundingRect))
resultSize.width += CounterLayout.maxDigitWidth
if spec.stringComponents.count <= 2 {
resultSize.width += CounterLayout.maxDigitWidth
} else {
resultSize.width += boundingRect.width
}
resultSize.height = max(resultSize.height, boundingRect.height)
}
size = CGSize(width: ceil(resultSize.width), height: ceil(resultSize.height))
@ -180,7 +293,6 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
let spec: Spec
let backgroundColor: UInt32
let clippingHeight: CGFloat
let sideInsets: CGFloat
let imageFrame: CGRect
@ -189,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
@ -243,75 +346,6 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
let imageFrame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize)
/*var previousDisplayCounter: String?
if let currentLayout = currentLayout {
if currentLayout.spec.component.avatarPeers.isEmpty {
previousDisplayCounter = countString(Int64(spec.component.count))
}
}
var currentDisplayCounter: String?
if spec.component.avatarPeers.isEmpty {
currentDisplayCounter = countString(Int64(spec.component.count))
}*/
/*let backgroundImage: UIImage
let extractedBackgroundImage: UIImage
if let currentLayout = currentLayout, currentLayout.spec.component.isSelected == spec.component.isSelected, currentLayout.spec.component.colors == spec.component.colors, previousDisplayCounter == currentDisplayCounter {
backgroundImage = currentLayout.backgroundImage
extractedBackgroundImage = currentLayout.extractedBackgroundImage
} else {
backgroundImage = generateImage(CGSize(width: height + 18.0, height: height), rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor(argb: backgroundColor).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: height, height: height)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - height, y: 0.0), size: CGSize(width: height, height: size.height)))
context.fill(CGRect(origin: CGPoint(x: height / 2.0, y: 0.0), size: CGSize(width: size.width - height, height: size.height)))
context.setBlendMode(.normal)
if let currentDisplayCounter = currentDisplayCounter {
let textColor = UIColor(argb: spec.component.isSelected ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground)
let string = NSAttributedString(string: currentDisplayCounter, font: Font.medium(11.0), textColor: textColor)
let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
if textColor.alpha < 1.0 {
context.setBlendMode(.copy)
}
string.draw(at: CGPoint(x: size.width - sideInsets - boundingRect.width, y: (size.height - boundingRect.height) / 2.0))
}
UIGraphicsPopContext()
})!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0))
extractedBackgroundImage = generateImage(CGSize(width: height + 18.0, height: height), rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor(argb: spec.component.colors.extractedBackground).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: height, height: height)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - height, y: 0.0), size: CGSize(width: height, height: size.height)))
context.fill(CGRect(origin: CGPoint(x: height / 2.0, y: 0.0), size: CGSize(width: size.width - height, height: size.height)))
context.setBlendMode(.normal)
if let currentDisplayCounter = currentDisplayCounter {
let textColor = UIColor(argb: spec.component.colors.extractedForeground)
let string = NSAttributedString(string: currentDisplayCounter, font: Font.medium(11.0), textColor: textColor)
let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
if textColor.alpha < 1.0 {
context.setBlendMode(.copy)
}
string.draw(at: CGPoint(x: size.width - sideInsets - boundingRect.width, y: (size.height - boundingRect.height) / 2.0))
}
UIGraphicsPopContext()
})!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0))
}*/
var counterLayout: CounterLayout?
var counterFrame: CGRect?
@ -363,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
)
}
@ -380,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()
@ -389,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()
@ -457,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
}
}
}))
@ -541,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
@ -642,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 {

View File

@ -17,6 +17,8 @@ swift_library(
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/WebPBinding:WebPBinding",
"//submodules/rlottie:RLottieBinding",
"//submodules/GZip:GZip",
],
visibility = [
"//visibility:public",

View File

@ -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?

View File

@ -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?()
})

View File

@ -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
}

View File

@ -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)

View File

@ -3,7 +3,8 @@ import CoreLocation
import SwiftSignalKit
public enum DeviceLocationMode: Int32 {
case precise = 0
case preciseForeground = 0
case preciseAlways = 1
}
private final class DeviceLocationSubscriber {
@ -51,15 +52,15 @@ public final class DeviceLocationManager: NSObject {
super.init()
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
self.manager.allowsBackgroundLocationUpdates = true
}
self.manager.delegate = self
self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
self.manager.distanceFilter = 5.0
self.manager.activityType = .other
self.manager.pausesLocationUpdatesAutomatically = false
self.manager.headingFilter = 2.0
if #available(iOS 11.0, *) {
self.manager.showsBackgroundLocationIndicator = true
}
}
public func push(mode: DeviceLocationMode, updated: @escaping (CLLocation, Double?) -> Void) -> Disposable {
@ -108,6 +109,14 @@ public final class DeviceLocationManager: NSObject {
self.requestedAuthorization = true
self.manager.requestAlwaysAuthorization()
}
switch topMode {
case .preciseForeground:
self.manager.allowsBackgroundLocationUpdates = false
case .preciseAlways:
self.manager.allowsBackgroundLocationUpdates = true
}
self.manager.startUpdatingLocation()
self.manager.startUpdatingHeading()
}
@ -164,7 +173,7 @@ extension DeviceLocationManager: CLLocationManagerDelegate {
public func currentLocationManagerCoordinate(manager: DeviceLocationManager, timeout timeoutValue: Double) -> Signal<CLLocationCoordinate2D?, NoError> {
return (
Signal { subscriber in
let disposable = manager.push(mode: .precise, updated: { location, _ in
let disposable = manager.push(mode: .preciseForeground, updated: { location, _ in
subscriber.putNext(location.coordinate)
subscriber.putCompletion()
})

View File

@ -57,6 +57,9 @@ private final class ChildWindowHostView: UIView, WindowHost {
func presentInGlobalOverlay(_ controller: ContainableController) {
}
func addGlobalPortalHostView(sourceView: PortalSourceView) {
}
}
public func childWindowHostView(parent: UIView) -> WindowHostView {

View File

@ -626,7 +626,7 @@ public extension ContainedViewLayoutTransition {
}
}
func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
func updateFrame(layer: CALayer, frame: CGRect, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
if layer.frame.equalTo(frame) {
completion?(true)
} else {
@ -637,7 +637,12 @@ public extension ContainedViewLayoutTransition {
completion(true)
}
case let .animated(duration, curve):
let previousFrame = layer.frame
let previousFrame: CGRect
if beginWithCurrentState, let presentation = layer.presentation() {
previousFrame = presentation.frame
} else {
previousFrame = layer.frame
}
layer.frame = frame
layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
if let completion = completion {

View File

@ -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? {

View 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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -235,12 +235,12 @@ public final class NavigationBackgroundNode: ASDisplayNode {
self.validLayout = (size, cornerRadius)
let contentFrame = CGRect(origin: CGPoint(), size: size)
transition.updateFrame(node: self.backgroundNode, frame: contentFrame)
transition.updateFrame(node: self.backgroundNode, frame: contentFrame, beginWithCurrentState: true)
if let effectView = self.effectView, effectView.frame != contentFrame {
transition.updateFrame(layer: effectView.layer, frame: contentFrame)
transition.updateFrame(layer: effectView.layer, frame: contentFrame, beginWithCurrentState: true)
if let sublayers = effectView.layer.sublayers {
for sublayer in sublayers {
transition.updateFrame(layer: sublayer, frame: contentFrame)
transition.updateFrame(layer: sublayer, frame: contentFrame, beginWithCurrentState: true)
}
}
}

View 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)
}
}
}
}

View 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)
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -22,6 +22,11 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
public var isPolling: Signal<Bool, NoError> {
return self.pollingOnce.get()
}
public var hasBackgroundTasks: Signal<Bool, NoError> {
return self.hasActiveMessagesToBroadcast.get()
}
private let pollingOnce = ValuePromise<Bool>(false, ignoreRepeated: true)
private var pollingOnceValue = false {
didSet {
@ -92,6 +97,8 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
|> map { inForeground, hasActiveMessagesToBroadcast, pollingOnce -> Bool in
if (inForeground || pollingOnce) && hasActiveMessagesToBroadcast {
return true
} else if hasActiveMessagesToBroadcast {
return true
} else {
return false
}
@ -100,7 +107,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
|> deliverOn(self.queue)).start(next: { [weak self] value in
if let strongSelf = self {
if value {
strongSelf.deviceLocationDisposable.set(strongSelf.locationManager.push(mode: .precise, updated: { [weak self] location, heading in
strongSelf.deviceLocationDisposable.set(strongSelf.locationManager.push(mode: .preciseAlways, updated: { [weak self] location, heading in
self?.deviceLocationPromise.set(.single((location, heading)))
}))
} else {
@ -249,9 +256,9 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
}
public func pollOnce() {
if !self.broadcastToMessageIds.isEmpty {
/*if !self.broadcastToMessageIds.isEmpty {
self.pollingOnceValue = true
}
}*/
}
public func internalMessageForPeerId(_ peerId: EnginePeer.Id) -> EngineMessage.Id? {

View File

@ -19,6 +19,7 @@ swift_library(
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/StickerResources:StickerResources",
"//submodules/AccountContext:AccountContext",
"//submodules/Components/ReactionButtonListComponent:ReactionButtonListComponent",
],
visibility = [
"//visibility:public",

View File

@ -150,32 +150,30 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
let contentBounds = backgroundFrame.insetBy(dx: -10.0, dy: -10.0).union(largeCircleFrame).union(smallCircleFrame)
transition.updateFrame(layer: self.backgroundLayer, frame: backgroundFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY))
transition.updateFrame(layer: self.largeCircleLayer, frame: largeCircleFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY))
transition.updateFrame(layer: self.smallCircleLayer, frame: smallCircleFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY))
transition.updateFrame(layer: self.backgroundLayer, frame: backgroundFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY), beginWithCurrentState: true)
transition.updateFrame(layer: self.largeCircleLayer, frame: largeCircleFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY), beginWithCurrentState: true)
transition.updateFrame(layer: self.smallCircleLayer, frame: smallCircleFrame.offsetBy(dx: -contentBounds.minX, dy: -contentBounds.minY), beginWithCurrentState: true)
transition.updateFrame(layer: self.backgroundShadowLayer, frame: backgroundFrame.insetBy(dx: -shadowInset, dy: -shadowInset))
transition.updateFrame(layer: self.largeCircleShadowLayer, frame: largeCircleFrame.insetBy(dx: -shadowInset, dy: -shadowInset))
transition.updateFrame(layer: self.smallCircleShadowLayer, frame: smallCircleFrame.insetBy(dx: -shadowInset, dy: -shadowInset))
transition.updateFrame(layer: self.backgroundShadowLayer, frame: backgroundFrame.insetBy(dx: -shadowInset, dy: -shadowInset), beginWithCurrentState: true)
transition.updateFrame(layer: self.largeCircleShadowLayer, frame: largeCircleFrame.insetBy(dx: -shadowInset, dy: -shadowInset), beginWithCurrentState: true)
transition.updateFrame(layer: self.smallCircleShadowLayer, frame: smallCircleFrame.insetBy(dx: -shadowInset, dy: -shadowInset), beginWithCurrentState: true)
transition.updateFrame(node: self.backgroundNode, frame: contentBounds)
transition.updateFrame(node: self.backgroundNode, frame: contentBounds, beginWithCurrentState: true)
self.backgroundNode.update(size: contentBounds.size, transition: transition)
}
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.animate(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: smallCircleDuration)
self.smallCircleLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: smallCircleDuration, delay: 0.0)
self.largeCircleLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.01, delay: largeCircleDelay)
self.largeCircleLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: largeCircleDuration, delay: largeCircleDelay)
self.largeCircleShadowLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: largeCircleDuration, delay: largeCircleDelay)
//self.largeCircleLayer.animate(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: largeCircleDuration)
self.backgroundLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.01, delay: mainCircleDelay)
self.backgroundLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay)
@ -183,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

View File

@ -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
@ -162,7 +171,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
rect.origin.x = max(sideInset, rect.origin.x)
rect.origin.y = max(insets.top + sideInset, rect.origin.y)
rect.origin.x = min(containerSize.width - contentSize.width - sideInset, rect.origin.x)
if rect.maxX > containerSize.width - sideInset {
let rightEdge = containerSize.width - sideInset
if rect.maxX > rightEdge {
rect.origin.x = containerSize.width - sideInset - rect.width
}
if rect.minX < sideInset {
@ -176,11 +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
visualRect.origin.x -= 4.0
visualRect.size.width += 8.0
}
return (rect, isLeftAligned, cloudSourcePoint)
return (rect, visualRect, isLeftAligned, cloudSourcePoint)
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
@ -262,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)
}
}
}
@ -295,9 +309,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if visibleContentWidth > size.width - sideInset * 2.0 {
visibleContentWidth = size.width - sideInset * 2.0
}
if self.highlightedReaction != nil {
visibleContentWidth += 4.0
}
let contentHeight = verticalInset * 2.0 + rowHeight
@ -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)
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
}
}

View File

@ -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,65 +183,69 @@ 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 {
self.validSize = size
}
if isPreviewing {
if self.stillAnimationNode == nil {
let stillAnimationNode = AnimatedStickerNode()
self.stillAnimationNode = stillAnimationNode
self.addSubnode(stillAnimationNode)
if self.animationNode == nil {
if isPreviewing {
if self.stillAnimationNode == nil {
let stillAnimationNode = AnimatedStickerNode()
self.stillAnimationNode = stillAnimationNode
self.addSubnode(stillAnimationNode)
stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id)))
stillAnimationNode.position = animationFrame.center
stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
stillAnimationNode.updateLayout(size: animationFrame.size)
stillAnimationNode.started = { [weak self, weak stillAnimationNode] in
guard let strongSelf = self, let stillAnimationNode = stillAnimationNode, strongSelf.stillAnimationNode === stillAnimationNode, strongSelf.animationNode == nil else {
return
}
strongSelf.staticAnimationNode.alpha = 0.0
if let animateInAnimationNode = strongSelf.animateInAnimationNode, !animateInAnimationNode.alpha.isZero {
animateInAnimationNode.alpha = 0.0
animateInAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1)
strongSelf.staticAnimationNode.isHidden = false
}
}
stillAnimationNode.visibility = true
transition.animateTransformScale(node: stillAnimationNode, from: self.staticAnimationNode.bounds.width / animationFrame.width)
transition.animatePositionAdditive(node: stillAnimationNode, offset: CGPoint(x: self.staticAnimationNode.frame.midX - animationFrame.midX, y: self.staticAnimationNode.frame.midY - animationFrame.midY))
} else {
if let stillAnimationNode = self.stillAnimationNode {
transition.updatePosition(node: stillAnimationNode, position: animationFrame.center, beginWithCurrentState: true)
transition.updateTransformScale(node: stillAnimationNode, scale: animationFrame.size.width / stillAnimationNode.bounds.width, beginWithCurrentState: true)
}
}
} else if let stillAnimationNode = self.stillAnimationNode {
self.stillAnimationNode = nil
self.dismissedStillAnimationNodes.append(stillAnimationNode)
stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id)))
stillAnimationNode.position = animationFrame.center
stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
stillAnimationNode.updateLayout(size: animationFrame.size)
stillAnimationNode.started = { [weak self, weak stillAnimationNode] in
guard let strongSelf = self, let stillAnimationNode = stillAnimationNode, strongSelf.stillAnimationNode === stillAnimationNode else {
transition.updatePosition(node: stillAnimationNode, position: animationFrame.center, beginWithCurrentState: true)
transition.updateTransformScale(node: stillAnimationNode, scale: animationFrame.size.width / stillAnimationNode.bounds.width, beginWithCurrentState: true)
stillAnimationNode.alpha = 0.0
stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self, weak stillAnimationNode] _ in
guard let strongSelf = self, let stillAnimationNode = stillAnimationNode else {
return
}
strongSelf.staticAnimationNode.alpha = 0.0
if let animateInAnimationNode = strongSelf.animateInAnimationNode, !animateInAnimationNode.alpha.isZero {
animateInAnimationNode.alpha = 0.0
animateInAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1)
strongSelf.staticAnimationNode.isHidden = false
}
}
stillAnimationNode.visibility = true
stillAnimationNode.removeFromSupernode()
strongSelf.dismissedStillAnimationNodes.removeAll(where: { $0 === stillAnimationNode })
})
transition.animateTransformScale(node: stillAnimationNode, from: self.staticAnimationNode.bounds.width / animationFrame.width)
transition.animatePositionAdditive(node: stillAnimationNode, offset: CGPoint(x: self.staticAnimationNode.frame.midX - animationFrame.midX, y: self.staticAnimationNode.frame.midY - animationFrame.midY))
} else {
if let stillAnimationNode = self.stillAnimationNode {
transition.updatePosition(node: stillAnimationNode, position: animationFrame.center, beginWithCurrentState: true)
transition.updateTransformScale(node: stillAnimationNode, scale: animationFrame.size.width / stillAnimationNode.bounds.width, beginWithCurrentState: true)
}
let previousAlpha = CGFloat(self.staticAnimationNode.layer.presentation()?.opacity ?? self.staticAnimationNode.layer.opacity)
self.staticAnimationNode.alpha = 1.0
self.staticAnimationNode.layer.animateAlpha(from: previousAlpha, to: 1.0, duration: 0.08)
}
} else if let stillAnimationNode = self.stillAnimationNode {
self.stillAnimationNode = nil
self.dismissedStillAnimationNodes.append(stillAnimationNode)
transition.updatePosition(node: stillAnimationNode, position: animationFrame.center, beginWithCurrentState: true)
transition.updateTransformScale(node: stillAnimationNode, scale: animationFrame.size.width / stillAnimationNode.bounds.width, beginWithCurrentState: true)
stillAnimationNode.alpha = 0.0
stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self, weak stillAnimationNode] _ in
guard let strongSelf = self, let stillAnimationNode = stillAnimationNode else {
return
}
stillAnimationNode.removeFromSupernode()
strongSelf.dismissedStillAnimationNodes.removeAll(where: { $0 === stillAnimationNode })
})
let previousAlpha = CGFloat(self.staticAnimationNode.layer.presentation()?.opacity ?? self.staticAnimationNode.layer.opacity)
self.staticAnimationNode.alpha = 1.0
self.staticAnimationNode.layer.animateAlpha(from: previousAlpha, to: 1.0, duration: 0.08)
}
if !self.didSetupStillAnimation {
@ -260,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()
}
}
}

View File

@ -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
}
}
*/

View File

@ -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",
],

View File

@ -11,6 +11,7 @@ import ItemListUI
import PresentationDataUtils
import AccountContext
import WebPBinding
import ReactionImageComponent
private final class QuickReactionSetupControllerArguments {
let context: AccountContext

View File

@ -16,6 +16,7 @@ import ItemListPeerActionItem
import UndoUI
import ShareController
import WebPBinding
import ReactionImageComponent
private final class InstalledStickerPacksControllerArguments {
let account: Account

View File

@ -384,7 +384,7 @@ public func themeAutoNightSettingsController(context: AccountContext) -> ViewCon
let forceUpdateLocation: () -> Void = {
let locationCoordinates = Signal<(Double, Double), NoError> { subscriber in
return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.precise, updated: { location, _ in
return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.preciseForeground, updated: { location, _ in
subscriber.putNext((location.coordinate.latitude, location.coordinate.longitude))
subscriber.putCompletion()
})

View File

@ -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) }

View File

@ -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

View File

@ -866,7 +866,7 @@ public final class AccountViewTracker {
added = true
updatedReactions = attribute.withUpdatedResults(reactions)
if updatedReactions.reactions == attribute.reactions {
if updatedReactions == attribute {
return .skip
}
attributes[j] = updatedReactions

View File

@ -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 {

View File

@ -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! {

View File

@ -251,6 +251,7 @@ swift_library(
"//submodules/InvisibleInkDustNode:InvisibleInkDustNode",
"//submodules/QrCodeUI:QrCodeUI",
"//submodules/Components/ReactionListContextMenuContent:ReactionListContextMenuContent",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/Translate:Translate",
"//submodules/TabBarUI:TabBarUI",
] + select({

View File

@ -827,7 +827,13 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|> mapToSignal { context -> Signal<AccountRecordId?, NoError> in
if let context = context, let liveLocationManager = context.context.liveLocationManager {
let accountId = context.context.account.id
return liveLocationManager.isPolling
return combineLatest(queue: .mainQueue(),
liveLocationManager.isPolling,
liveLocationManager.hasBackgroundTasks
)
|> map { isPolling, hasBackgroundTasks -> Bool in
return isPolling || hasBackgroundTasks
}
|> distinctUntilChanged
|> map { value -> AccountRecordId? in
if value {

View File

@ -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,

View File

@ -18,6 +18,7 @@ import AccountContext
import ChatInterfaceState
import ChatListUI
import ComponentFlow
import ReactionSelectionNode
extension ChatReplyThreadMessage {
var effectiveTopId: MessageId {
@ -1784,6 +1785,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let historyView = transactionState.historyView
var isTopReplyThreadMessageShownValue = false
var topVisibleMessageRange: ChatTopVisibleMessageRange?
if let visible = displayedRange.visibleRange {
let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)
if indexRange.0 > indexRange.1 {
@ -1797,7 +1799,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let toLaterRange = (historyView.filteredEntries.count - 1 - (visible.firstIndex - 1), historyView.filteredEntries.count - 1)
var messageIdsWithViewCount: [MessageId] = []
var messageIdsWithPossibleReactions: [MessageId] = []
var messageIdsWithLiveLocation: [MessageId] = []
var messageIdsWithUnsupportedMedia: [MessageId] = []
var messageIdsWithRefreshMedia: [MessageId] = []
@ -1833,11 +1834,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
contentRequiredValidation = true
}
}
var hasAction = false
for media in message.media {
if let _ = media as? TelegramMediaAction {
hasAction = true
} else if let _ = media as? TelegramMediaUnsupported {
if let _ = media as? TelegramMediaUnsupported {
contentRequiredValidation = true
} else if message.flags.contains(.Incoming), let media = media as? TelegramMediaMap, let liveBroadcastingTimeout = media.liveBroadcastingTimeout {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
@ -1864,14 +1862,6 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
}
}
if !hasAction {
switch message.id.peerId.namespace {
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
messageIdsWithPossibleReactions.append(message.id)
default:
break
}
}
if contentRequiredValidation {
messageIdsWithUnsupportedMedia.append(message.id)
}
@ -1931,6 +1921,46 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
}
var messageIdsWithPossibleReactions: [MessageId] = []
for entry in historyView.filteredEntries {
switch entry {
case let .MessageEntry(message, _, _, _, _, _):
var hasAction = false
for media in message.media {
if let _ = media as? TelegramMediaAction {
hasAction = true
}
}
if !hasAction {
switch message.id.peerId.namespace {
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
messageIdsWithPossibleReactions.append(message.id)
default:
break
}
}
case let .MessageGroupEntry(_, messages, _):
for (message, _, _, _, _) in messages {
var hasAction = false
for media in message.media {
if let _ = media as? TelegramMediaAction {
hasAction = true
}
}
if !hasAction {
switch message.id.peerId.namespace {
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
messageIdsWithPossibleReactions.append(message.id)
default:
break
}
}
}
default:
break
}
}
func addMediaToPrefetch(_ message: Message, _ media: Media, _ messages: inout [(Message, Media)]) -> Bool {
if media is TelegramMediaImage || media is TelegramMediaFile {
messages.append((message, media))
@ -2316,6 +2346,77 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let completion: (Bool, ListViewDisplayedItemRange) -> Void = { [weak self] wasTransformed, visibleRange in
if let strongSelf = self {
var newIncomingReactions: [MessageId: String] = [:]
if case let .peer(peerId) = strongSelf.chatLocation, peerId.namespace == Namespaces.Peer.CloudUser, let previousHistoryView = strongSelf.historyView {
var updatedIncomingReactions: [MessageId: String] = [:]
for entry in transition.historyView.filteredEntries {
switch entry {
case let .MessageEntry(message, _, _, _, _, _):
if message.flags.contains(.Incoming) {
continue
}
if let reactions = message.reactionsAttribute {
for reaction in reactions.reactions {
if !reaction.isSelected {
updatedIncomingReactions[message.id] = reaction.value
}
}
}
case let .MessageGroupEntry(_, messages, _):
for message in messages {
if message.0.flags.contains(.Incoming) {
continue
}
if let reactions = message.0.reactionsAttribute {
for reaction in reactions.reactions {
if !reaction.isSelected {
updatedIncomingReactions[message.0.id] = reaction.value
}
}
}
}
default:
break
}
}
for entry in previousHistoryView.filteredEntries {
switch entry {
case let .MessageEntry(message, _, _, _, _, _):
if let updatedReaction = updatedIncomingReactions[message.id] {
var previousReaction: String?
if let reactions = message.reactionsAttribute {
for reaction in reactions.reactions {
if !reaction.isSelected {
previousReaction = reaction.value
}
}
}
if previousReaction != updatedReaction {
newIncomingReactions[message.id] = updatedReaction
}
}
case let .MessageGroupEntry(_, messages, _):
for message in messages {
if let updatedReaction = updatedIncomingReactions[message.0.id] {
var previousReaction: String?
if let reactions = message.0.reactionsAttribute {
for reaction in reactions.reactions {
if !reaction.isSelected {
previousReaction = reaction.value
}
}
}
if previousReaction != updatedReaction {
newIncomingReactions[message.0.id] = updatedReaction
}
}
}
default:
break
}
}
}
strongSelf.historyView = transition.historyView
let loadState: ChatHistoryNodeLoadState
@ -2360,7 +2461,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if let visible = visibleRange.visibleRange {
let visibleFirstIndex = visible.firstIndex
if visibleFirstIndex <= visible.lastIndex {
let (incomingIndex, overallIndex) = maxMessageIndexForEntries(transition.historyView, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visibleFirstIndex))
let (incomingIndex, overallIndex) = maxMessageIndexForEntries(transition.historyView, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visibleFirstIndex))
let messageIndex: MessageIndex?
switch strongSelf.chatLocation {
@ -2463,6 +2564,39 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
}
if !newIncomingReactions.isEmpty, let chatDisplayNode = strongSelf.controllerInteraction.chatControllerNode() as? ChatControllerNode {
strongSelf.forEachVisibleItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, let updatedReaction = newIncomingReactions[item.content.firstMessage.id], let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: updatedReaction) {
for reaction in availableReactions.reactions {
if reaction.value == updatedReaction {
let standaloneReactionAnimation = StandaloneReactionAnimation()
chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
chatDisplayNode.addSubnode(standaloneReactionAnimation)
standaloneReactionAnimation.frame = chatDisplayNode.bounds
standaloneReactionAnimation.animateReactionSelection(
context: strongSelf.context,
theme: item.presentationData.theme.theme,
reaction: ReactionContextItem(
reaction: ReactionContextItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation,
listAnimation: reaction.activateAnimation,
applicationAnimation: reaction.effectAnimation
),
targetView: targetView,
hideNode: true,
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.removeFromSupernode()
}
)
}
}
}
}
}
strongSelf.hasActiveTransition = false
strongSelf.dequeueHistoryViewTransitions()
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -1072,7 +1072,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
impressionCount: viewCount,
dateText: dateText,
type: statusType,
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
constrainedSize: textConstrainedSize,
availableReactions: item.associatedData.availableReactions,
reactions: dateReactionsAndPeers.reactions,
@ -1084,8 +1084,6 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
))
}
let _ = statusSuggestedWidthAndContinue
var poll: TelegramMediaPoll?
for media in item.message.media {
if let media = media as? TelegramMediaPoll {
@ -1167,14 +1165,8 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size)
var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom))
/*var statusFrame: CGRect?
if let statusSize = statusSize {
statusFrame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.maxX - statusSize.width, y: textFrameWithoutInsets.maxY - statusSize.height), size: statusSize)
}*/
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
//statusFrame = statusFrame?.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
var boundingSize: CGSize = textFrameWithoutInsets.size
boundingSize.width += additionalTextRightInset
@ -1183,6 +1175,10 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
boundingSize.width = max(boundingSize.width, buttonSubmitInactiveTextLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/)
boundingSize.width = max(boundingSize.width, buttonViewResultsTextLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/)
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0)
}
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
@ -1299,20 +1295,19 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
resultSize.height += 26.0
}
var statusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> Void)?
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth)
}
if let statusSizeAndApply = statusSizeAndApply {
resultSize.height += statusSizeAndApply.0.height - 6.0
}
let buttonSubmitInactiveTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitInactiveTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonSubmitInactiveTextLayout.size)
let buttonSubmitActiveTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitActiveTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonSubmitActiveTextLayout.size)
let buttonViewResultsTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonViewResultsTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonViewResultsTextLayout.size)
/*var adjustedStatusFrame: CGRect?
if let statusFrame = statusFrame {
var localStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - layoutConstants.text.bubbleInsets.right, y: resultSize.height - statusFrame.size.height - 6.0), size: statusFrame.size)
if localStatusFrame.minX <= buttonViewResultsTextFrame.maxX || localStatusFrame.minX <= buttonSubmitActiveTextFrame.maxX {
localStatusFrame.origin.y += 10.0
resultSize.height += 10.0
}
adjustedStatusFrame = localStatusFrame
}*/
return (resultSize, { [weak self] animation, synchronousLoad in
if let strongSelf = self {
strongSelf.item = item
@ -1382,28 +1377,6 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
strongSelf.optionNodes = updatedOptionNodes
/*if let statusApply = statusApply, let adjustedStatusFrame = adjustedStatusFrame {
let previousStatusFrame = strongSelf.statusNode.frame
strongSelf.statusNode.frame = adjustedStatusFrame
var hasAnimation = true
if case .None = animation {
hasAnimation = false
}
statusApply(hasAnimation)
if strongSelf.statusNode.supernode == nil {
strongSelf.addSubnode(strongSelf.statusNode)
} else {
if case let .System(duration) = animation {
let delta = CGPoint(x: previousStatusFrame.maxX - adjustedStatusFrame.maxX, y: previousStatusFrame.minY - adjustedStatusFrame.minY)
let statusPosition = strongSelf.statusNode.layer.position
let previousPosition = CGPoint(x: statusPosition.x + delta.x, y: statusPosition.y + delta.y)
strongSelf.statusNode.layer.animatePosition(from: previousPosition, to: statusPosition, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
}
}
} else */if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode()
}
if textLayout.hasRTL {
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size)
} else {
@ -1535,13 +1508,29 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
}
let _ = votersApply()
strongSelf.votersNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size)
let votersFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size)
strongSelf.votersNode.frame = votersFrame
if animation.isAnimated, let previousPoll = previousPoll, let poll = poll {
if previousPoll.results.totalVoters == nil && poll.results.totalVoters != nil {
strongSelf.votersNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
if let statusSizeAndApply = statusSizeAndApply {
let statusFrame = CGRect(origin: CGPoint(x: resultSize.width - statusSizeAndApply.0.width - layoutConstants.text.bubbleInsets.right, y: votersFrame.maxY), size: statusSizeAndApply.0)
if strongSelf.statusNode.supernode == nil {
statusSizeAndApply.1(.None)
strongSelf.statusNode.frame = statusFrame
strongSelf.addSubnode(strongSelf.statusNode)
} else {
statusSizeAndApply.1(animation)
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil)
}
} else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode()
}
let _ = buttonSubmitInactiveTextApply()
strongSelf.buttonSubmitInactiveTextNode.frame = buttonSubmitInactiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset)

View File

@ -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,

View File

@ -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)

View File

@ -18,6 +18,7 @@ objc_library(
"tgcalls/tgcalls/platform/android/**",
"tgcalls/tgcalls/platform/windows/**",
"tgcalls/tgcalls/platform/uwp/**",
"tgcalls/tgcalls/platform/darwin/SQueue*",
"tgcalls/tgcalls/platform/darwin/macOS/**",
"tgcalls/tgcalls/platform/darwin/VideoCameraCapturerMac.*",
"tgcalls/tgcalls/platform/darwin/VideoMetalViewMac.*",

@ -1 +1 @@
Subproject commit 0b2e3082edb1d485b80f35b9d40a5742c66c7b7b
Subproject commit 48851770349761f89c969e1c9cde34b7fae5cf67

View File

@ -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();

View File

@ -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;
}

View File

@ -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*/

View File

@ -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)];
});
}

View File

@ -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;
}

View File

@ -1,5 +1,5 @@
{
"app": "8.4",
"app": "8.4.1",
"bazel": "4.0.0",
"xcode": "13.1"
}