diff --git a/submodules/AccountContext/Sources/LiveLocationManager.swift b/submodules/AccountContext/Sources/LiveLocationManager.swift index 6224e1a410..73d73aa356 100644 --- a/submodules/AccountContext/Sources/LiveLocationManager.swift +++ b/submodules/AccountContext/Sources/LiveLocationManager.swift @@ -10,6 +10,7 @@ public protocol LiveLocationSummaryManager { public protocol LiveLocationManager { var summaryManager: LiveLocationSummaryManager { get } var isPolling: Signal { get } + var hasBackgroundTasks: Signal { get } func cancelLiveLocation(peerId: EnginePeer.Id) func pollOnce() diff --git a/submodules/Components/ReactionButtonListComponent/BUILD b/submodules/Components/ReactionButtonListComponent/BUILD index 0f1f2ccde5..d2f35697e7 100644 --- a/submodules/Components/ReactionButtonListComponent/BUILD +++ b/submodules/Components/ReactionButtonListComponent/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/WebPBinding:WebPBinding", "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", + "//submodules/Components/ReactionImageComponent:ReactionImageComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index ae0b468144..8c5eb9db8f 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -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 - private let measureTextView: ComponentHostView - - private var currentComponent: ReactionButtonComponent? - - private let iconImageDisposable = MetaDisposable() - - init() { - self.iconView = UIImageView() - self.iconView.isUserInteractionEnabled = false - - self.textView = ComponentHostView() - self.textView.isUserInteractionEnabled = false - - self.measureTextView = ComponentHostView() - 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, 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, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) - } } public final class ReactionButtonsAsyncLayoutContainer { diff --git a/submodules/Components/ReactionImageComponent/BUILD b/submodules/Components/ReactionImageComponent/BUILD index 603bc1d792..2791aa47b6 100644 --- a/submodules/Components/ReactionImageComponent/BUILD +++ b/submodules/Components/ReactionImageComponent/BUILD @@ -17,6 +17,8 @@ swift_library( "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/WebPBinding:WebPBinding", + "//submodules/rlottie:RLottieBinding", + "//submodules/GZip:GZip", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift index 8de5bcd575..825c8ef4e3 100644 --- a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift +++ b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift @@ -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 { + 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? diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 6b3864e74c..335ecc550a 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -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?() }) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 7b2aa1f458..95d3ad5f29 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -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 } diff --git a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift index cd27640916..8571766276 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift @@ -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) diff --git a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift index 839f28e184..6336580241 100644 --- a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift +++ b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift @@ -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 { 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() }) diff --git a/submodules/Display/Source/ChildWindowHostView.swift b/submodules/Display/Source/ChildWindowHostView.swift index 0d5e5af144..36dadaac16 100644 --- a/submodules/Display/Source/ChildWindowHostView.swift +++ b/submodules/Display/Source/ChildWindowHostView.swift @@ -57,6 +57,9 @@ private final class ChildWindowHostView: UIView, WindowHost { func presentInGlobalOverlay(_ controller: ContainableController) { } + + func addGlobalPortalHostView(sourceView: PortalSourceView) { + } } public func childWindowHostView(parent: UIView) -> WindowHostView { diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 07668fb552..3cdacae9df 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -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 { diff --git a/submodules/Display/Source/GlobalOverlayPresentationContext.swift b/submodules/Display/Source/GlobalOverlayPresentationContext.swift index 127aa07ddd..041e6d8617 100644 --- a/submodules/Display/Source/GlobalOverlayPresentationContext.swift +++ b/submodules/Display/Source/GlobalOverlayPresentationContext.swift @@ -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? { diff --git a/submodules/Display/Source/GlobalPortalView.swift b/submodules/Display/Source/GlobalPortalView.swift new file mode 100644 index 0000000000..cd6c779542 --- /dev/null +++ b/submodules/Display/Source/GlobalPortalView.swift @@ -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) + } +} diff --git a/submodules/Display/Source/HapticFeedback.swift b/submodules/Display/Source/HapticFeedback.swift index ef1159a88a..84751602b2 100644 --- a/submodules/Display/Source/HapticFeedback.swift +++ b/submodules/Display/Source/HapticFeedback.swift @@ -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) + } +} diff --git a/submodules/Display/Source/NativeWindowHostView.swift b/submodules/Display/Source/NativeWindowHostView.swift index cde42af983..7db299b632 100644 --- a/submodules/Display/Source/NativeWindowHostView.swift +++ b/submodules/Display/Source/NativeWindowHostView.swift @@ -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) } diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 6a655fa138..636286c95b 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -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) } } } diff --git a/submodules/Display/Source/PortalSourceView.swift b/submodules/Display/Source/PortalSourceView.swift new file mode 100644 index 0000000000..337edb299f --- /dev/null +++ b/submodules/Display/Source/PortalSourceView.swift @@ -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) + } + } + } +} diff --git a/submodules/Display/Source/PortalView.swift b/submodules/Display/Source/PortalView.swift new file mode 100644 index 0000000000..aa8c401fed --- /dev/null +++ b/submodules/Display/Source/PortalView.swift @@ -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) + } + } +} diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index f53077c4c8..d5f8844fa3 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -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) diff --git a/submodules/Display/Source/WindowContent.swift b/submodules/Display/Source/WindowContent.swift index 34f98e1b9b..02c9ab65ea 100644 --- a/submodules/Display/Source/WindowContent.swift +++ b/submodules/Display/Source/WindowContent.swift @@ -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) diff --git a/submodules/LiveLocationManager/Sources/LiveLocationManager.swift b/submodules/LiveLocationManager/Sources/LiveLocationManager.swift index b78f60a1cd..352e0c9815 100644 --- a/submodules/LiveLocationManager/Sources/LiveLocationManager.swift +++ b/submodules/LiveLocationManager/Sources/LiveLocationManager.swift @@ -22,6 +22,11 @@ public final class LiveLocationManagerImpl: LiveLocationManager { public var isPolling: Signal { return self.pollingOnce.get() } + + public var hasBackgroundTasks: Signal { + return self.hasActiveMessagesToBroadcast.get() + } + private let pollingOnce = ValuePromise(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? { diff --git a/submodules/ReactionSelectionNode/BUILD b/submodules/ReactionSelectionNode/BUILD index 2e9671167e..393180a383 100644 --- a/submodules/ReactionSelectionNode/BUILD +++ b/submodules/ReactionSelectionNode/BUILD @@ -19,6 +19,7 @@ swift_library( "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/StickerResources:StickerResources", "//submodules/AccountContext:AccountContext", + "//submodules/Components/ReactionButtonListComponent:ReactionButtonListComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift index 515cff7fe1..aa057cd5f6 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -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 diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index b119193336..e046c3b3a1 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -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 } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index b2099cba02..4dd3cafd19 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -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() + } + } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift index 7a1a0c9498..8b13789179 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift @@ -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 - } -} - - -*/ diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 7c0f25465a..32ac140020 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -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", ], diff --git a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift index 4a1f7515a2..f6d33ad0d7 100644 --- a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift +++ b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift @@ -11,6 +11,7 @@ import ItemListUI import PresentationDataUtils import AccountContext import WebPBinding +import ReactionImageComponent private final class QuickReactionSetupControllerArguments { let context: AccountContext diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index ed02df9081..04ec62c84a 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -16,6 +16,7 @@ import ItemListPeerActionItem import UndoUI import ShareController import WebPBinding +import ReactionImageComponent private final class InstalledStickerPacksControllerArguments { let account: Account diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift index bfa5f81ea8..e1b9d76ff0 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift @@ -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() }) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 12f45f1055..97e8b98da8 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -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) } diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index c767bf7ff6..1d4cb88c84 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 494caf4978..1c3435e00a 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/State/AvailableReactions.swift b/submodules/TelegramCore/Sources/State/AvailableReactions.swift index 8ffd638bc8..2aad1ac0c4 100644 --- a/submodules/TelegramCore/Sources/State/AvailableReactions.swift +++ b/submodules/TelegramCore/Sources/State/AvailableReactions.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 1394b7a585..cc3ce23ec2 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -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! { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 6892a89902..df390590b1 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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({ diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index c013b96e56..cd828ece93 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -827,7 +827,13 @@ private func extractAccountManagerState(records: AccountRecordsView mapToSignal { context -> Signal 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 { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 0a0d2100fa..1034186428 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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, diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index de90518046..787d9364fd 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -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() } diff --git a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift index 133d59024f..7d89c921f9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionButtonsNode.swift @@ -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) } diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 1a91c45b44..c1ad11dafc 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -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 } } diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 302fb56e16..d6f8435e8a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift index bcff6c1fc7..eaf4fc3810 100644 --- a/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessagePollBubbleContentNode.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index dca403670c..9fb81ef209 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -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, diff --git a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift index c7e3cb838a..5c0d5dba45 100644 --- a/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageRestrictedBubbleContentNode.swift @@ -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) diff --git a/submodules/TgVoipWebrtc/BUILD b/submodules/TgVoipWebrtc/BUILD index 7a09039abf..f506c64290 100644 --- a/submodules/TgVoipWebrtc/BUILD +++ b/submodules/TgVoipWebrtc/BUILD @@ -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.*", diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 0b2e3082ed..4885177034 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 0b2e3082edb1d485b80f35b9d40a5742c66c7b7b +Subproject commit 48851770349761f89c969e1c9cde34b7fae5cf67 diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h index f335aae7a1..8cf4bc6277 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.h @@ -9,3 +9,18 @@ CGFloat springAnimationValueAtImpl(CABasicAnimation * _Nonnull animation, CGFloa UIBlurEffect * _Nonnull makeCustomZoomBlurEffectImpl(bool isLight); void applySmoothRoundedCornersImpl(CALayer * _Nonnull layer); + +@protocol UIKitPortalViewProtocol + +@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 * _Nullable makePortalView(); diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m index 0b33273d6b..a58c63b7c8 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIKitUtils.m @@ -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 * _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 *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; +} diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h index c592dfc1b2..6ef3dd6b6a 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h @@ -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*/ diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index d5a8a31551..ce7c1a9681 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -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)]; }); } diff --git a/submodules/rlottie/LottieInstance.mm b/submodules/rlottie/LottieInstance.mm index 654979c2df..57c601ee0c 100755 --- a/submodules/rlottie/LottieInstance.mm +++ b/submodules/rlottie/LottieInstance.mm @@ -35,7 +35,7 @@ break; } - _animation = rlottie::Animation::loadFromData(std::string(reinterpret_cast(data.bytes), data.length), std::string([cacheKey UTF8String]), "", true, {}, modifier); + _animation = rlottie::Animation::loadFromData(std::string(reinterpret_cast(data.bytes), data.length), std::string([cacheKey UTF8String]), "", false, {}, modifier); if (_animation == nullptr) { return nil; } diff --git a/versions.json b/versions.json index 320ec2a314..40cbe0153a 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "8.4", + "app": "8.4.1", "bazel": "4.0.0", "xcode": "13.1" }