diff --git a/submodules/CodeInputView/Sources/CodeInputView.swift b/submodules/CodeInputView/Sources/CodeInputView.swift index 6228e08a7f..ba82a935af 100644 --- a/submodules/CodeInputView/Sources/CodeInputView.swift +++ b/submodules/CodeInputView/Sources/CodeInputView.swift @@ -25,7 +25,7 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { } private final class ItemView: ASDisplayNode { - private let backgroundView: UIImageView + private let backgroundView: UIView private let textNode: ImmediateTextNode private var borderColorValue: UInt32? @@ -33,7 +33,7 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { private var text: String = "" override init() { - self.backgroundView = UIImageView() + self.backgroundView = UIView() self.textNode = ImmediateTextNode() super.init() @@ -48,11 +48,17 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { fatalError("init(coder:) has not been implemented") } - func update(borderColor: UInt32) { + func update(borderColor: UInt32, isHighlighted: Bool) { if self.borderColorValue != borderColor { self.borderColorValue = borderColor - self.backgroundView.image = generateStretchableFilledCircleImage(diameter: 10.0, color: nil, strokeColor: UIColor(argb: borderColor), strokeWidth: 1.0, backgroundColor: nil) + let previousColor = self.backgroundView.layer.borderColor + self.backgroundView.layer.cornerRadius = 5.0 + self.backgroundView.layer.borderColor = UIColor(argb: borderColor).cgColor + self.backgroundView.layer.borderWidth = 1.0 + if let previousColor = previousColor { + self.backgroundView.layer.animate(from: previousColor, to: UIColor(argb: borderColor).cgColor, keyPath: "borderColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.15) + } } } @@ -63,7 +69,7 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { if animated && previousText.isEmpty != text.isEmpty { if !text.isEmpty { self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) - self.textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: size.height / 2.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.textNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: 0.0, y: size.height / 2.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, additive: true) } else { if let copyView = self.textNode.view.snapshotContentTree() { self.view.insertSubview(copyView, at: 0) @@ -77,7 +83,11 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { let fontSize: CGFloat = floor(21.0 * size.height / 28.0) - self.textNode.attributedText = NSAttributedString(string: text, font: Font.monospace(fontSize), textColor: UIColor(argb: textColor)) + if #available(iOS 13.0, *) { + self.textNode.attributedText = NSAttributedString(string: text, font: UIFont.monospacedSystemFont(ofSize: fontSize, weight: .regular), textColor: UIColor(argb: textColor)) + } else { + self.textNode.attributedText = NSAttributedString(string: text, font: Font.monospace(fontSize), textColor: UIColor(argb: textColor)) + } let textSize = self.textNode.updateLayout(size) self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) @@ -86,9 +96,9 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { } private let prefixLabel: ImmediateTextNode - private let textField: UITextField + public let textField: UITextField - private var focusIndex: Int? + private var focusIndex: Int? = 0 private var itemViews: [ItemView] = [] public var updated: (() -> Void)? @@ -183,7 +193,7 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { } public func textFieldDidEndEditing(_ textField: UITextField) { - self.focusIndex = nil + self.focusIndex = textField.text?.count ?? 0 self.updateItemViews(animated: true) } @@ -205,7 +215,7 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { let itemView = self.itemViews[i] let itemSize = itemView.bounds.size - itemView.update(borderColor: self.focusIndex == i ? theme.activeBorder : theme.inactiveBorder) + itemView.update(borderColor: self.focusIndex == i ? theme.activeBorder : theme.inactiveBorder, isHighlighted: self.focusIndex == i) let itemText: String if i < self.textValue.count { itemText = String(self.textValue[self.textValue.index(self.textValue.startIndex, offsetBy: i)]) @@ -233,7 +243,11 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { height = 28.0 } - self.prefixLabel.attributedText = NSAttributedString(string: prefix, font: Font.monospace(21.0), textColor: UIColor(argb: theme.foreground)) + if #available(iOS 13.0, *) { + self.prefixLabel.attributedText = NSAttributedString(string: prefix, font: UIFont.monospacedSystemFont(ofSize: 21.0, weight: .regular), textColor: UIColor(argb: theme.foreground)) + } else { + self.prefixLabel.attributedText = NSAttributedString(string: prefix, font: Font.monospace(21.0), textColor: UIColor(argb: theme.foreground)) + } let prefixSize = self.prefixLabel.updateLayout(CGSize(width: width, height: 100.0)) let prefixSpacing: CGFloat = prefix.isEmpty ? 0.0 : 8.0 @@ -255,7 +269,7 @@ public final class CodeInputView: ASDisplayNode, UITextFieldDelegate { self.itemViews.append(itemView) self.addSubnode(itemView) } - itemView.update(borderColor: self.focusIndex == i ? theme.activeBorder : theme.inactiveBorder) + itemView.update(borderColor: self.focusIndex == i ? theme.activeBorder : theme.inactiveBorder, isHighlighted: self.focusIndex == i) let itemText: String if i < self.textValue.count { itemText = String(self.textValue[self.textValue.index(self.textValue.startIndex, offsetBy: i)]) diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 9debce3ba5..576df8e2e2 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -8,9 +8,9 @@ import TelegramCore import AccountContext import TelegramPresentationData import UIKit -import WebPBinding import AnimatedAvatarSetNode import ReactionImageComponent +import WebPBinding public final class ReactionIconView: PortalSourceView { public let imageView: UIImageView @@ -39,6 +39,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { var foreground: UInt32 var extractedBackground: UInt32 var extractedForeground: UInt32 + var isSelected: Bool } struct Counter: Equatable { @@ -47,12 +48,13 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { struct Layout: Equatable { var colors: Colors - var baseSize: CGSize + var size: CGSize var counter: Counter? } private struct AnimationState { - var fromCounter: Counter + var fromCounter: Counter? + var fromColors: Colors var startTime: Double var duration: Double } @@ -69,8 +71,8 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { 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()) + if let currentLayout = self.currentLayout, (currentLayout.counter != layout.counter || currentLayout.colors.isSelected != layout.colors.isSelected) { + self.animationState = AnimationState(fromCounter: currentLayout.counter, fromColors: currentLayout.colors, startTime: CACurrentMediaTime(), duration: 0.15 * UIView.animationDurationFactor()) } self.currentLayout = layout @@ -128,108 +130,122 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { } } - var imageWidth = layout.baseSize.width - while imageWidth < layout.baseSize.height / 2.0 + 1.0 + totalComponentWidth + 8.0 { - imageWidth += 2.0 - } - - let image = generateImage(CGSize(width: imageWidth, height: layout.baseSize.height), rotatedContext: { size, context in + let image = generateImage(layout.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) UIGraphicsPushContext(context) - let backgroundColor: UIColor - let foregroundColor: UIColor - if self.isExtracted { - backgroundColor = UIColor(argb: layout.colors.extractedBackground) - foregroundColor = UIColor(argb: layout.colors.extractedForeground) - } else { - backgroundColor = UIColor(argb: layout.colors.background) - foregroundColor = UIColor(argb: layout.colors.foreground) - } - - context.setBlendMode(.copy) - - context.setFillColor(backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.height, height: size.height))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height))) - context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height))) - - #if DEBUG && false - context.setFillColor(UIColor.blue.withAlphaComponent(0.5).cgColor) - context.fill(CGRect(origin: CGPoint(x: layout.baseSize.height / 2.0 + 1.0, y: 0.0), size: CGSize(width: size.width - (layout.baseSize.height / 2.0 + 1.0 + 8.0), height: size.height))) - #endif - - if let counter = layout.counter { - let isForegroundTransparent = foregroundColor.alpha < 1.0 - context.setBlendMode(isForegroundTransparent ? .copy : .normal) - - //let textAreaWidth = size.width - (layout.baseSize.height / 2.0 + 1.0 + 8.0) - - var textOrigin: CGFloat = layout.baseSize.height / 2.0 + 1.0 - 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)) + func drawContents(colors: Colors) { + let backgroundColor: UIColor + let foregroundColor: UIColor + if self.isExtracted { + backgroundColor = UIColor(argb: colors.extractedBackground) + foregroundColor = UIColor(argb: colors.extractedForeground) } else { - animationFraction = 1.0 + backgroundColor = UIColor(argb: colors.background) + foregroundColor = UIColor(argb: colors.foreground) } - for i in (0 ..< counter.components.count).reversed() { - let component = counter.components[i] - var componentAlpha: CGFloat = 1.0 - var componentVerticalOffset: CGFloat = 0.0 + context.setBlendMode(.copy) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.height, height: size.height))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height))) + context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height))) + + if let counter = layout.counter { + let isForegroundTransparent = foregroundColor.alpha < 1.0 + context.setBlendMode(isForegroundTransparent ? .copy : .normal) - 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 textOrigin: CGFloat = 36.0 + + var rightTextOrigin = textOrigin + totalComponentWidth + + let animationFraction: CGFloat + if let animationState = self.animationState, animationState.fromCounter != nil { + 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 fromCounter = animationState.fromCounter { + let reverseIndex = counter.components.count - 1 - i + if reverseIndex < fromCounter.components.count { + let previousComponent = fromCounter.components[fromCounter.components.count - 1 - reverseIndex] - let previousComponentAlpha = 1.0 - componentAlpha - var previousComponentVerticalOffset = -animationFraction * 8.0 - if previousComponent.string < component.string { - previousComponentVerticalOffset = -previousComponentVerticalOffset + 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 + } + + var componentOrigin = rightTextOrigin - previousComponent.bounds.width + componentOrigin = max(componentOrigin, layout.size.height / 2.0 + UIScreenPixel) + let previousColor: UIColor + if isForegroundTransparent { + previousColor = foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - previousComponentAlpha) + } else { + previousColor = foregroundColor.withMultipliedAlpha(previousComponentAlpha) + } + let string = NSAttributedString(string: previousComponent.string, font: Font.medium(11.0), textColor: previousColor) + string.draw(at: previousComponent.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - previousComponent.bounds.height) / 2.0 + previousComponentVerticalOffset)) } - - var componentOrigin = rightTextOrigin - previousComponent.bounds.width - componentOrigin = max(componentOrigin, layout.baseSize.height / 2.0 + UIScreenPixel) - let previousColor: UIColor - if isForegroundTransparent { - previousColor = foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - previousComponentAlpha) - } else { - previousColor = foregroundColor.withMultipliedAlpha(previousComponentAlpha) - } - let string = NSAttributedString(string: previousComponent.string, font: Font.medium(11.0), textColor: previousColor) - 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 currentColor: UIColor + if isForegroundTransparent { + currentColor = foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - componentAlpha) + } else { + currentColor = foregroundColor.withMultipliedAlpha(componentAlpha) + } + let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: currentColor) + string.draw(at: component.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0 + componentVerticalOffset)) + + rightTextOrigin -= component.bounds.width } - - let componentOrigin = rightTextOrigin - component.bounds.width - let currentColor: UIColor - if isForegroundTransparent { - currentColor = foregroundColor.mixedWith(backgroundColor, alpha: 1.0 - componentAlpha) - } else { - currentColor = foregroundColor.withMultipliedAlpha(componentAlpha) - } - let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: currentColor) - string.draw(at: component.bounds.origin.offsetBy(dx: componentOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0 + componentVerticalOffset)) - - rightTextOrigin -= component.bounds.width } } + if let animationState = self.animationState, animationState.fromColors.isSelected != layout.colors.isSelected { + var animationFraction: CGFloat = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration)) + if !layout.colors.isSelected { + animationFraction = 1.0 - animationFraction + } + + let center = CGPoint(x: 21.0, y: size.height / 2.0) + let diameter = 0.0 * (1.0 - animationFraction) + (size.width - center.x) * 2.0 * animationFraction + + context.beginPath() + context.addEllipse(in: CGRect(origin: CGPoint(x: center.x - diameter / 2.0, y: center.y - diameter / 2.0), size: CGSize(width: diameter, height: diameter))) + context.clip(using: .evenOdd) + drawContents(colors: layout.colors.isSelected ? layout.colors : animationState.fromColors) + + context.resetClip() + + context.beginPath() + context.addRect(CGRect(origin: CGPoint(), size: size)) + context.addEllipse(in: CGRect(origin: CGPoint(x: center.x - diameter / 2.0, y: center.y - diameter / 2.0), size: CGSize(width: diameter, height: diameter))) + context.clip(using: .evenOdd) + drawContents(colors: layout.colors.isSelected ? animationState.fromColors : layout.colors) + } else { + drawContents(colors: layout.colors) + } + UIGraphicsPopContext() - })?.stretchableImage(withLeftCapWidth: Int(layout.baseSize.height / 2.0), topCapHeight: Int(layout.baseSize.height / 2.0)) + })//?.stretchableImage(withLeftCapWidth: Int(layout.baseSize.height / 2.0), topCapHeight: Int(layout.baseSize.height / 2.0)) if let image = image { let previousContents = self.layer.contents @@ -293,7 +309,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { resultComponents.append(Component(string: component, bounds: boundingRect)) - if i == spec.stringComponents.count - 1 && component.count == 1 && component[component.startIndex].isNumber { + if i == spec.stringComponents.count - 1 && component[component.startIndex].isNumber { resultSize.width += CounterLayout.maxDigitWidth } else { resultSize.width += boundingRect.width @@ -357,24 +373,29 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { let spacing: CGFloat = 2.0 let boundingImageSize = CGSize(width: 20.0, height: 20.0) - let defaultImageSize = CGSize(width: boundingImageSize.width + floor(boundingImageSize.width * 0.5 * 2.0), height: boundingImageSize.height + floor(boundingImageSize.height * 0.5 * 2.0)) let imageSize: CGSize if let file = spec.component.reaction.centerAnimation { + let defaultImageSize = CGSize(width: boundingImageSize.width + floor(boundingImageSize.width * 0.5 * 2.0), height: boundingImageSize.height + floor(boundingImageSize.height * 0.5 * 2.0)) imageSize = file.dimensions?.cgSize.aspectFitted(defaultImageSize) ?? defaultImageSize + } else if let file = spec.component.reaction.legacyIcon { + imageSize = file.dimensions?.cgSize.aspectFitted(boundingImageSize) ?? boundingImageSize } else { - imageSize = defaultImageSize + imageSize = boundingImageSize } var counterComponents: [String] = [] for character in countString(Int64(spec.component.count)) { counterComponents.append(String(character)) } - #if DEBUG && false - counterComponents.removeAll() - for character in "42" { - counterComponents.append(String(character)) + + /*#if DEBUG + if spec.component.count % 2 == 0 { + counterComponents.removeAll() + for character in "123.5K" { + counterComponents.append(String(character)) + } } - #endif + #endif*/ let backgroundColor = spec.component.isSelected ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground @@ -411,7 +432,8 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { background: spec.component.isSelected ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground, foreground: spec.component.isSelected ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground, extractedBackground: spec.component.colors.extractedBackground, - extractedForeground: spec.component.colors.extractedForeground + extractedForeground: spec.component.colors.extractedForeground, + isSelected: spec.component.isSelected ) var backgroundCounter: ReactionButtonAsyncNode.ContainerButtonNode.Counter? if let counterLayout = counterLayout { @@ -421,7 +443,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { } let backgroundLayout = ContainerButtonNode.Layout( colors: backgroundColors, - baseSize: CGSize(width: height + 2.0, height: height), + size: size, counter: backgroundCounter ) @@ -447,6 +469,18 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { private let iconImageDisposable = MetaDisposable() + public var activateAfterCompletion: Bool = false { + didSet { + if self.activateAfterCompletion { + self.contextGesture?.activatedAfterCompletion = { [weak self] in + self?.pressed() + } + } else { + self.contextGesture?.activatedAfterCompletion = nil + } + } + } + override init() { self.containerNode = ContextExtractedContentContainingNode() self.buttonNode = ContainerButtonNode() @@ -475,22 +509,13 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { } self.isGestureEnabled = true + self.beginDelay = 0.0 self.containerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, _ in guard let strongSelf = self else { return } strongSelf.buttonNode.updateIsExtracted(isExtracted: isExtracted, animated: true) - - /*let backgroundImage = isExtracted ? layout.extractedBackgroundImage : layout.backgroundImage - - let previousContents = strongSelf.buttonNode.layer.contents - - ASDisplayNodeSetResizableContents(strongSelf.buttonNode.layer, backgroundImage) - - if let previousContents = previousContents { - strongSelf.buttonNode.layer.animate(from: previousContents as! CGImage, to: backgroundImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) - }*/ } } @@ -502,6 +527,16 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { self.iconImageDisposable.dispose() } + override public func didLoad() { + super.didLoad() + + if self.activateAfterCompletion { + self.contextGesture?.activatedAfterCompletion = { [weak self] in + self?.pressed() + } + } + } + @objc private func pressed() { guard let layout = self.layout else { return @@ -515,7 +550,6 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { self.containerNode.contentRect = CGRect(origin: CGPoint(), size: layout.size) animation.animator.updateFrame(layer: self.buttonNode.layer, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) - //ASDisplayNodeSetResizableContents(self.buttonNode.layer, layout.backgroundImage) self.buttonNode.update(layout: layout.backgroundLayout) animation.animator.updateFrame(layer: self.iconView.layer, frame: layout.imageFrame, completion: nil) @@ -535,6 +569,19 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode { } } })) + } else if let legacyIcon = layout.spec.component.reaction.legacyIcon { + self.iconImageDisposable.set((layout.spec.component.context.account.postbox.mediaBox.resourceData(legacyIcon.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.imageView.image = image + } + } + })) } } @@ -608,10 +655,12 @@ public final class ReactionButtonComponent: Equatable { public struct Reaction: Equatable { public var value: String public var centerAnimation: TelegramMediaFile? + public var legacyIcon: TelegramMediaFile? - public init(value: String, centerAnimation: TelegramMediaFile?) { + public init(value: String, centerAnimation: TelegramMediaFile?, legacyIcon: TelegramMediaFile?) { self.value = value self.centerAnimation = centerAnimation + self.legacyIcon = legacyIcon } public static func ==(lhs: Reaction, rhs: Reaction) -> Bool { @@ -621,6 +670,9 @@ public final class ReactionButtonComponent: Equatable { if lhs.centerAnimation?.fileId != rhs.centerAnimation?.fileId { return false } + if lhs.legacyIcon?.fileId != rhs.legacyIcon?.fileId { + return false + } return true } } diff --git a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift index 7e8c7fe9fa..ec616175cb 100644 --- a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift +++ b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift @@ -58,22 +58,51 @@ public func reactionStaticImage(context: AccountContext, animation: TelegramMedi }) } -public final class ReactionImageNode: ASImageNode { +public final class ReactionImageNode: ASDisplayNode { private var disposable: Disposable? - public let size: CGSize + private let size: CGSize + private let isAnimation: Bool + + private let iconNode: ASImageNode public init(context: AccountContext, availableReactions: AvailableReactions?, reaction: String) { + self.iconNode = ASImageNode() + var file: TelegramMediaFile? + var animationFile: TelegramMediaFile? if let availableReactions = availableReactions { for availableReaction in availableReactions.reactions { if availableReaction.value == reaction { file = availableReaction.staticIcon + animationFile = availableReaction.centerAnimation break } } } - if let file = file { - self.size = file.dimensions?.cgSize ?? CGSize(width: 18.0, height: 18.0) + if let animationFile = animationFile { + self.size = animationFile.dimensions?.cgSize ?? CGSize(width: 32.0, height: 32.0) + var displaySize = self.size.aspectFitted(CGSize(width: 20.0, height: 20.0)) + displaySize.width = floor(displaySize.width * 2.0) + displaySize.height = floor(displaySize.height * 2.0) + self.isAnimation = true + + super.init() + + self.disposable = (reactionStaticImage(context: context, animation: animationFile, pixelSize: CGSize(width: displaySize.width * UIScreenScale, height: displaySize.height * UIScreenScale)) + |> deliverOnMainQueue).start(next: { [weak self] data in + guard let strongSelf = self else { + return + } + + if data.isComplete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if let image = UIImage(data: dataValue) { + strongSelf.iconNode.image = image + } + } + }) + } else if let file = file { + self.size = file.dimensions?.cgSize ?? CGSize(width: 32.0, height: 32.0) + self.isAnimation = false super.init() @@ -85,60 +114,29 @@ public final class ReactionImageNode: ASImageNode { if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { if let image = WebP.convert(fromWebP: dataValue) { - strongSelf.image = image + strongSelf.iconNode.image = image } } }) } else { - self.size = CGSize(width: 18.0, height: 18.0) + self.size = CGSize(width: 32.0, height: 32.0) + self.isAnimation = false super.init() } + + self.addSubnode(self.iconNode) } deinit { self.disposable?.dispose() } -} - -public final class ReactionFileImageNode: ASImageNode { - private let disposable = MetaDisposable() - - private var currentFile: TelegramMediaFile? - override public init() { - } - - deinit { - self.disposable.dispose() - } - - public func asyncLayout() -> (_ context: AccountContext, _ file: TelegramMediaFile?) -> (size: CGSize, apply: () -> Void) { - return { [weak self] context, file in - let size = file?.dimensions?.cgSize ?? CGSize(width: 18.0, height: 18.0) - - return (size, { - guard let strongSelf = self else { - return - } - if strongSelf.currentFile != file { - strongSelf.currentFile = file - - if let file = file { - strongSelf.disposable.set((context.account.postbox.mediaBox.resourceData(file.resource) - |> deliverOnMainQueue).start(next: { 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.image = image - } - } - })) - } - } - }) + public func update(size: CGSize) { + var imageSize = self.size.aspectFitted(size) + if self.isAnimation { + imageSize.width *= 2.0 + imageSize.height *= 2.0 } + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize) } } diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 685193e3ea..7e57f68429 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -8,7 +8,6 @@ import TelegramCore import AccountContext import TelegramPresentationData import UIKit -import WebPBinding import AnimatedAvatarSetNode import ContextUI import AvatarNode @@ -168,8 +167,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let iconSpacing: CGFloat = 4.0 var iconSize = CGSize(width: 22.0, height: 22.0) - if let reactionIconNode = self.reactionIconNode { - iconSize = reactionIconNode.size.aspectFitted(iconSize) + if let _ = self.reactionIconNode { } else if let iconNode = self.iconNode, let image = iconNode.image { iconSize = image.size.aspectFitted(iconSize) } @@ -183,6 +181,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent if let reactionIconNode = self.reactionIconNode { reactionIconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) + reactionIconNode.update(size: iconSize) } else if let iconNode = self.iconNode { iconNode.frame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((constrainedSize.height - iconSize.height) / 2.0)), size: iconSize) } @@ -386,8 +385,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.titleLabelNode.frame = CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) if let reactionIconNode = self.reactionIconNode { - let reactionSize = reactionIconNode.size.aspectFitted(CGSize(width: 22.0, height: 22.0)) + let reactionSize = CGSize(width: 22.0, height: 22.0) reactionIconNode.frame = CGRect(origin: CGPoint(x: size.width - 32.0 - floor((32.0 - reactionSize.width) / 2.0), y: floor((size.height - reactionSize.height) / 2.0)), size: reactionSize) + reactionIconNode.update(size: reactionSize) } self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel)) diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 335ecc550a..cf9772e795 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, animateTargetContainer: UIView?, completion: @escaping () -> Void) { + func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { if let presentationNode = self.presentationNode { - presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, completion: completion) + presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, 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, animateTargetContainer: animateTargetContainer, completion: { [weak self] in + reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, 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, animateTargetContainer: UIView?, completion: (() -> Void)?) { + public func dismissWithReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: (() -> Void)?) { if !self.wasDismissed { self.wasDismissed = true - self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, completion: { [weak self] in + self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() }) diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index c9b861d35e..e80d571c3f 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -970,7 +970,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode { let transition: ContainedViewLayoutTransition if animated { - transition = .animated(duration: 0.45, curve: .spring) + transition = .animated(duration: self.itemContainers.count == 1 ? 0.3 : 0.45, curve: .spring) } else { transition = .immediate } diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index 95d3ad5f29..644ed04281 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, animateTargetContainer: UIView?, completion: @escaping () -> Void) { + func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, 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, animateTargetContainer: animateTargetContainer, completion: { [weak self] in + reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, 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 8571766276..54eb0fec80 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift @@ -6,6 +6,7 @@ import TelegramPresentationData import TextSelectionNode import TelegramCore import SwiftSignalKit +import ReactionSelectionNode enum ContextControllerPresentationNodeStateTransition { case animateIn @@ -24,7 +25,7 @@ protocol ContextControllerPresentationNode: ASDisplayNode { stateTransition: ContextControllerPresentationNodeStateTransition? ) - func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, completion: @escaping () -> Void) + func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) func cancelReactionAnimation() func highlightGestureMoved(location: CGPoint) diff --git a/submodules/Display/Source/ContextControllerSourceNode.swift b/submodules/Display/Source/ContextControllerSourceNode.swift index 1cadfe7ba7..a83edbd227 100644 --- a/submodules/Display/Source/ContextControllerSourceNode.swift +++ b/submodules/Display/Source/ContextControllerSourceNode.swift @@ -2,13 +2,18 @@ import Foundation import AsyncDisplayKit open class ContextControllerSourceNode: ASDisplayNode { - private var contextGesture: ContextGesture? + public private(set) var contextGesture: ContextGesture? public var isGestureEnabled: Bool = true { didSet { self.contextGesture?.isEnabled = self.isGestureEnabled } } + public var beginDelay: Double = 0.12 { + didSet { + self.contextGesture?.beginDelay = self.beginDelay + } + } public var animateScale: Bool = true public var activated: ((ContextGesture, CGPoint) -> Void)? @@ -31,6 +36,9 @@ open class ContextControllerSourceNode: ASDisplayNode { self.contextGesture = contextGesture self.view.addGestureRecognizer(contextGesture) + contextGesture.beginDelay = self.beginDelay + contextGesture.isEnabled = self.isGestureEnabled + contextGesture.shouldBegin = { [weak self] point in guard let strongSelf = self, !strongSelf.bounds.width.isZero else { return false diff --git a/submodules/Display/Source/ContextGesture.swift b/submodules/Display/Source/ContextGesture.swift index 2130599be4..7e2eedc52c 100644 --- a/submodules/Display/Source/ContextGesture.swift +++ b/submodules/Display/Source/ContextGesture.swift @@ -20,8 +20,6 @@ private class TimerTargetWrapper: NSObject { } } -private let beginDelay: Double = 0.12 - public func cancelParentGestures(view: UIView) { if let gestureRecognizers = view.gestureRecognizers { for recognizer in gestureRecognizers { @@ -55,16 +53,19 @@ private func cancelOtherGestures(gesture: ContextGesture, view: UIView) { } public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDelegate { + public var beginDelay: Double = 0.12 private var currentProgress: CGFloat = 0.0 private var delayTimer: Timer? private var animator: DisplayLinkAnimator? private var isValidated: Bool = false + private var wasActivated: Bool = false public var shouldBegin: ((CGPoint) -> Bool)? public var activationProgress: ((CGFloat, ContextGestureTransition) -> Void)? public var activated: ((ContextGesture, CGPoint) -> Void)? public var externalUpdated: ((UIView?, CGPoint) -> Void)? public var externalEnded: (((UIView?, CGPoint)?) -> Void)? + public var activatedAfterCompletion: (() -> Void)? override public init(target: Any?, action: Selector?) { super.init(target: target, action: action) @@ -85,6 +86,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg self.externalEnded = nil self.animator?.invalidate() self.animator = nil + self.wasActivated = false } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { @@ -116,7 +118,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg } if self.delayTimer == nil { - let delayTimer = Timer(timeInterval: beginDelay, target: TimerTargetWrapper { [weak self] in + let delayTimer = Timer(timeInterval: self.beginDelay, target: TimerTargetWrapper { [weak self] in guard let strongSelf = self, let _ = strongSelf.delayTimer else { return } @@ -139,6 +141,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg strongSelf.delayTimer?.invalidate() strongSelf.animator?.invalidate() strongSelf.activated?(strongSelf, location) + strongSelf.wasActivated = true if let view = strongSelf.view?.superview { if let window = view.window { cancelOtherGestures(gesture: strongSelf, view: window) @@ -174,6 +177,7 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg self.delayTimer?.invalidate() self.animator?.invalidate() self.activated?(self, touch.location(in: self.view)) + self.wasActivated = true if let view = self.view?.superview { if let window = view.window { cancelOtherGestures(gesture: self, view: window) @@ -198,6 +202,9 @@ public final class ContextGesture: UIGestureRecognizer, UIGestureRecognizerDeleg if !self.currentProgress.isZero, self.isValidated { self.currentProgress = 0.0 self.activationProgress?(0.0, .ended(self.currentProgress)) + if self.wasActivated { + self.activatedAfterCompletion?() + } } self.externalEnded?((self.view, touch.location(in: self.view))) diff --git a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift index b525e5fb7e..a0a88cea78 100644 --- a/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift +++ b/submodules/PeerInfoUI/Sources/ItemListReactionItem.swift @@ -13,7 +13,8 @@ import AccountContext public class ItemListReactionItem: ListViewItem, ItemListItem { let context: AccountContext let presentationData: ItemListPresentationData - let file: TelegramMediaFile? + let availableReactions: AvailableReactions? + let reaction: String let title: String let value: Bool let enabled: Bool @@ -25,7 +26,8 @@ public class ItemListReactionItem: ListViewItem, ItemListItem { public init( context: AccountContext, presentationData: ItemListPresentationData, - file: TelegramMediaFile?, + availableReactions: AvailableReactions?, + reaction: String, title: String, value: Bool, enabled: Bool = true, @@ -36,7 +38,8 @@ public class ItemListReactionItem: ListViewItem, ItemListItem { ) { self.context = context self.presentationData = presentationData - self.file = file + self.availableReactions = availableReactions + self.reaction = reaction self.title = title self.value = value self.enabled = enabled @@ -122,7 +125,7 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { private let highlightedBackgroundNode: ASDisplayNode private let maskNode: ASImageNode - private let imageNode: ReactionFileImageNode + private var imageNode: ReactionImageNode? private let titleNode: TextNode private var switchNode: ASDisplayNode & ItemListSwitchNodeImpl private let switchGestureNode: ASDisplayNode @@ -150,8 +153,6 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode.isLayerBacked = true - self.imageNode = ReactionFileImageNode() - self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false @@ -166,7 +167,6 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.imageNode) self.addSubnode(self.titleNode) self.addSubnode(self.switchNode) self.addSubnode(self.switchGestureNode) @@ -191,7 +191,6 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { } func asyncLayout() -> (_ item: ItemListReactionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { - let makeImageLayout = self.imageNode.asyncLayout() let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let currentItem = self.item @@ -227,8 +226,6 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { insets = itemListNeighborsGroupedInsets(neighbors, params) } - let (imageSize, imageApply) = makeImageLayout(item.context, item.file) - let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 80.0 - sideImageInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) contentSize.height = max(contentSize.height, titleLayout.size.height + 22.0) @@ -361,9 +358,17 @@ public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } - let imageFitSize = imageSize.aspectFitted(CGSize(width: 30.0, height: 30.0)) - strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor(sideImageInset - imageFitSize.width), y: floor((contentSize.height - imageFitSize.height) / 2.0)), size: imageFitSize) - imageApply() + if strongSelf.imageNode == nil, let availableReactions = item.availableReactions { + let imageNode = ReactionImageNode(context: item.context, availableReactions: availableReactions, reaction: item.reaction) + strongSelf.imageNode = imageNode + strongSelf.addSubnode(imageNode) + } + + if let imageNode = strongSelf.imageNode { + let imageFitSize = CGSize(width: 30.0, height: 30.0) + imageNode.frame = CGRect(origin: CGPoint(x: params.leftInset + floor(sideImageInset - imageFitSize.width), y: floor((contentSize.height - imageFitSize.height) / 2.0)), size: imageFitSize) + imageNode.update(size: imageFitSize) + } strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) if let switchView = strongSelf.switchNode.view as? UISwitch { diff --git a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift index dcd12aa8e6..046ec7f0bd 100644 --- a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift +++ b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift @@ -45,7 +45,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { case allowAllInfo(String) case itemsHeader(String) - case item(index: Int, value: String, file: TelegramMediaFile?, text: String, isEnabled: Bool) + case item(index: Int, value: String, availableReactions: AvailableReactions?, reaction: String, text: String, isEnabled: Bool) var section: ItemListSectionId { switch self { @@ -64,7 +64,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { return .allowAllInfo case .itemsHeader: return .itemsHeader - case let .item(_, value, _, _, _): + case let .item(_, value, _, _, _, _): return .item(value) } } @@ -77,7 +77,7 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { return 1 case .itemsHeader: return 2 - case let .item(index, _, _, _, _): + case let .item(index, _, _, _, _, _): return 100 + index } } @@ -102,8 +102,8 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { } else { return false } - case let .item(index, value, file, text, isEnabled): - if case .item(index, value, file, text, isEnabled) = rhs { + case let .item(index, value, availableReactions, reaction, text, isEnabled): + if case .item(index, value, availableReactions, reaction, text, isEnabled) = rhs { return true } else { return false @@ -126,11 +126,12 @@ private enum PeerAllowedReactionListControllerEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .itemsHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .item(_, value, file, text, isEnabled): + case let .item(_, value, availableReactions, reaction, text, isEnabled): return ItemListReactionItem( context: arguments.context, presentationData: presentationData, - file: file, + availableReactions: availableReactions, + reaction: reaction, title: text, value: isEnabled, sectionId: self.section, @@ -172,7 +173,7 @@ private func peerAllowedReactionListControllerEntries( if !availableReaction.isEnabled { continue } - entries.append(.item(index: index, value: availableReaction.value, file: availableReaction.staticIcon, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value))) + entries.append(.item(index: index, value: availableReaction.value, availableReactions: availableReactions, reaction: availableReaction.value, text: availableReaction.title, isEnabled: allowedReactions.contains(availableReaction.value))) index += 1 } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift index aa057cd5f6..0b73fb9202 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -67,7 +67,6 @@ final class ReactionContextBackgroundNode: ASDisplayNode { self.backgroundLayer.backgroundColor = UIColor.black.cgColor self.backgroundLayer.masksToBounds = true - self.backgroundLayer.cornerRadius = 52.0 / 2.0 self.largeCircleLayer.backgroundColor = UIColor.black.cgColor self.largeCircleLayer.masksToBounds = true @@ -114,6 +113,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode { size: CGSize, cloudSourcePoint: CGFloat, isLeftAligned: Bool, + isMinimized: Bool, transition: ContainedViewLayoutTransition ) { let shadowInset: CGFloat = 15.0 @@ -136,7 +136,13 @@ final class ReactionContextBackgroundNode: ASDisplayNode { } } - let backgroundFrame = CGRect(origin: CGPoint(), size: size) + var backgroundFrame = CGRect(origin: CGPoint(), size: size) + if isMinimized { + let updatedHeight = floor(size.height * 0.9) + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - updatedHeight), size: CGSize(width: size.width, height: updatedHeight)) + } + + transition.updateCornerRadius(layer: self.backgroundLayer, cornerRadius: backgroundFrame.height / 2.0) let largeCircleFrame: CGRect let smallCircleFrame: CGRect diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 2a6e155eee..62b4cc4252 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -21,20 +21,26 @@ public final class ReactionContextItem { public let appearAnimation: TelegramMediaFile public let stillAnimation: TelegramMediaFile public let listAnimation: TelegramMediaFile + public let largeListAnimation: TelegramMediaFile public let applicationAnimation: TelegramMediaFile + public let largeApplicationAnimation: TelegramMediaFile public init( reaction: ReactionContextItem.Reaction, appearAnimation: TelegramMediaFile, stillAnimation: TelegramMediaFile, listAnimation: TelegramMediaFile, - applicationAnimation: TelegramMediaFile + largeListAnimation: TelegramMediaFile, + applicationAnimation: TelegramMediaFile, + largeApplicationAnimation: TelegramMediaFile ) { self.reaction = reaction self.appearAnimation = appearAnimation self.stillAnimation = stillAnimation self.listAnimation = listAnimation + self.largeListAnimation = largeListAnimation self.applicationAnimation = applicationAnimation + self.largeApplicationAnimation = largeApplicationAnimation } } @@ -56,8 +62,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private weak var currentLongPressItemNode: ReactionNode? - private var isExpanded: Bool = true private var highlightedReaction: ReactionContextItem.Reaction? + private var didTriggerExpandedReaction: Bool = false private var continuousHaptic: Any? private var validLayout: (CGSize, UIEdgeInsets, CGRect)? private var isLeftAligned: Bool = true @@ -187,12 +193,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { cloudSourcePoint = max(rect.minX + rect.height / 2.0, anchorRect.minX) } - var visualRect = rect + let visualRect = rect - if self.highlightedReaction != nil { + /*if self.highlightedReaction != nil { visualRect.origin.x -= 4.0 visualRect.size.width += 8.0 - } + }*/ return (rect, visualRect, isLeftAligned, cloudSourcePoint) } @@ -205,52 +211,96 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let sideInset: CGFloat = 11.0 let itemSpacing: CGFloat = 9.0 let itemSize: CGFloat = 40.0 - let verticalInset: CGFloat = 13.0 - let rowHeight: CGFloat = 30.0 - let visibleBounds = self.scrollNode.view.bounds - let appearBounds = self.scrollNode.view.bounds.insetBy(dx: 16.0, dy: 0.0) + let containerHeight: CGFloat = 52.0 + var contentHeight: CGFloat = containerHeight + if self.highlightedReaction != nil { + contentHeight = floor(contentHeight * 0.9) + } + + //let highlightItemOffset: CGFloat = floor(itemSize * 0.8 / 2.0 * 0.5) + let totalVisibleCount: CGFloat = CGFloat(self.items.count)//7.0 + let totalVisibleWidth: CGFloat = totalVisibleCount * itemSize + (totalVisibleCount - 1.0) * itemSpacing + + //width = count * itemSize + (count - 1) * spacing + //count * itemSize = width - (count - 1) * spacing + //itemSize = (width - (count - 1) * spacing) / count + + let selectedItemSize = floor(itemSize * 1.5) + let remainingVisibleWidth = totalVisibleWidth - selectedItemSize + let remainingVisibleCount = totalVisibleCount - 1.0 + let remainingItemSize = floor((remainingVisibleWidth - (remainingVisibleCount - 1.0) * itemSpacing) / remainingVisibleCount) + + let highlightItemSpacing: CGFloat = floor(itemSize * 0.2) + _ = highlightItemSpacing + + //print("self.highlightedReaction = \(String(describing: self.highlightedReaction))") + + var visibleBounds = self.scrollNode.view.bounds self.previewingItemContainer.bounds = visibleBounds + if self.highlightedReaction != nil { + visibleBounds = visibleBounds.insetBy(dx: remainingItemSize - selectedItemSize, dy: 0.0) + } + let appearBounds = visibleBounds.insetBy(dx: 16.0, dy: 0.0) let highlightedReactionIndex = self.items.firstIndex(where: { $0.reaction == self.highlightedReaction }) var validIndices = Set() + var nextX: CGFloat = sideInset for i in 0 ..< self.items.count { - let columnIndex = i - let column = CGFloat(columnIndex) - - let itemOffsetY: CGFloat = -1.0 - - var baseItemFrame = CGRect(origin: CGPoint(x: sideInset + column * (itemSize + itemSpacing), y: verticalInset + floor((rowHeight - itemSize) / 2.0) + itemOffsetY), size: CGSize(width: itemSize, height: itemSize)) + var currentItemSize = itemSize if let highlightedReactionIndex = highlightedReactionIndex { - if i > highlightedReactionIndex { - baseItemFrame.origin.x += 8.0 - } else if i == highlightedReactionIndex { - baseItemFrame.origin.x += 4.0 + if highlightedReactionIndex == i { + currentItemSize = selectedItemSize + } else { + currentItemSize = remainingItemSize } } + var baseItemFrame = CGRect(origin: CGPoint(x: nextX, y: containerHeight - contentHeight + floor((contentHeight - currentItemSize) / 2.0)), size: CGSize(width: currentItemSize, height: currentItemSize)) + if highlightedReactionIndex == i { + let updatedSize = floor(itemSize * 2.0) + baseItemFrame = baseItemFrame.insetBy(dx: (baseItemFrame.width - updatedSize) / 2.0, dy: (baseItemFrame.height - updatedSize) / 2.0) + + baseItemFrame.origin.y = containerHeight - contentHeight + floor((contentHeight - itemSize) / 2.0) + itemSize + 4.0 - updatedSize + } + nextX += currentItemSize + itemSpacing + + /*if let highlightedReactionIndex = highlightedReactionIndex { + let indexDistance = i - highlightedReactionIndex + _ = indexDistance + if i > highlightedReactionIndex { + baseItemFrame.origin.x += highlightItemOffset// - highlightItemSpacing * CGFloat(indexDistance) + } else if i == highlightedReactionIndex { + //baseItemFrame.origin.x += highlightItemOffset * 0.5 + } else { + baseItemFrame.origin.x -= highlightItemOffset// - highlightItemSpacing * CGFloat(indexDistance) + } + }*/ + if appearBounds.intersects(baseItemFrame) || (self.visibleItemNodes[i] != nil && visibleBounds.intersects(baseItemFrame)) { validIndices.insert(i) - var itemFrame = baseItemFrame + let itemFrame = baseItemFrame var isPreviewing = false if self.highlightedReaction == self.items[i].reaction { - let updatedSize = CGSize(width: floor(itemFrame.width * 1.66), height: floor(itemFrame.height * 1.66)) - itemFrame = CGRect(origin: CGPoint(x: itemFrame.midX - updatedSize.width / 2.0, y: itemFrame.maxY + 4.0 - updatedSize.height), size: updatedSize) + //let updatedSize = CGSize(width: floor(itemFrame.width * 2.5), height: floor(itemFrame.height * 2.5)) + //itemFrame = CGRect(origin: CGPoint(x: itemFrame.midX - updatedSize.width / 2.0, y: itemFrame.maxY + 4.0 - updatedSize.height), size: updatedSize) isPreviewing = true } else if self.highlightedReaction != nil { - let updatedSize = CGSize(width: floor(itemFrame.width * 0.9), height: floor(itemFrame.height * 0.9)) - itemFrame = CGRect(origin: CGPoint(x: itemFrame.midX - updatedSize.width / 2.0, y: itemFrame.midY - updatedSize.height / 2.0), size: updatedSize) + //let updatedSize = CGSize(width: floor(itemFrame.width * 0.8), height: floor(itemFrame.height * 0.8)) + //itemFrame = CGRect(origin: CGPoint(x: itemFrame.midX - updatedSize.width / 2.0, y: itemFrame.midY - updatedSize.height / 2.0), size: updatedSize) } var animateIn = false let itemNode: ReactionNode + var itemTransition = transition if let current = self.visibleItemNodes[i] { itemNode = current } else { animateIn = self.didAnimateIn + itemTransition = .immediate itemNode = ReactionNode(context: self.context, theme: self.theme, item: self.items[i]) self.visibleItemNodes[i] = itemNode @@ -264,7 +314,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - transition.updateFrame(node: itemNode, frame: itemFrame, beginWithCurrentState: true, completion: { [weak self, weak itemNode] completed in + itemTransition.updateFrame(node: itemNode, frame: itemFrame, beginWithCurrentState: true, completion: { [weak self, weak itemNode] completed in guard let strongSelf = self, let itemNode = itemNode else { return } @@ -277,7 +327,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } }) - itemNode.updateLayout(size: itemFrame.size, isExpanded: false, isPreviewing: isPreviewing, transition: transition) + itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: isPreviewing, transition: itemTransition) if animateIn { itemNode.appear(animated: !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion) @@ -337,6 +387,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { size: visualBackgroundFrame.size, cloudSourcePoint: cloudSourcePoint - visualBackgroundFrame.minX, isLeftAligned: isLeftAligned, + isMinimized: self.highlightedReaction != nil, transition: transition ) @@ -410,15 +461,6 @@ 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 @@ -454,6 +496,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if hideNode { targetView.alpha = 1.0 targetView.isHidden = false + if let targetView = targetView as? ReactionIconView { + targetView.imageView.alpha = 1.0 + } targetSnapshotView?.isHidden = true targetScaleCompleted = true intermediateCompletion() @@ -475,86 +520,158 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - public func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, completion: @escaping () -> Void) { + public func animateOutToReaction(value: String, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { + var foundItemNode: ReactionNode? for (_, itemNode) in self.visibleItemNodes { - if itemNode.item.reaction.rawValue != value { - continue + if itemNode.item.reaction.rawValue == value { + foundItemNode = itemNode + break } - - self.animationTargetView = targetView - self.animationHideNode = hideNode - - if hideNode { - 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 - } - }) + } + guard let itemNode = foundItemNode else { + completion() + return + } + + self.animationTargetView = targetView + self.animationHideNode = hideNode + + if hideNode { + 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 + } + }) + } + } + + itemNode.isExtracted = true + let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view) + let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) + + var expandedSize: CGSize = selfTargetRect.size + if self.didTriggerExpandedReaction { + expandedSize = CGSize(width: 120.0, height: 120.0) + } + + let expandedFrame = CGRect(origin: CGPoint(x: selfTargetRect.midX - expandedSize.width / 2.0, y: selfTargetRect.midY - expandedSize.height / 2.0), size: expandedSize) + + let effectFrame: CGRect + let incomingMessage: Bool = expandedFrame.midX < self.bounds.width / 2.0 + if self.didTriggerExpandedReaction { + effectFrame = 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) + } else { + effectFrame = expandedFrame.insetBy(dx: -60.0, dy: -60.0) + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear) + + self.addSubnode(itemNode) + itemNode.position = expandedFrame.center + transition.updateBounds(node: itemNode, bounds: CGRect(origin: CGPoint(), size: expandedFrame.size)) + itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, largeExpanded: self.didTriggerExpandedReaction, isPreviewing: false, transition: transition) + + let additionalAnimationNode = AnimatedStickerNode() + + let additionalAnimation: TelegramMediaFile + if self.didTriggerExpandedReaction { + additionalAnimation = itemNode.item.largeApplicationAnimation + if incomingMessage { + additionalAnimationNode.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + } + } else { + additionalAnimation = itemNode.item.applicationAnimation + } + + additionalAnimationNode.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: additionalAnimation.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: itemNode.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(additionalAnimation.resource.id))) + additionalAnimationNode.frame = effectFrame + additionalAnimationNode.updateLayout(size: effectFrame.size) + self.addSubnode(additionalAnimationNode) + + var mainAnimationCompleted = false + var additionalAnimationCompleted = false + let intermediateCompletion: () -> Void = { + if mainAnimationCompleted && additionalAnimationCompleted { + completion() + } + } + + additionalAnimationNode.completed = { _ in + additionalAnimationCompleted = true + intermediateCompletion() + } + + transition.animatePositionWithKeyframes(node: itemNode, keyframes: generateParabollicMotionKeyframes(from: selfSourceRect.center, to: expandedFrame.center, elevation: 30.0), completion: { [weak self, weak itemNode, weak targetView] _ in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { + guard let strongSelf = self else { + return } - } - - itemNode.isExtracted = true - let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view) - let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) - - let expandedSize: CGSize = selfTargetRect.size - - let expandedFrame = CGRect(origin: CGPoint(x: selfTargetRect.midX - expandedSize.width / 2.0, y: selfTargetRect.midY - expandedSize.height / 2.0), size: expandedSize) - - let effectFrame = expandedFrame.insetBy(dx: -60.0, dy: -60.0) - - let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .linear) - - self.addSubnode(itemNode) - 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) - - let additionalAnimationNode = AnimatedStickerNode() - - 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 - var additionalAnimationCompleted = false - let intermediateCompletion: () -> Void = { - if mainAnimationCompleted && additionalAnimationCompleted { - completion() + if strongSelf.didTriggerExpandedReaction { + return } - } - - additionalAnimationNode.completed = { _ in - additionalAnimationCompleted = true - intermediateCompletion() - } - - 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) + guard let itemNode = itemNode else { + return } + guard let targetView = targetView as? ReactionIconView else { + return + } + targetView.isHidden = false + targetView.imageView.alpha = 0.0 + targetView.addSubnode(itemNode) + itemNode.frame = targetView.bounds }) - - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + min(5.0, 2.0 * UIView.animationDurationFactor()), execute: { - self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { + }) + + 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: { + if self.didTriggerExpandedReaction { + self.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: hideNode, completion: { [weak self] in + if let strongSelf = self, strongSelf.didTriggerExpandedReaction, let addStandaloneReactionAnimation = addStandaloneReactionAnimation { + let standaloneReactionAnimation = StandaloneReactionAnimation() + + addStandaloneReactionAnimation(standaloneReactionAnimation) + + standaloneReactionAnimation.animateReactionSelection( + context: strongSelf.context, + theme: strongSelf.context.sharedContext.currentPresentationData.with({ $0 }).theme, + reaction: itemNode.item, + targetView: targetView, + hideNode: true, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) + } + mainAnimationCompleted = true intermediateCompletion() }) - }) - return - } - completion() + } else { + if hideNode { + targetView.alpha = 1.0 + targetView.isHidden = false + if let targetView = targetView as? ReactionIconView { + targetView.imageView.alpha = 1.0 + itemNode.removeFromSupernode() + } + } + mainAnimationCompleted = true + intermediateCompletion() + } + }) } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -575,7 +692,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if #available(iOS 13.0, *) { self.continuousHaptic = try? ContinuousHaptic(duration: 2.5) } - //itemNode.updateIsLongPressing(isLongPressing: true) if self.hapticFeedback == nil { self.hapticFeedback = HapticFeedback() @@ -587,11 +703,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } case .ended, .cancelled: self.continuousHaptic = nil - if let itemNode = self.currentLongPressItemNode { - self.currentLongPressItemNode = nil - self.reactionSelected?(itemNode.item) - itemNode.updateIsLongPressing(isLongPressing: false) - } + self.didTriggerExpandedReaction = true self.highlightGestureFinished(performAction: true) default: break @@ -714,13 +826,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) } } - - @objc private func disclosurePressed() { - self.isExpanded = true - if let (size, insets, anchorRect) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .animated(duration: 0.3, curve: .spring), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) - } - } } public final class StandaloneReactionAnimation: ASDisplayNode { @@ -762,13 +867,22 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if let targetView = targetView as? ReactionIconView { self.itemNodeIsEmbedded = true targetView.addSubnode(itemNode) - - targetView.imageView.isHidden = true } else { self.addSubnode(itemNode) - - if hideNode { - targetView.isHidden = true + } + + itemNode.expandedAnimationDidBegin = { [weak self, weak targetView] in + guard let strongSelf = self, let targetView = targetView else { + return + } + if let targetView = targetView as? ReactionIconView { + strongSelf.itemNodeIsEmbedded = true + + targetView.imageView.isHidden = true + } else { + if hideNode { + targetView.isHidden = true + } } } @@ -802,7 +916,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } } - itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, isPreviewing: false, transition: .immediate) + itemNode.updateLayout(size: expandedFrame.size, isExpanded: true, largeExpanded: false, isPreviewing: false, transition: .immediate) let additionalAnimationNode = AnimatedStickerNode() 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))) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index f2d7f53ad7..bce90e3253 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -58,6 +58,8 @@ final class ReactionNode: ASDisplayNode { private var isLongPressing: Bool = false private var longPressAnimator: DisplayLinkAnimator? + var expandedAnimationDidBegin: (() -> Void)? + init(context: AccountContext, theme: PresentationTheme, item: ReactionContextItem) { self.context = context self.item = item @@ -105,7 +107,7 @@ final class ReactionNode: ASDisplayNode { } } - func updateLayout(size: CGSize, isExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) { let intrinsicSize = size let animationSize = self.item.stillAnimation.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) @@ -128,7 +130,15 @@ final class ReactionNode: ASDisplayNode { self.animationNode = animationNode self.addSubnode(animationNode) - 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.started = { [weak self] in + self?.expandedAnimationDidBegin?() + } + + if largeExpanded { + animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.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.largeListAnimation.resource.id))) + } else { + 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) diff --git a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift index 9e13035841..9b5258a10b 100644 --- a/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift +++ b/submodules/SettingsUI/Sources/Reactions/QuickReactionSetupController.swift @@ -10,7 +10,6 @@ import TelegramUIPreferences import ItemListUI import PresentationDataUtils import AccountContext -import WebPBinding import ReactionImageComponent private final class QuickReactionSetupControllerArguments { @@ -273,19 +272,19 @@ public func quickReactionSetupController( if !availableReaction.isEnabled { continue } + guard let centerAnimation = availableReaction.centerAnimation else { + continue + } - let signal: Signal<(String, UIImage?), NoError> = context.account.postbox.mediaBox.resourceData(availableReaction.staticIcon.resource) - |> distinctUntilChanged(isEqual: { lhs, rhs in - return lhs.complete == rhs.complete - }) + let signal: Signal<(String, UIImage?), NoError> = reactionStaticImage(context: context, animation: centerAnimation, pixelSize: CGSize(width: 72.0, height: 72.0)) |> map { data -> (String, UIImage?) in - guard data.complete else { + guard data.isComplete else { return (availableReaction.value, nil) } guard let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else { return (availableReaction.value, nil) } - guard let image = WebP.convert(fromWebP: dataValue) else { + guard let image = UIImage(data: dataValue) else { return (availableReaction.value, nil) } return (availableReaction.value, image) diff --git a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift index 6bbc3cc9bf..2c4a70dcd4 100644 --- a/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift +++ b/submodules/SettingsUI/Sources/Reactions/ReactionChatPreviewItem.swift @@ -166,7 +166,9 @@ class ReactionChatPreviewItemNode: ListViewItemNode { appearAnimation: reaction.appearAnimation, stillAnimation: reaction.selectAnimation, listAnimation: centerAnimation, - applicationAnimation: aroundAnimation + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation ), targetView: targetView, hideNode: true, diff --git a/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift b/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift index e283434ba3..3e83d53201 100644 --- a/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/submodules/TelegramUI/Sources/AuthorizationSequenceCodeEntryControllerNode.swift @@ -42,7 +42,6 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var currentCode: String { return self.codeInputView.text - //return self.codeField.textField.text ?? "" } var loginWithCode: ((String) -> Void)? @@ -53,7 +52,6 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF var inProgress: Bool = false { didSet { self.codeInputView.alpha = self.inProgress ? 0.6 : 1.0 - //self.codeField.alpha = self.inProgress ? 0.6 : 1.0 } } @@ -89,11 +87,18 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.nextOptionButtonNode.isUserInteractionEnabled = nextOptionActive self.nextOptionButtonNode.addSubnode(self.nextOptionTitleNode) - /*self.codeSeparatorNode = ASDisplayNode() - self.codeSeparatorNode.isLayerBacked = true - self.codeSeparatorNode.backgroundColor = self.theme.list.itemPlainSeparatorColor*/ - self.codeInputView = CodeInputView() + self.codeInputView.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.codeInputView.textField.returnKeyType = .done + self.codeInputView.textField.disableAutomaticKeyboardHandling = [.forward, .backward] + if #available(iOSApplicationExtension 12.0, iOS 12.0, *) { + self.codeInputView.textField.textContentType = .oneTimeCode + } + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + self.codeInputView.textField.keyboardType = .asciiCapableNumberPad + } else { + self.codeInputView.textField.keyboardType = .numberPad + } /*self.codeField = TextFieldNode() self.codeField.textField.font = Font.regular(24.0) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 535d941037..8ad640c2ae 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1030,7 +1030,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G appearAnimation: reaction.appearAnimation, stillAnimation: reaction.selectAnimation, listAnimation: centerAnimation, - applicationAnimation: aroundAnimation + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation )) } } @@ -1080,7 +1082,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G hideTargetButton = targetView.superview } - controller.dismissWithReaction(value: updatedReaction, targetView: targetView, hideNode: true, animateTargetContainer: hideTargetButton, completion: { [weak itemNode, weak targetView] in + controller.dismissWithReaction(value: updatedReaction, targetView: targetView, hideNode: true, animateTargetContainer: hideTargetButton, addStandaloneReactionAnimation: { standaloneReactionAnimation in + guard let strongSelf = self else { + return + } + strongSelf.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation) + standaloneReactionAnimation.frame = strongSelf.chatDisplayNode.bounds + strongSelf.chatDisplayNode.addSubnode(standaloneReactionAnimation) + }, completion: { [weak itemNode, weak targetView] in guard let strongSelf = self, let itemNode = itemNode, let targetView = targetView else { return } @@ -1279,7 +1288,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G appearAnimation: reaction.appearAnimation, stillAnimation: reaction.selectAnimation, listAnimation: centerAnimation, - applicationAnimation: aroundAnimation + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation ), targetView: targetView, hideNode: true, diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 8e6e3ba806..b0b8aef217 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2590,7 +2590,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { appearAnimation: reaction.appearAnimation, stillAnimation: reaction.selectAnimation, listAnimation: centerAnimation, - applicationAnimation: aroundAnimation + largeListAnimation: reaction.activateAnimation, + applicationAnimation: aroundAnimation, + largeApplicationAnimation: reaction.effectAnimation ), targetView: targetView, hideNode: true, diff --git a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift index 4908f2f5a7..fa1e3c0bd7 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateAndStatusNode.swift @@ -9,7 +9,6 @@ import TelegramPresentationData import AccountContext import AppBundle import ReactionButtonListComponent -import WebPBinding import ReactionImageComponent private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { @@ -672,11 +671,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { }, reactions: arguments.reactions.map { reaction in var centerAnimation: TelegramMediaFile? + var legacyIcon: TelegramMediaFile? if let availableReactions = arguments.availableReactions { for availableReaction in availableReactions.reactions { if availableReaction.value == reaction.value { centerAnimation = availableReaction.centerAnimation + legacyIcon = availableReaction.staticIcon break } } @@ -695,7 +696,8 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { return ReactionButtonsAsyncLayoutContainer.Reaction( reaction: ReactionButtonComponent.Reaction( value: reaction.value, - centerAnimation: centerAnimation + centerAnimation: centerAnimation, + legacyIcon: legacyIcon ), count: Int(reaction.count), peers: peers, @@ -808,12 +810,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode { item.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - item.node.isGestureEnabled = true let itemValue = item.value let itemNode = item.node - item.node.isGestureEnabled = arguments.canViewReactionList + item.node.isGestureEnabled = true + let canViewReactionList = arguments.canViewReactionList + item.node.activateAfterCompletion = !canViewReactionList item.node.activated = { [weak itemNode] gesture, _ in - guard let strongSelf = self else { + guard let strongSelf = self, canViewReactionList else { return } guard let itemNode = itemNode else { diff --git a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift index 9fb81ef209..c26597b4e8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReactionsFooterContentNode.swift @@ -141,11 +141,13 @@ final class MessageReactionButtonsNode: ASDisplayNode { }, reactions: reactions.reactions.map { reaction in var centerAnimation: TelegramMediaFile? + var legacyIcon: TelegramMediaFile? if let availableReactions = availableReactions { for availableReaction in availableReactions.reactions { if availableReaction.value == reaction.value { centerAnimation = availableReaction.centerAnimation + legacyIcon = availableReaction.staticIcon break } } @@ -170,7 +172,8 @@ final class MessageReactionButtonsNode: ASDisplayNode { return ReactionButtonsAsyncLayoutContainer.Reaction( reaction: ReactionButtonComponent.Reaction( value: reaction.value, - centerAnimation: centerAnimation + centerAnimation: centerAnimation, + legacyIcon: legacyIcon ), count: Int(reaction.count), peers: peers, @@ -308,12 +311,17 @@ final class MessageReactionButtonsNode: ASDisplayNode { let itemValue = item.value let itemNode = item.node - item.node.isGestureEnabled = canViewMessageReactionList(message: message) + item.node.isGestureEnabled = true + let canViewReactionList = canViewMessageReactionList(message: message) + item.node.activateAfterCompletion = !canViewReactionList item.node.activated = { [weak itemNode] gesture, _ in guard let strongSelf = self, let itemNode = itemNode else { gesture.cancel() return } + if !canViewReactionList { + return + } strongSelf.openReactionPreview?(gesture, itemNode.containerNode, itemValue) } item.node.additionalActivationProgressLayer = itemMaskView.layer