diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift index 157a9bee57..949fe5450d 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPasswordEntryController.swift @@ -7,64 +7,145 @@ import SwiftSignalKit import TelegramPresentationData import AccountContext -private struct BotCheckoutPasswordAlertAction { - public let title: String - public let action: () -> Void +private final class BotCheckoutPassworInputFieldNode: ASDisplayNode, UITextFieldDelegate { + private var theme: PresentationTheme + private let backgroundNode: ASImageNode + private let textInputNode: TextFieldNode + private let placeholderNode: ASTextNode - public init(title: String, action: @escaping () -> Void) { - self.title = title - self.action = action - } -} - -private final class BotCheckoutPasswordAlertActionNode: HighlightableButtonNode { - private let backgroundNode: ASDisplayNode + var updateHeight: (() -> Void)? + var complete: (() -> Void)? + var textChanged: ((String) -> Void)? - let action: BotCheckoutPasswordAlertAction + private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) + private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) - init(theme: PresentationTheme, action: BotCheckoutPasswordAlertAction) { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = theme.actionSheet.opaqueItemHighlightedBackgroundColor - self.backgroundNode.alpha = 0.0 - - self.action = action - - super.init() - - self.setTitle(action.title, with: Font.regular(17.0), with: theme.actionSheet.controlAccentColor, for: []) - self.setTitle(action.title, with: Font.regular(17.0), with: theme.actionSheet.disabledActionTextColor, for: [.disabled]) - - self.highligthedChanged = { [weak self] value in - if let strongSelf = self { - if value { - if strongSelf.backgroundNode.supernode == nil { - strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) - } - strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") - strongSelf.backgroundNode.alpha = 1.0 - } else if !strongSelf.backgroundNode.alpha.isZero { - strongSelf.backgroundNode.alpha = 0.0 - strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) - } - } + var text: String { + get { + return self.textInputNode.textField.text ?? "" + } + set { + self.textInputNode.textField.text = newValue + self.placeholderNode.isHidden = !newValue.isEmpty } } - override func didLoad() { - super.didLoad() - - self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + var placeholder: String = "" { + didSet { + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + } } - @objc func pressed() { - self.action.action() + init(theme: PresentationTheme, placeholder: String) { + self.theme = theme + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + + self.textInputNode = TextFieldNode() + self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor] + self.textInputNode.textField.clipsToBounds = true + self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + self.textInputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance + self.textInputNode.textField.returnKeyType = .done + self.textInputNode.textField.isSecureTextEntry = true + self.textInputNode.textField.tintColor = theme.actionSheet.controlAccentColor + + self.placeholderNode = ASTextNode() + self.placeholderNode.isUserInteractionEnabled = false + self.placeholderNode.displaysAsynchronously = false + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + + super.init() + + self.textInputNode.textField.delegate = self + self.textInputNode.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textInputNode) + self.addSubnode(self.placeholderNode) } - override func layout() { - super.layout() + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme - self.backgroundNode.frame = self.bounds + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + self.textInputNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + self.textInputNode.textField.tintColor = self.theme.actionSheet.controlAccentColor + self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor] + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: width) + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) + transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) + + transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) + + return panelHeight + } + + func activateInput() { + self.textInputNode.becomeFirstResponder() + } + + func deactivateInput() { + self.textInputNode.resignFirstResponder() + } + + func shake() { + self.layer.addShakeAnimation() + } + + @objc func textDidChange() { + self.updateTextNodeText(animated: true) + self.textChanged?(self.textInputNode.textField.text ?? "") + self.placeholderNode.isHidden = !(self.textInputNode.textField.text ?? "").isEmpty + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if text == "\n" { + self.complete?() + return false + } + return true + } + + private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) + + return min(61.0, max(33.0, unboundTextFieldHeight)) + } + + private func updateTextNodeText(animated: Bool) { + let backgroundInsets = self.backgroundInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) + + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight?() + } + } + + @objc func clearPressed() { + self.textInputNode.textField.text = nil + self.deactivateInput() } } @@ -78,14 +159,13 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { private let textNode: ASTextNode private let actionNodesSeparator: ASDisplayNode - private let actionNodes: [BotCheckoutPasswordAlertActionNode] + private let actionNodes: [TextAlertContentActionNode] private let actionVerticalSeparators: [ASDisplayNode] - private let cancelActionNode: BotCheckoutPasswordAlertActionNode - private let doneActionNode: BotCheckoutPasswordAlertActionNode + private let cancelActionNode: TextAlertContentActionNode + private let doneActionNode: TextAlertContentActionNode - private let textFieldNodeBackground: ASImageNode - private let textFieldNode: TextFieldNode + let inputFieldNode: BotCheckoutPassworInputFieldNode private var validLayout: CGSize? private var isVerifying = false @@ -99,6 +179,8 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { self.requiresBiometrics = requiresBiometrics self.completion = completion + let alertTheme = AlertControllerTheme(presentationTheme: theme, fontSize: .regular) + let titleNode = ASTextNode() titleNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Title, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) titleNode.displaysAsynchronously = false @@ -112,16 +194,18 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { self.textNode.displaysAsynchronously = false self.textNode.isUserInteractionEnabled = false + self.inputFieldNode = BotCheckoutPassworInputFieldNode(theme: theme, placeholder: passwordTip ?? "") + self.actionNodesSeparator = ASDisplayNode() self.actionNodesSeparator.isLayerBacked = true self.actionNodesSeparator.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor - self.cancelActionNode = BotCheckoutPasswordAlertActionNode(theme: theme, action: BotCheckoutPasswordAlertAction(title: strings.Common_Cancel, action: { + self.cancelActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: { cancel() })) var doneImpl: (() -> Void)? - self.doneActionNode = BotCheckoutPasswordAlertActionNode(theme: theme, action: BotCheckoutPasswordAlertAction(title: strings.Checkout_PasswordEntry_Pay, action: { + self.doneActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .defaultAction, title: strings.Checkout_PasswordEntry_Pay, action: { doneImpl?() })) @@ -138,26 +222,6 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { } self.actionVerticalSeparators = actionVerticalSeparators - self.textFieldNodeBackground = ASImageNode() - self.textFieldNodeBackground.displaysAsynchronously = false - self.textFieldNodeBackground.displayWithoutProcessing = true - self.textFieldNodeBackground.image = generateImage(CGSize(width: 4.0, height: 4.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.actionSheet.primaryTextColor.cgColor) - context.setLineWidth(UIScreenPixel) - context.stroke(CGRect(origin: CGPoint(), size: size)) - })?.stretchableImage(withLeftCapWidth: 2, topCapHeight: 2) - - self.textFieldNode = TextFieldNode() - self.textFieldNode.textField.textColor = theme.actionSheet.primaryTextColor - self.textFieldNode.textField.font = Font.regular(12.0) - self.textFieldNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(12.0)] - self.textFieldNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textFieldNode.textField.isSecureTextEntry = true - self.textFieldNode.textField.tintColor = theme.list.itemAccentColor - self.textFieldNode.textField.placeholder = passwordTip - - super.init() self.addSubnode(self.titleNode) @@ -173,11 +237,14 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { self.addSubnode(separatorNode) } - self.addSubnode(self.textFieldNodeBackground) - self.addSubnode(self.textFieldNode) - - self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) + self.addSubnode(self.inputFieldNode) + self.inputFieldNode.textChanged = { [weak self] _ in + if let strongSelf = self { + strongSelf.updateState() + } + } + self.updateState() doneImpl = { [weak self] in @@ -213,13 +280,11 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize) transition.updateFrame(node: self.textNode, frame: textFrame) - let inputHeight: CGFloat = 38.0 + let resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom + 46.0) - let resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom + inputHeight) - - let textFieldBackgroundFrame = CGRect(origin: CGPoint(x: insets.left, y: resultSize.height - inputHeight + 12.0 - actionsHeight - insets.bottom), size: CGSize(width: resultSize.width - insets.left - insets.right, height: 25.0)) - self.textFieldNodeBackground.frame = textFieldBackgroundFrame - self.textFieldNode.frame = textFieldBackgroundFrame.offsetBy(dx: 0.0, dy: 0.0).insetBy(dx: 4.0, dy: 0.0) + let inputFieldWidth = resultSize.width + let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: resultSize.height - 36.0 - actionsHeight - insets.bottom, width: resultSize.width, height: inputFieldHeight)) self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) @@ -250,7 +315,7 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { } if previousLayout == nil { - self.textFieldNode.textField.becomeFirstResponder() + self.inputFieldNode.activateInput() } return resultSize @@ -262,24 +327,15 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { private func updateState() { var enabled = true - - if self.isVerifying { + if self.isVerifying || self.inputFieldNode.text.isEmpty { enabled = false } - - if let text = self.textFieldNode.textField.text { - if text.isEmpty { - enabled = false - } - } else { - enabled = false - } - - self.doneActionNode.isEnabled = enabled + self.doneActionNode.actionEnabled = enabled } private func verify() { - guard let text = self.textFieldNode.textField.text, !text.isEmpty else { + let text = self.inputFieldNode.text + guard !text.isEmpty else { return } @@ -290,8 +346,7 @@ private final class BotCheckoutPasswordAlertContentNode: AlertContentNode { } }, error: { [weak self] _ in if let strongSelf = self { - strongSelf.textFieldNodeBackground.layer.addShakeAnimation() - strongSelf.textFieldNode.layer.addShakeAnimation() + strongSelf.inputFieldNode.shake() strongSelf.hapticFeedback.error() strongSelf.isVerifying = false strongSelf.updateState() diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorGenericToolView.m b/submodules/LegacyComponents/Sources/TGPhotoEditorGenericToolView.m index 6e0f0d7883..4b5ad1d166 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorGenericToolView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorGenericToolView.m @@ -45,6 +45,7 @@ _explicit = explicit; _sliderView = [[TGPhotoEditorSliderView alloc] initWithFrame:CGRectZero]; + _sliderView.enablePanHandling = true; if (editorItem.segmented) _sliderView.positionsCount = (NSInteger)editorItem.maximumValue + 1; _sliderView.minimumValue = editorItem.minimumValue; diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m b/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m index efcc66f445..39154218cd 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorSliderView.m @@ -68,6 +68,7 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f; _panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; _panGestureRecognizer.enabled = false; + _panGestureRecognizer.delegate = self; [self addGestureRecognizer:_panGestureRecognizer]; _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; @@ -531,6 +532,19 @@ const CGFloat TGPhotoEditorSliderViewInternalMargin = 7.0f; } } +- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer == _panGestureRecognizer) { + CGPoint velocity = [gestureRecognizer velocityInView:gestureRecognizer.view]; + if (ABS(velocity.x) > ABS(velocity.y)) { + return true; + } else { + return false; + } + } + return true; +} + + - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)__unused event { if (!_enablePanHandling) { diff --git a/submodules/LegacyComponents/Sources/TGPhotoToolsController.m b/submodules/LegacyComponents/Sources/TGPhotoToolsController.m index 778c836b45..6a635517e7 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoToolsController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoToolsController.m @@ -266,6 +266,7 @@ const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize _portraitCollectionView.toolsDataSource = self; _portraitCollectionView.interactionBegan = _interactionBegan; _portraitCollectionView.interactionEnded = _interactionEnded; + _portraitCollectionView.canCancelContentTouches = true; [_portraitToolsWrapperView addSubview:_portraitCollectionView]; if (!TGIsPad()) @@ -278,6 +279,7 @@ const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize _landscapeCollectionView.toolsDataSource = self; _landscapeCollectionView.interactionBegan = _interactionBegan; _landscapeCollectionView.interactionEnded = _interactionEnded; + _landscapeCollectionView.canCancelContentTouches = true; [_landscapeToolsWrapperView addSubview:_landscapeCollectionView]; } diff --git a/submodules/TabBarUI/Sources/TabBarNode.swift b/submodules/TabBarUI/Sources/TabBarNode.swift index 9a9cc97ce5..ce69593e2e 100644 --- a/submodules/TabBarUI/Sources/TabBarNode.swift +++ b/submodules/TabBarUI/Sources/TabBarNode.swift @@ -93,6 +93,17 @@ private final class TabBarItemNode: ASDisplayNode { var contentWidth: CGFloat? var isSelected: Bool = false + let ringImageNode: ASImageNode + var ringColor: UIColor? { + didSet { + if let ringColor = self.ringColor { + self.ringImageNode.image = generateCircleImage(diameter: 29.0, lineWidth: 1.0, color: ringColor, backgroundColor: nil) + } else { + self.ringImageNode.image = nil + } + } + } + var swiped: ((TabBarItemSwipeDirection) -> Void)? var pointerInteraction: PointerInteraction? @@ -101,6 +112,11 @@ private final class TabBarItemNode: ASDisplayNode { self.extractedContainerNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() + self.ringImageNode = ASImageNode() + self.ringImageNode.isUserInteractionEnabled = false + self.ringImageNode.displayWithoutProcessing = true + self.ringImageNode.displaysAsynchronously = false + self.imageNode = ASImageNode() self.imageNode.isUserInteractionEnabled = false self.imageNode.displayWithoutProcessing = true @@ -136,6 +152,7 @@ private final class TabBarItemNode: ASDisplayNode { self.isAccessibilityElement = true + self.extractedContainerNode.contentNode.addSubnode(self.ringImageNode) self.extractedContainerNode.contentNode.addSubnode(self.textImageNode) self.extractedContainerNode.contentNode.addSubnode(self.imageNode) self.extractedContainerNode.contentNode.addSubnode(self.animationContainerNode) @@ -150,6 +167,7 @@ private final class TabBarItemNode: ASDisplayNode { guard let strongSelf = self else { return } + transition.updateAlpha(node: strongSelf.ringImageNode, alpha: isExtracted ? 0.0 : 1.0) transition.updateAlpha(node: strongSelf.imageNode, alpha: isExtracted ? 0.0 : 1.0) transition.updateAlpha(node: strongSelf.animationNode, alpha: isExtracted ? 0.0 : 1.0) transition.updateAlpha(node: strongSelf.textImageNode, alpha: isExtracted ? 0.0 : 1.0) @@ -441,6 +459,12 @@ class TabBarNode: ASDisplayNode { }, swipeAction: { [weak self] direction in self?.swipeAction(i, direction) }) + if item.item.ringSelection { + node.ringColor = self.theme.tabBarSelectedIconColor + } else { + node.ringColor = nil + } + if let selectedIndex = self.selectedIndex, selectedIndex == i { let (textImage, contentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered) let (image, imageContentWidth): (UIImage, CGFloat) @@ -507,6 +531,12 @@ class TabBarNode: ASDisplayNode { self.centered = self.theme.tabBarTextColor == .clear + if item.item.ringSelection { + node.ringColor = self.theme.tabBarSelectedIconColor + } else { + node.ringColor = nil + } + let previousImageSize = node.imageNode.image?.size ?? CGSize() let previousTextImageSize = node.textImageNode.image?.size ?? CGSize() if let selectedIndex = self.selectedIndex, selectedIndex == index { @@ -524,7 +554,11 @@ class TabBarNode: ASDisplayNode { node.animationNode.setOverlayColor(self.theme.tabBarSelectedIconColor, replace: true, animated: false) node.animationNode.updateLayout(size: CGSize(width: 51.0, height: 51.0)) } else { - (image, imageContentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered) + if item.item.ringSelection { + (image, imageContentWidth) = (item.item.selectedImage ?? UIImage(), item.item.selectedImage?.size.width ?? 0.0) + } else { + (image, imageContentWidth) = tabBarItemImage(item.item.selectedImage, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarSelectedIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered) + } node.animationNode.isHidden = true node.animationNode.visibility = false @@ -539,9 +573,22 @@ class TabBarNode: ASDisplayNode { node.contextImageNode.image = contextImage node.contentWidth = max(contentWidth, imageContentWidth) node.isSelected = true + + ContainedViewLayoutTransition.animated(duration: 0.35, curve: .easeInOut).updateTransformScale(node: node.ringImageNode, scale: 1.0, delay: 0.1) + node.imageNode.layer.animateScale(from: 1.0, to: 0.87, duration: 0.1, removeOnCompletion: false, completion: { [weak node] _ in + node?.imageNode.layer.animateScale(from: 0.87, to: 1.0, duration: 0.35, removeOnCompletion: false, completion: { [weak node] _ in + node?.imageNode.layer.removeAllAnimations() + }) + }) } else { let (textImage, contentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered) - let (image, imageContentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered) + + let (image, imageContentWidth): (UIImage, CGFloat) + if item.item.ringSelection { + (image, imageContentWidth) = (item.item.image ?? UIImage(), item.item.image?.size.width ?? 0.0) + } else { + (image, imageContentWidth) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered) + } let (contextTextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarExtractedTextColor, horizontal: self.horizontal, imageMode: false, centered: self.centered) let (contextImage, _) = tabBarItemImage(item.item.image, title: item.item.title ?? "", backgroundColor: .clear, tintColor: self.theme.tabBarExtractedIconColor, horizontal: self.horizontal, imageMode: true, centered: self.centered) @@ -556,6 +603,8 @@ class TabBarNode: ASDisplayNode { node.contextImageNode.image = contextImage node.contentWidth = max(contentWidth, imageContentWidth) node.isSelected = false + + ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateTransformScale(node: node.ringImageNode, scale: 0.5) } let updatedImageSize = node.imageNode.image?.size ?? CGSize() @@ -647,18 +696,33 @@ class TabBarNode: ASDisplayNode { node.containerNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size) node.hitTestSlop = UIEdgeInsets(top: -3.0, left: -horizontalHitTestInset, bottom: -3.0, right: -horizontalHitTestInset) node.containerNode.hitTestSlop = UIEdgeInsets(top: -3.0, left: -horizontalHitTestInset, bottom: -3.0, right: -horizontalHitTestInset) - node.imageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size) + if node.ringColor == nil { + node.imageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size) + } node.textImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size) node.contextImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size) node.contextTextImageNode.frame = CGRect(origin: CGPoint(), size: nodeFrame.size) - + let scaleFactor: CGFloat = horizontal ? 0.8 : 1.0 node.animationContainerNode.subnodeTransform = CATransform3DMakeScale(scaleFactor, scaleFactor, 1.0) let animationOffset: CGPoint = self.tabBarItems[i].item.animationOffset + let ringImageFrame: CGRect + let imageFrame: CGRect if horizontal { node.animationNode.frame = CGRect(origin: CGPoint(x: -10.0 - UIScreenPixel, y: -4.0 - UIScreenPixel), size: CGSize(width: 51.0, height: 51.0)) + ringImageFrame = CGRect(origin: CGPoint(x: UIScreenPixel, y: 5.0 + UIScreenPixel), size: CGSize(width: 23.0, height: 23.0)) + imageFrame = ringImageFrame.insetBy(dx: -1.0 + UIScreenPixel, dy: -1.0 + UIScreenPixel) } else { node.animationNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((nodeSize.width - 51.0) / 2.0), y: -10.0 - UIScreenPixel).offsetBy(dx: animationOffset.x, dy: animationOffset.y), size: CGSize(width: 51.0, height: 51.0)) + ringImageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((nodeSize.width - 29.0) / 2.0), y: 1.0), size: CGSize(width: 29.0, height: 29.0)) + imageFrame = ringImageFrame.insetBy(dx: -1.0, dy: -1.0) + } + node.ringImageNode.bounds = CGRect(origin: CGPoint(), size: ringImageFrame.size) + node.ringImageNode.position = ringImageFrame.center + + if node.ringColor != nil { + node.imageNode.bounds = CGRect(origin: CGPoint(), size: imageFrame.size) + node.imageNode.position = imageFrame.center } if container.badgeValue != container.appliedBadgeValue { diff --git a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift index a9e16035d8..3c707d6839 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -367,7 +367,12 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { self.expandBackgroundNode.image = generateExpandBackground(size: expandBackgroundFrame.size, color: presentationData.theme.list.itemBlocksBackgroundColor) transition.updateFrame(node: self.labelNode, frame: labelFrame) - transition.updateFrame(node: self.textNode, frame: textFrame) + + var textTransition = transition + if self.textNode.frame.size != textFrame.size { + textTransition = .immediate + } + textTransition.updateFrame(node: self.textNode, frame: textFrame) let height = labelSize.height + 3.0 + textSize.height + 22.0 diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index b1fbb5c976..85bb7d78a0 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2010,7 +2010,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { var requestAvatarExpansion: ((Bool, [AvatarGalleryEntry], AvatarGalleryEntry?, (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?) -> Void)? var requestOpenAvatarForEditing: ((Bool) -> Void)? var cancelUpload: (() -> Void)? - var requestUpdateLayout: (() -> Void)? + var requestUpdateLayout: ((Bool) -> Void)? var animateOverlaysFadeIn: (() -> Void)? var displayAvatarContextMenu: ((ASDisplayNode, ContextGesture?) -> Void)? @@ -2100,7 +2100,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { super.init() requestUpdateLayoutImpl = { [weak self] in - self?.requestUpdateLayout?() + self?.requestUpdateLayout?(false) } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 17a2608ac0..8633cccb67 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -460,7 +460,7 @@ private final class PeerInfoInteraction { let performMemberAction: (PeerInfoMember, PeerInfoMemberAction) -> Void let openPeerInfoContextMenu: (PeerInfoContextSubject, ASDisplayNode) -> Void let performBioLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void - let requestLayout: () -> Void + let requestLayout: (Bool) -> Void let openEncryptionKey: () -> Void let openSettings: (PeerInfoSettingsSection) -> Void let switchToAccount: (AccountRecordId) -> Void @@ -502,7 +502,7 @@ private final class PeerInfoInteraction { performMemberAction: @escaping (PeerInfoMember, PeerInfoMemberAction) -> Void, openPeerInfoContextMenu: @escaping (PeerInfoContextSubject, ASDisplayNode) -> Void, performBioLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, - requestLayout: @escaping () -> Void, + requestLayout: @escaping (Bool) -> Void, openEncryptionKey: @escaping () -> Void, openSettings: @escaping (PeerInfoSettingsSection) -> Void, switchToAccount: @escaping (AccountRecordId) -> Void, @@ -864,7 +864,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese }, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.phone(formattedPhone), sourceNode) }, requestLayout: { - interaction.requestLayout() + interaction.requestLayout(false) })) } if let username = user.username { @@ -875,21 +875,21 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese }, iconAction: { interaction.openQrCode() }, requestLayout: { - interaction.requestLayout() + interaction.requestLayout(false) })) } if let cachedData = data.cachedData as? CachedUserData { if user.isFake { items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_FakeBotWarning : presentationData.strings.UserInfo_FakeUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { - interaction.requestLayout() + interaction.requestLayout(false) })) } else if user.isScam { items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { - interaction.requestLayout() + interaction.requestLayout(false) })) } else if let about = cachedData.about, !about.isEmpty { items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { - interaction.requestLayout() + interaction.requestLayout(false) })) } } @@ -982,7 +982,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese }, iconAction: { interaction.openQrCode() }, requestLayout: { - interaction.requestLayout() + interaction.requestLayout(false) })) } if let cachedData = data.cachedData as? CachedChannelData { @@ -1011,7 +1011,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese enabledEntities = enabledPrivateBioEntities } items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { - interaction.requestLayout() + interaction.requestLayout(true) })) } @@ -1056,7 +1056,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese if let aboutText = aboutText { items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { - interaction.requestLayout() + interaction.requestLayout(true) })) } } @@ -1776,8 +1776,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate performBioLinkAction: { [weak self] action, item in self?.performBioLinkAction(action: action, item: item) }, - requestLayout: { [weak self] in - self?.requestLayout() + requestLayout: { [weak self] animated in + self?.requestLayout(animated: animated) }, openEncryptionKey: { [weak self] in self?.openEncryptionKey() @@ -2571,12 +2571,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate navigationBar.layer.animateAlpha(from: 0.0, to: navigationBar.alpha, duration: 0.25) } - self.headerNode.requestUpdateLayout = { [weak self] in + self.headerNode.requestUpdateLayout = { [weak self] animated in guard let strongSelf = self else { return } if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: animated ? .animated(duration: 0.35, curve: .slide) : .immediate, additive: false) } } @@ -5630,8 +5630,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate self.context.sharedContext.handleTextLinkAction(context: self.context, peerId: peer.id, navigateDisposable: self.resolveUrlDisposable, controller: controller, action: action, itemLink: item) } - private func requestLayout() { - self.headerNode.requestUpdateLayout?() + private func requestLayout(animated: Bool = false) { + self.headerNode.requestUpdateLayout?(animated) } private func openDeletePeer() { @@ -7763,18 +7763,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { if let signal = peerAvatarImage(account: primary.0, peerReference: PeerReference(primary.1._asPeer()), authorOfMessage: nil, representation: primary.1.profileImageRepresentations.first, displayDimensions: size, inset: 3.0, emptyColor: nil, synchronousLoad: false) { return signal |> map { imageVersions -> (UIImage, UIImage)? in - let image = imageVersions?.0 - if let image = image, let selectedImage = generateImage(size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.draw(image.cgImage!, in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - context.setLineWidth(1.0) - context.setStrokeColor(primary.2.rootController.tabBar.selectedIconColor.cgColor) - context.strokeEllipse(in: CGRect(x: 1.5, y: 1.5, width: 28.0, height: 28.0)) - }) { - return (image.withRenderingMode(.alwaysOriginal), selectedImage.withRenderingMode(.alwaysOriginal)) + if let image = imageVersions?.0 { + return (image.withRenderingMode(.alwaysOriginal), image.withRenderingMode(.alwaysOriginal)) } else { return nil } @@ -7792,22 +7782,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { drawPeerAvatarLetters(context: context, size: CGSize(width: size.width - inset * 2.0, height: size.height - inset * 2.0), font: avatarFont, letters: displayLetters, peerId: primary.1.id) })?.withRenderingMode(.alwaysOriginal) - - let selectedImage = generateImage(size, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - if let cgImage = image?.cgImage { - context.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - } - context.setLineWidth(1.0) - context.setStrokeColor(primary.2.rootController.tabBar.selectedIconColor.cgColor) - context.strokeEllipse(in: CGRect(x: 1.5, y: 1.5, width: 28.0, height: 28.0)) - })?.withRenderingMode(.alwaysOriginal) - - if let image = image, let selectedImage = selectedImage { - subscriber.putNext((image, selectedImage)) + if let image = image { + subscriber.putNext((image, image)) } else { subscriber.putNext(nil) } @@ -7884,6 +7860,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { strongSelf.tabBarItem.image = image strongSelf.tabBarItem.selectedImage = selectedImage strongSelf.tabBarItem.animationName = isAvatar ? nil : "TabSettings" + strongSelf.tabBarItem.ringSelection = isAvatar strongSelf.tabBarItem.badgeValue = badgeValue } }) diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.h index d74520c5d6..62d1b010e3 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.h @@ -50,5 +50,6 @@ NSInteger UITabBarItem_addSetBadgeListener(UITabBarItem * _Nonnull item, UITabBa @property (nonatomic, strong) NSString * _Nullable animationName; @property (nonatomic, assign) CGPoint animationOffset; +@property (nonatomic, assign) bool ringSelection; @end diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.m index bac5ab17db..27d3b068e8 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UINavigationItem+Proxy.m @@ -18,6 +18,7 @@ static const void *setBadgeListenerBagKey = &setBadgeListenerBagKey; static const void *badgeKey = &badgeKey; static const void *animationNameKey = &animationNameKey; static const void *animationOffsetKey = &animationOffsetKey; +static const void *ringSelectionKey = &ringSelectionKey; @implementation UINavigationItem (Proxy) @@ -419,4 +420,12 @@ NSInteger UITabBarItem_addSetBadgeListener(UITabBarItem *item, UITabBarItemSetBa return ((NSValue *)[self associatedObjectForKey:animationOffsetKey]).CGPointValue; } +- (void)setRingSelection:(bool)ringSelection { + [self setAssociatedObject:@(ringSelection) forKey:ringSelectionKey]; +} + +- (bool)ringSelection { + return ((NSNumber *)[self associatedObjectForKey:ringSelectionKey]).boolValue; +} + @end