diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 620bf03962..966bb8418a 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -440,7 +440,7 @@ public protocol SharedAccountContext: class { func openChatMessage(_ params: OpenChatMessageParams) -> Bool func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, account: Account, chatLocation: ChatLocation, tagMask: MessageTags?) -> Signal<(MessageIndex?, Bool), NoError> func makeOverlayAudioPlayerController(context: AccountContext, peerId: PeerId, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController - func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode) -> ViewController? + func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool) -> ViewController? func makeDeviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController func makePeersNearbyController(context: AccountContext) -> ViewController func makeComposeController(context: AccountContext) -> ViewController diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 8de824eba8..1b87df0834 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -149,7 +149,7 @@ public final class CallListController: ViewController { let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in - if let strongSelf = self, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .calls(messages: messages)) { + if let strongSelf = self, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .calls(messages: messages), avatarInitiallyExpanded: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) } }) diff --git a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift index 27c2014190..3578b12212 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityContentNode.swift @@ -95,6 +95,15 @@ public class ChatTitleActivityContentNode: ASDisplayNode { self.textNode.attributedText = text } + func makeCopy() -> ASDisplayNode { + let node = ASDisplayNode() + let textNode = self.textNode.makeCopy() + textNode.frame = self.textNode.frame + node.addSubnode(textNode) + node.frame = self.frame + return node + } + public func animateOut(to: ChatTitleActivityNodeState, style: ChatTitleActivityAnimationStyle, completion: @escaping () -> Void) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration, removeOnCompletion: false, completion: { _ in completion() diff --git a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift index c345f428bb..2ccf9a29f5 100644 --- a/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift +++ b/submodules/ChatTitleActivityNode/Sources/ChatTitleActivityNode.swift @@ -61,6 +61,15 @@ public class ChatTitleActivityNode: ASDisplayNode { super.init() } + public func makeCopy() -> ASDisplayNode { + let node = ASDisplayNode() + if let contentNode = self.contentNode { + node.addSubnode(contentNode.makeCopy()) + } + node.frame = self.frame + return node + } + public func transitionToState(_ state: ChatTitleActivityNodeState, animation: ChatTitleActivityAnimationStyle = .crossfade, completion: @escaping () -> Void = {}) -> Bool { if self.state != state { let currentState = self.state diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 7dcfc4b716..087fdfa678 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -530,7 +530,7 @@ public class ContactsController: ViewController { return } if let peer = peer { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } } else { diff --git a/submodules/Display/Display/ContainedViewLayoutTransition.swift b/submodules/Display/Display/ContainedViewLayoutTransition.swift index 3df59e854a..286e3070e1 100644 --- a/submodules/Display/Display/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Display/ContainedViewLayoutTransition.swift @@ -147,14 +147,17 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: - node.frame = frame + node.position = frame.center + node.bounds = CGRect(origin: node.bounds.origin, size: frame.size) if let completion = completion { completion(true) } - case .animated: - let previousFrame = node.frame - node.frame = frame - self.animatePositionAdditive(node: node, offset: CGPoint(x: previousFrame.midX - frame.midX, y: previousFrame.midY - frame.midY)) + case let .animated(duration, curve): + let previousBounds = node.bounds + let previousCenter = node.frame.center + node.position = frame.center + node.bounds = CGRect(origin: node.bounds.origin, size: frame.size) + self.animatePositionAdditive(node: node, offset: CGPoint(x: previousCenter.x - frame.midX, y: previousCenter.y - frame.midY)) } } } @@ -655,6 +658,40 @@ public extension ContainedViewLayoutTransition { } } + func updateSublayerTransformScaleAdditive(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { + if !node.isNodeLoaded { + node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) + completion?(true) + return + } + let t = node.layer.sublayerTransform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + if currentScale.isEqual(to: scale) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let t = node.layer.sublayerTransform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0) + node.layer.animate(from: -(scale - currentScale) as NSNumber, to: 0.0 as NSNumber, keyPath: "sublayerTransform.scale", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: true, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } + func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) diff --git a/submodules/Display/Display/ImmediateTextNode.swift b/submodules/Display/Display/ImmediateTextNode.swift index c8c70cc94f..38488ecda3 100644 --- a/submodules/Display/Display/ImmediateTextNode.swift +++ b/submodules/Display/Display/ImmediateTextNode.swift @@ -34,6 +34,13 @@ public class ImmediateTextNode: TextNode { public var tapAttributeAction: (([NSAttributedString.Key: Any]) -> Void)? public var longTapAttributeAction: (([NSAttributedString.Key: Any]) -> Void)? + public func makeCopy() -> TextNode { + let node = TextNode() + node.cachedLayout = self.cachedLayout + node.frame = self.frame + return node + } + public func updateLayout(_ constrainedSize: CGSize) -> CGSize { let makeLayout = TextNode.asyncLayout(self) let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets, textShadowColor: self.textShadowColor, textStroke: self.textStroke)) diff --git a/submodules/Display/Display/NavigationBar.swift b/submodules/Display/Display/NavigationBar.swift index e26a082850..6fc314f111 100644 --- a/submodules/Display/Display/NavigationBar.swift +++ b/submodules/Display/Display/NavigationBar.swift @@ -111,6 +111,9 @@ open class NavigationBar: ASDisplayNode { public var backPressed: () -> () = { } + public var userInfo: Any? + public var makeCustomTransitionNode: ((NavigationBar) -> CustomNavigationTransitionNode?)? + private var collapsed: Bool { get { return self.frame.size.height.isLess(than: 44.0) @@ -243,6 +246,8 @@ open class NavigationBar: ASDisplayNode { } } + public var customBackButtonText: String? + private var title: String? { didSet { if let title = self.title { @@ -261,7 +266,7 @@ open class NavigationBar: ASDisplayNode { } } - private var titleView: UIView? { + public private(set) var titleView: UIView? { didSet { if let oldValue = oldValue { oldValue.removeFromSuperview() @@ -377,7 +382,9 @@ open class NavigationBar: ASDisplayNode { case let .item(itemValue): self.previousItemListenerKey = itemValue.addSetTitleListener { [weak self] _, _ in if let strongSelf = self, let previousItem = strongSelf.previousItem, case let .item(itemValue) = previousItem { - if let backBarButtonItem = itemValue.backBarButtonItem { + if let customBackButtonText = strongSelf.customBackButtonText { + strongSelf.backButtonNode.updateManualText(customBackButtonText) + } else if let backBarButtonItem = itemValue.backBarButtonItem { strongSelf.backButtonNode.updateManualText(backBarButtonItem.title ?? "") } else { strongSelf.backButtonNode.updateManualText(itemValue.title ?? "") @@ -389,7 +396,9 @@ open class NavigationBar: ASDisplayNode { self.previousItemBackListenerKey = itemValue.addSetBackBarButtonItemListener { [weak self] _, _, _ in if let strongSelf = self, let previousItem = strongSelf.previousItem, case let .item(itemValue) = previousItem { - if let backBarButtonItem = itemValue.backBarButtonItem { + if let customBackButtonText = strongSelf.customBackButtonText { + strongSelf.backButtonNode.updateManualText(customBackButtonText) + } else if let backBarButtonItem = itemValue.backBarButtonItem { strongSelf.backButtonNode.updateManualText(backBarButtonItem.title ?? "") } else { strongSelf.backButtonNode.updateManualText(itemValue.title ?? "") @@ -505,7 +514,9 @@ open class NavigationBar: ASDisplayNode { self.leftButtonNode.removeFromSupernode() var backTitle: String? - if let leftBarButtonItem = item.leftBarButtonItem, leftBarButtonItem.backButtonAppearance { + if let customBackButtonText = self.customBackButtonText { + backTitle = customBackButtonText + } else if let leftBarButtonItem = item.leftBarButtonItem, leftBarButtonItem.backButtonAppearance { backTitle = leftBarButtonItem.title } else if let previousItem = self.previousItem { switch previousItem { @@ -589,12 +600,11 @@ open class NavigationBar: ASDisplayNode { self.updateAccessibilityElements() } - private let backButtonNode: NavigationButtonNode - private let badgeNode: NavigationBarBadgeNode - private let backButtonArrow: ASImageNode - private let leftButtonNode: NavigationButtonNode - private let rightButtonNode: NavigationButtonNode - + public let backButtonNode: NavigationButtonNode + public let badgeNode: NavigationBarBadgeNode + public let backButtonArrow: ASImageNode + public let leftButtonNode: NavigationButtonNode + public let rightButtonNode: NavigationButtonNode private var _transitionState: NavigationBarTransitionState? var transitionState: NavigationBarTransitionState? { @@ -694,6 +704,7 @@ open class NavigationBar: ASDisplayNode { self.leftButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor self.rightButtonNode.color = self.presentationData.theme.buttonColor self.rightButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor + self.rightButtonNode.rippleColor = self.presentationData.theme.primaryTextColor.withAlphaComponent(0.05) self.backButtonArrow.image = backArrowImage(color: self.presentationData.theme.buttonColor) if let title = self.title { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.presentationData.theme.primaryTextColor) @@ -768,6 +779,7 @@ open class NavigationBar: ASDisplayNode { self.leftButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor self.rightButtonNode.color = self.presentationData.theme.buttonColor self.rightButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor + self.rightButtonNode.rippleColor = self.presentationData.theme.primaryTextColor.withAlphaComponent(0.05) self.backButtonArrow.image = backArrowImage(color: self.presentationData.theme.buttonColor) if let title = self.title { self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.presentationData.theme.primaryTextColor) @@ -821,7 +833,7 @@ open class NavigationBar: ASDisplayNode { transition.updateFrame(node: self.stripeNode, frame: CGRect(x: 0.0, y: size.height, width: size.width, height: UIScreenPixel)) - let nominalHeight: CGFloat = self.collapsed ? 32.0 : defaultHeight + let nominalHeight: CGFloat = defaultHeight let contentVerticalOrigin = size.height - nominalHeight - expansionHeight var leftTitleInset: CGFloat = leftInset + 1.0 @@ -958,7 +970,7 @@ open class NavigationBar: ASDisplayNode { if let titleView = self.titleView { let titleSize = CGSize(width: max(1.0, size.width - max(leftTitleInset, rightTitleInset) * 2.0), height: nominalHeight) - let titleFrame = CGRect(origin: CGPoint(x: leftTitleInset, y: contentVerticalOrigin), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) titleView.frame = titleFrame if let titleView = titleView as? NavigationBarTitleView { @@ -996,7 +1008,7 @@ open class NavigationBar: ASDisplayNode { } } titleView.alpha = 1.0 - titleView.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize) + titleView.frame = titleFrame } } } @@ -1017,18 +1029,45 @@ open class NavigationBar: ASDisplayNode { } } - private func makeTransitionBackButtonNode(accentColor: UIColor) -> NavigationButtonNode? { + public func makeTransitionBackButtonNode(accentColor: UIColor) -> NavigationButtonNode? { if self.backButtonNode.supernode != nil { let node = NavigationButtonNode() node.updateManualText(self.backButtonNode.manualText) node.color = accentColor + if let (size, defaultHeight, _, _) = self.validLayout { + node.updateLayout(constrainedSize: CGSize(width: size.width, height: defaultHeight)) + node.frame = self.backButtonNode.frame + } return node } else { return nil } } - private func makeTransitionBackArrowNode(accentColor: UIColor) -> ASDisplayNode? { + public func makeTransitionRightButtonNode(accentColor: UIColor) -> NavigationButtonNode? { + if self.rightButtonNode.supernode != nil { + let node = NavigationButtonNode() + var items: [UIBarButtonItem] = [] + if let item = self.item { + if let rightBarButtonItems = item.rightBarButtonItems, !rightBarButtonItems.isEmpty { + items = rightBarButtonItems + } else if let rightBarButtonItem = item.rightBarButtonItem { + items = [rightBarButtonItem] + } + } + node.updateItems(items) + node.color = accentColor + if let (size, defaultHeight, _, _) = self.validLayout { + node.updateLayout(constrainedSize: CGSize(width: size.width, height: defaultHeight)) + node.frame = self.backButtonNode.frame + } + return node + } else { + return nil + } + } + + public func makeTransitionBackArrowNode(accentColor: UIColor) -> ASDisplayNode? { if self.backButtonArrow.supernode != nil { let node = ASImageNode() node.image = backArrowImage(color: accentColor) @@ -1041,7 +1080,7 @@ open class NavigationBar: ASDisplayNode { } } - private func makeTransitionBadgeNode() -> ASDisplayNode? { + public func makeTransitionBadgeNode() -> ASDisplayNode? { if self.badgeNode.supernode != nil && !self.badgeNode.isHidden { let node = NavigationBarBadgeNode(fillColor: self.presentationData.theme.badgeBackgroundColor, strokeColor: self.presentationData.theme.badgeStrokeColor, textColor: self.presentationData.theme.badgeTextColor) node.text = self.badgeNode.text diff --git a/submodules/Display/Display/NavigationBarBadge.swift b/submodules/Display/Display/NavigationBarBadge.swift index 089b88b0e3..348ff085c5 100644 --- a/submodules/Display/Display/NavigationBarBadge.swift +++ b/submodules/Display/Display/NavigationBarBadge.swift @@ -2,7 +2,7 @@ import Foundation import UIKit import AsyncDisplayKit -final class NavigationBarBadgeNode: ASDisplayNode { +public final class NavigationBarBadgeNode: ASDisplayNode { private var fillColor: UIColor private var strokeColor: UIColor private var textColor: UIColor @@ -19,7 +19,7 @@ final class NavigationBarBadgeNode: ASDisplayNode { } } - init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { + public init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { self.fillColor = fillColor self.strokeColor = strokeColor self.textColor = textColor @@ -48,7 +48,7 @@ final class NavigationBarBadgeNode: ASDisplayNode { self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) } - override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { let badgeSize = self.textNode.measure(constrainedSize) let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0) let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) diff --git a/submodules/Display/Display/NavigationButtonNode.swift b/submodules/Display/Display/NavigationButtonNode.swift index d246df1c1f..29a6d37f0e 100644 --- a/submodules/Display/Display/NavigationButtonNode.swift +++ b/submodules/Display/Display/NavigationButtonNode.swift @@ -53,6 +53,7 @@ private final class NavigationButtonItemNode: ASTextNode { } private var imageNode: ASImageNode? + private let imageRippleNode: ASImageNode private var _image: UIImage? public var image: UIImage? { @@ -61,18 +62,34 @@ private final class NavigationButtonItemNode: ASTextNode { } set(value) { _image = value - if let _ = value { + if let value = value { if self.imageNode == nil { let imageNode = ASImageNode() imageNode.displayWithoutProcessing = true imageNode.displaysAsynchronously = false self.imageNode = imageNode + if value.size == CGSize(width: 30.0, height: 30.0) { + if self.imageRippleNode.supernode == nil { + self.addSubnode(self.imageRippleNode) + self.imageRippleNode.image = generateFilledCircleImage(diameter: 30.0, color: self.rippleColor) + } + } else { + if self.imageRippleNode.supernode != nil { + self.imageRippleNode.image = nil + self.imageRippleNode.removeFromSupernode() + } + } + self.addSubnode(imageNode) } self.imageNode?.image = image } else if let imageNode = self.imageNode { imageNode.removeFromSupernode() self.imageNode = nil + if self.imageRippleNode.supernode != nil { + self.imageRippleNode.image = nil + self.imageRippleNode.removeFromSupernode() + } } self.invalidateCalculatedLayout() @@ -101,6 +118,14 @@ private final class NavigationButtonItemNode: ASTextNode { } } + public var rippleColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.05) { + didSet { + if self.imageRippleNode.image != nil { + self.imageRippleNode.image = generateFilledCircleImage(diameter: 30.0, color: self.rippleColor) + } + } + } + public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) { didSet { if let text = self._text { @@ -160,6 +185,11 @@ private final class NavigationButtonItemNode: ASTextNode { } override public init() { + self.imageRippleNode = ASImageNode() + self.imageRippleNode.displaysAsynchronously = false + self.imageRippleNode.displayWithoutProcessing = true + self.imageRippleNode.alpha = 0.0 + super.init() self.isAccessibilityElement = true @@ -183,7 +213,9 @@ private final class NavigationButtonItemNode: ASTextNode { } else if let imageNode = self.imageNode { let nodeSize = imageNode.image?.size ?? CGSize() let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height)) - imageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0) + 5.0, y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize) + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0) + 5.0, y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize) + imageNode.frame = imageFrame + self.imageRippleNode.frame = imageFrame return size } return superSize @@ -242,7 +274,15 @@ private final class NavigationButtonItemNode: ASTextNode { } if shouldChangeHighlight { - self.alpha = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0) + if let imageNode = self.imageNode { + let previousAlpha = self.imageRippleNode.alpha + self.imageRippleNode.alpha = highlighted ? 1.0 : 0.0 + if !highlighted { + self.imageRippleNode.layer.animateAlpha(from: previousAlpha, to: self.imageRippleNode.alpha, duration: 0.25) + } + } else { + self.alpha = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0) + } self.highlightChanged(highlighted) } } @@ -263,7 +303,7 @@ private final class NavigationButtonItemNode: ASTextNode { } -final class NavigationButtonNode: ASDisplayNode { +public final class NavigationButtonNode: ASDisplayNode { private var nodes: [NavigationButtonItemNode] = [] public var pressed: (Int) -> () = { _ in } @@ -279,6 +319,16 @@ final class NavigationButtonNode: ASDisplayNode { } } + public var rippleColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.05) { + didSet { + if !self.rippleColor.isEqual(oldValue) { + for node in self.nodes { + node.rippleColor = self.rippleColor + } + } + } + } + public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) { didSet { if !self.disabledColor.isEqual(oldValue) { @@ -296,7 +346,7 @@ final class NavigationButtonNode: ASDisplayNode { } } - override init() { + override public init() { super.init() self.isAccessibilityElement = false @@ -313,6 +363,7 @@ final class NavigationButtonNode: ASDisplayNode { } else { node = NavigationButtonItemNode() node.color = self.color + node.rippleColor = self.rippleColor node.highlightChanged = { [weak node, weak self] value in if let strongSelf = self, let node = node { if let index = strongSelf.nodes.firstIndex(where: { $0 === node }) { @@ -353,6 +404,7 @@ final class NavigationButtonNode: ASDisplayNode { } else { node = NavigationButtonItemNode() node.color = self.color + node.rippleColor = self.rippleColor node.highlightChanged = { [weak node, weak self] value in if let strongSelf = self, let node = node { if let index = strongSelf.nodes.firstIndex(where: { $0 === node }) { @@ -385,7 +437,7 @@ final class NavigationButtonNode: ASDisplayNode { } } - func updateLayout(constrainedSize: CGSize) -> CGSize { + public func updateLayout(constrainedSize: CGSize) -> CGSize { var nodeOrigin = CGPoint() var totalSize = CGSize() for node in self.nodes { diff --git a/submodules/Display/Display/NavigationTransitionCoordinator.swift b/submodules/Display/Display/NavigationTransitionCoordinator.swift index a3ea0997c5..f439a5bc64 100644 --- a/submodules/Display/Display/NavigationTransitionCoordinator.swift +++ b/submodules/Display/Display/NavigationTransitionCoordinator.swift @@ -15,12 +15,17 @@ private func generateShadow() -> UIImage? { context.setShadow(offset: CGSize(), blur: 16.0, color: UIColor(white: 0.0, alpha: 0.5).cgColor) context.fill(CGRect(origin: CGPoint(x: size.width, y: 0.0), size: CGSize(width: 16.0, height: 1.0))) }) - //return UIImage(named: "NavigationShadow", in: getAppBundle(), compatibleWith: nil)?.precomposed().resizableImage(withCapInsets: UIEdgeInsets(), resizingMode: .tile) } private let shadowImage = generateShadow() -class NavigationTransitionCoordinator { +public protocol CustomNavigationTransitionNode: ASDisplayNode { + func setup(topNavigationBar: NavigationBar, bottomNavigationBar: NavigationBar) + func update(containerSize: CGSize, fraction: CGFloat, transition: ContainedViewLayoutTransition) + func restore() +} + +final class NavigationTransitionCoordinator { private var _progress: CGFloat = 0.0 var progress: CGFloat { get { @@ -36,6 +41,7 @@ class NavigationTransitionCoordinator { private let bottomNavigationBar: NavigationBar? private let dimNode: ASDisplayNode private let shadowNode: ASImageNode + private let customTransitionNode: CustomNavigationTransitionNode? private let inlineNavigationBarTransition: Bool @@ -58,25 +64,43 @@ class NavigationTransitionCoordinator { self.shadowNode.displayWithoutProcessing = true self.shadowNode.image = shadowImage - if let topNavigationBar = topNavigationBar, let bottomNavigationBar = bottomNavigationBar, !topNavigationBar.isHidden, !bottomNavigationBar.isHidden, topNavigationBar.canTransitionInline, bottomNavigationBar.canTransitionInline, topNavigationBar.item?.leftBarButtonItem == nil { - var topFrame = topNavigationBar.view.convert(topNavigationBar.bounds, to: container.view) - var bottomFrame = bottomNavigationBar.view.convert(bottomNavigationBar.bounds, to: container.view) - topFrame.origin.x = 0.0 - bottomFrame.origin.x = 0.0 - self.inlineNavigationBarTransition = true// topFrame.equalTo(bottomFrame) + if let topNavigationBar = topNavigationBar, let bottomNavigationBar = bottomNavigationBar { + if let customTransitionNode = topNavigationBar.makeCustomTransitionNode?(bottomNavigationBar) { + self.inlineNavigationBarTransition = false + customTransitionNode.setup(topNavigationBar: topNavigationBar, bottomNavigationBar: bottomNavigationBar) + self.customTransitionNode = customTransitionNode + } else if let customTransitionNode = bottomNavigationBar.makeCustomTransitionNode?(topNavigationBar) { + self.inlineNavigationBarTransition = false + customTransitionNode.setup(topNavigationBar: topNavigationBar, bottomNavigationBar: bottomNavigationBar) + self.customTransitionNode = customTransitionNode + } else if !topNavigationBar.isHidden, !bottomNavigationBar.isHidden, topNavigationBar.canTransitionInline, bottomNavigationBar.canTransitionInline, topNavigationBar.item?.leftBarButtonItem == nil { + var topFrame = topNavigationBar.view.convert(topNavigationBar.bounds, to: container.view) + var bottomFrame = bottomNavigationBar.view.convert(bottomNavigationBar.bounds, to: container.view) + topFrame.origin.x = 0.0 + bottomFrame.origin.x = 0.0 + self.inlineNavigationBarTransition = true + self.customTransitionNode = nil + } else { + self.inlineNavigationBarTransition = false + self.customTransitionNode = nil + } } else { self.inlineNavigationBarTransition = false + self.customTransitionNode = nil } switch transition { - case .Push: - self.container.addSubnode(topNode) - case .Pop: - self.container.insertSubnode(bottomNode, belowSubnode: topNode) + case .Push: + self.container.addSubnode(topNode) + case .Pop: + self.container.insertSubnode(bottomNode, belowSubnode: topNode) } self.container.insertSubnode(self.dimNode, belowSubnode: topNode) - self.container.insertSubnode(self.shadowNode, belowSubnode: dimNode) + self.container.insertSubnode(self.shadowNode, belowSubnode: self.dimNode) + if let customTransitionNode = self.customTransitionNode { + self.container.addSubnode(customTransitionNode) + } self.maybeCreateNavigationBarTransition() self.updateProgress(0.0, transition: .immediate, completion: {}) @@ -91,10 +115,10 @@ class NavigationTransitionCoordinator { let position: CGFloat switch self.transition { - case .Push: - position = 1.0 - progress - case .Pop: - position = progress + case .Push: + position = 1.0 - progress + case .Pop: + position = progress } var dimInset: CGFloat = 0.0 @@ -119,10 +143,15 @@ class NavigationTransitionCoordinator { self.updateNavigationBarTransition(transition: transition) + if let customTransitionNode = self.customTransitionNode { + customTransitionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerSize.width, height: containerSize.height)) + customTransitionNode.update(containerSize: containerSize, fraction: position, transition: transition) + } + self.didUpdateProgress?(self.progress, transition, topFrame, bottomFrame) } - func updateNavigationBarTransition(transition: ContainedViewLayoutTransition) { + private func updateNavigationBarTransition(transition: ContainedViewLayoutTransition) { if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition { let position: CGFloat switch self.transition { @@ -178,6 +207,9 @@ class NavigationTransitionCoordinator { strongSelf.dimNode.removeFromSupernode() strongSelf.shadowNode.removeFromSupernode() + strongSelf.customTransitionNode?.restore() + strongSelf.customTransitionNode?.removeFromSupernode() + strongSelf.endNavigationBarTransition() if let currentCompletion = strongSelf.currentCompletion { @@ -195,6 +227,9 @@ class NavigationTransitionCoordinator { self.dimNode.removeFromSupernode() self.shadowNode.removeFromSupernode() + self.customTransitionNode?.restore() + self.customTransitionNode?.removeFromSupernode() + self.endNavigationBarTransition() if let currentCompletion = self.currentCompletion { @@ -209,6 +244,9 @@ class NavigationTransitionCoordinator { strongSelf.dimNode.removeFromSupernode() strongSelf.shadowNode.removeFromSupernode() + strongSelf.customTransitionNode?.restore() + strongSelf.customTransitionNode?.removeFromSupernode() + strongSelf.endNavigationBarTransition() if let currentCompletion = strongSelf.currentCompletion { @@ -228,6 +266,9 @@ class NavigationTransitionCoordinator { self.dimNode.removeFromSupernode() self.shadowNode.removeFromSupernode() + self.customTransitionNode?.restore() + self.customTransitionNode?.removeFromSupernode() + self.endNavigationBarTransition() if let currentCompletion = self.currentCompletion { diff --git a/submodules/Display/Display/TextNode.swift b/submodules/Display/Display/TextNode.swift index 4f8d2a301e..1e7268cdde 100644 --- a/submodules/Display/Display/TextNode.swift +++ b/submodules/Display/Display/TextNode.swift @@ -771,7 +771,7 @@ public final class TextAccessibilityOverlayNode: ASDisplayNode { } public class TextNode: ASDisplayNode { - public private(set) var cachedLayout: TextNodeLayout? + public internal(set) var cachedLayout: TextNodeLayout? override public init() { super.init() diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index c930ad926b..e15556a53a 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -1180,7 +1180,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self { - if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { strongSelf.getNavigationController()?.pushViewController(controller) } } diff --git a/submodules/PassportUI/Sources/SecureIdAuthController.swift b/submodules/PassportUI/Sources/SecureIdAuthController.swift index a21449e859..66ca2894ea 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthController.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthController.swift @@ -330,7 +330,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable guard let strongSelf = self else { return } - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } }) diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 30c25a1b5b..0692fc4ad2 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -61,7 +61,7 @@ public final class AvatarGalleryControllerPresentationArguments { } } -private func initialAvatarGalleryEntries(peer: Peer) -> [AvatarGalleryEntry]{ +private func initialAvatarGalleryEntries(peer: Peer) -> [AvatarGalleryEntry] { var initialEntries: [AvatarGalleryEntry] = [] if !peer.profileImageRepresentations.isEmpty, let peerReference = PeerReference(peer) { initialEntries.append(.topImage(peer.profileImageRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), nil)) @@ -70,26 +70,30 @@ private func initialAvatarGalleryEntries(peer: Peer) -> [AvatarGalleryEntry]{ } public func fetchedAvatarGalleryEntries(account: Account, peer: Peer) -> Signal<[AvatarGalleryEntry], NoError> { - return requestPeerPhotos(account: account, peerId: peer.id) - |> map { photos -> [AvatarGalleryEntry] in - var result: [AvatarGalleryEntry] = [] - let initialEntries = initialAvatarGalleryEntries(peer: peer) - if photos.isEmpty { - result = initialEntries - } else { - var index: Int32 = 0 - for photo in photos { - let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) - if result.isEmpty, let first = initialEntries.first { - result.append(.image(photo.image.reference, first.representations, peer, photo.date, indexData, photo.messageId)) - } else { - result.append(.image(photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.standalone(resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId)) + let initialEntries = initialAvatarGalleryEntries(peer: peer) + return Signal<[AvatarGalleryEntry], NoError>.single(initialEntries) + |> then( + requestPeerPhotos(account: account, peerId: peer.id) + |> map { photos -> [AvatarGalleryEntry] in + var result: [AvatarGalleryEntry] = [] + let initialEntries = initialAvatarGalleryEntries(peer: peer) + if photos.isEmpty { + result = initialEntries + } else { + var index: Int32 = 0 + for photo in photos { + let indexData = GalleryItemIndexData(position: index, totalCount: Int32(photos.count)) + if result.isEmpty, let first = initialEntries.first { + result.append(.image(photo.image.reference, first.representations, peer, photo.date, indexData, photo.messageId)) + } else { + result.append(.image(photo.image.reference, photo.image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.standalone(resource: $0.resource)) }), peer, photo.date, indexData, photo.messageId)) + } + index += 1 } - index += 1 } + return result } - return result - } + ) } public class AvatarGalleryController: ViewController, StandalonePresentableController { diff --git a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift index 59a2a57ad7..0750e10edc 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift @@ -366,7 +366,7 @@ public func channelBlacklistController(context: AccountContext, peerId: PeerId) } items.append(ActionSheetButtonItem(title: presentationData.strings.GroupRemoved_ViewUserInfo, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: participant.peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false) { pushControllerImpl?(infoController) } })) diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index 85c6dce8d6..dc6dc37ef0 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -450,7 +450,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> } })) }, openPeer: { peer in - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { pushControllerImpl?(controller) } }, inviteViaLink: { @@ -502,7 +502,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) -> return state.withUpdatedSearchingMembers(false) } }, openPeer: { peer, _ in - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { pushControllerImpl?(infoController) } }, pushController: { c in diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index 7615d720b0..05e6b55824 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -666,7 +666,7 @@ public func channelPermissionsController(context: AccountContext, peerId origina }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) }, openPeerInfo: { peer in - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { pushControllerImpl?(controller) } }, openKicked: { diff --git a/submodules/PeerInfoUI/Sources/GroupInfoController.swift b/submodules/PeerInfoUI/Sources/GroupInfoController.swift index c5c90866b8..8e3288a0aa 100644 --- a/submodules/PeerInfoUI/Sources/GroupInfoController.swift +++ b/submodules/PeerInfoUI/Sources/GroupInfoController.swift @@ -599,7 +599,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { })) } return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!, .standard), editing: editing, revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: enabled, selectable: selectable, sectionId: self.section, action: { - if let infoController = arguments.context.sharedContext.makePeerInfoController(context: arguments.context, peer: peer, mode: .generic), selectable { + if let infoController = arguments.context.sharedContext.makePeerInfoController(context: arguments.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false), selectable { arguments.pushController(infoController) } }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in @@ -2342,7 +2342,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: return state.withUpdatedSearchingMembers(false) } }, openPeer: { peer, _ in - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { arguments.pushController(infoController) } }, pushController: { c in diff --git a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift index d81c161b2f..b59ad0429d 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift @@ -259,7 +259,7 @@ public func blockedPeersController(context: AccountContext, blockedPeersContext: } })) }, openPeer: { peer in - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { pushControllerImpl?(controller) } }) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift index d889d5aac4..802c7bac02 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift @@ -112,7 +112,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { private var disabledOverlayNode: ASDisplayNode? private let maskNode: ASImageNode - private let avatarNode: AvatarNode + let avatarNode: AvatarNode private let titleNode: TextNode private let appNode: TextNode private let locationNode: TextNode diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index 627b6c3af5..130a649729 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -341,7 +341,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri return transaction.getPeer(peerId) } |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) else { + guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) else { return } pushControllerImpl?(controller) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 0c11f4342c..845a5b7daf 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -20,6 +20,7 @@ public enum PresentationResourceKey: Int32 { case navigationShareIcon case navigationSearchIcon case navigationCompactSearchIcon + case navigationMoreIcon case navigationAddIcon case navigationPlayerCloseButton diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift index b5982305bb..afc4cc9f0a 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift @@ -71,6 +71,19 @@ public struct PresentationResourcesRootController { }) } + public static func navigationMoreIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationMoreIcon.rawValue, { theme in + return generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.rootController.navigationBar.accentTextColor.cgColor) + let dotSize: CGFloat = 4.0 + context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: floor((size.height - dotSize) / 2.0)), size: CGSize(width: dotSize, height: dotSize))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 13.0, y: floor((size.height - dotSize) / 2.0)), size: CGSize(width: dotSize, height: dotSize))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 20.0, y: floor((size.height - dotSize) / 2.0)), size: CGSize(width: dotSize, height: dotSize))) + }) + }) + } + public static func navigationAddIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationAddIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddIcon"), color: theme.rootController.navigationBar.accentTextColor) diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/Contents.json new file mode 100644 index 0000000000..196b36b491 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_addmember.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/ic_pf_addmember.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/ic_pf_addmember.pdf new file mode 100644 index 0000000000..ad2274b415 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/ic_pf_addmember.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/Contents.json new file mode 100644 index 0000000000..94b8f3fdef --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_call.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/ic_pf_call.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/ic_pf_call.pdf new file mode 100644 index 0000000000..6fdc5ac345 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/ic_pf_call.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json index f8f827e40b..c1007d847a 100644 --- a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json @@ -2,15 +2,7 @@ "images" : [ { "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "ic_pf_message.pdf" } ], "info" : { diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/ic_pf_message.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/ic_pf_message.pdf new file mode 100644 index 0000000000..64fe2bdbd1 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/ic_pf_message.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/Contents.json new file mode 100644 index 0000000000..176c3d211d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_more.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/ic_pf_more.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/ic_pf_more.pdf new file mode 100644 index 0000000000..a105960756 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/ic_pf_more.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/Contents.json new file mode 100644 index 0000000000..61322e832d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_mute.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/ic_pf_mute.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/ic_pf_mute.pdf new file mode 100644 index 0000000000..2cabbef68d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/ic_pf_mute.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/Contents.json new file mode 100644 index 0000000000..29259c8539 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_unmute.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/ic_pf_unmute.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/ic_pf_unmute.pdf new file mode 100644 index 0000000000..617622f59a Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/ic_pf_unmute.pdf differ diff --git a/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift index 0d7cbb8267..30d69469e3 100644 --- a/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift @@ -48,6 +48,8 @@ final class ChatAvatarNavigationNode: ASDisplayNode { } } + var tapped: (() -> Void)? + override init() { self.containerNode = ContextControllerSourceNode() self.avatarNode = AvatarNode(font: normalFont) @@ -67,6 +69,9 @@ final class ChatAvatarNavigationNode: ASDisplayNode { } strongSelf.contextAction?(strongSelf.containerNode, gesture) } + + self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)) + self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)) } override func didLoad() { @@ -74,6 +79,14 @@ final class ChatAvatarNavigationNode: ASDisplayNode { self.view.isOpaque = false (self.view as? ChatAvatarNavigationNodeView)?.targetNode = self (self.view as? ChatAvatarNavigationNodeView)?.chatController = self.chatController + + self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.avatarTapGesture(_:)))) + } + + @objc private func avatarTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.tapped?() + } } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { @@ -85,7 +98,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode { } func onLayout() { - let bounds = self.bounds + /*let bounds = self.bounds if self.bounds.size.height.isLessThanOrEqualTo(26.0) { if !self.avatarNode.bounds.size.equalTo(bounds.size) { self.avatarNode.font = smallFont @@ -98,6 +111,6 @@ final class ChatAvatarNavigationNode: ASDisplayNode { } self.containerNode.frame = bounds.offsetBy(dx: 10.0, dy: 1.0) self.avatarNode.frame = bounds - } + }*/ } } diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 8e92f38297..ee1d46ff01 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -363,12 +363,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } super.init(context: context, navigationBarPresentationData: navigationBarPresentationData, mediaAccessoryPanelVisibility: mediaAccessoryPanelVisibility, locationBroadcastPanelSource: locationBroadcastPanelSource) - /*switch mode { - case .overlay: - self.navigationPresentation = .standaloneModal - default: - break - }*/ + self.navigationBar?.customBackButtonText = "" self.blocksBackgroundWhenInOverlay = true @@ -1871,67 +1866,43 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.controllerInteraction = controllerInteraction - self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder) - self.navigationItem.titleView = self.chatTitleView - self.chatTitleView?.pressed = { [weak self] in - if let strongSelf = self { - if strongSelf.chatLocation == .peer(strongSelf.context.account.peerId) { - strongSelf.effectiveNavigationController?.pushViewController(PeerMediaCollectionController(context: strongSelf.context, peerId: strongSelf.context.account.peerId)) - } else { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedTitlePanelContext { - if let index = $0.firstIndex(where: { - switch $0 { - case .chatInfo: - return true - default: - return false - } - }) { - var updatedContexts = $0 - updatedContexts.remove(at: index) - return updatedContexts - } else { - var updatedContexts = $0 - updatedContexts.append(.chatInfo) - return updatedContexts.sorted() - } - } - }) + var displayNavigationAvatar = false + if case let .peer(peerId) = chatLocation, peerId != context.account.peerId { + displayNavigationAvatar = true + self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId) + } + self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, displayAvatar: displayNavigationAvatar) + if let avatarNode = self.chatTitleView?.avatarNode { + avatarNode.chatController = self + avatarNode.contextAction = { [weak self] node, gesture in + guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, peer.smallProfileImage != nil else { + return } + let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in + }, synchronousLoad: true) + galleryController.setHintWillBePresentedInPreviewingContext(true) + + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in + f(.dismissWithoutContent) + self?.navigationButtonAction(.openChatInfo(expandAvatar: false)) + })) + ] + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + strongSelf.presentInGlobalOverlay(contextController) + } + avatarNode.tapped = { [weak self] in + self?.navigationButtonAction(.openChatInfo(expandAvatar: true)) } } - - let chatInfoButtonItem: UIBarButtonItem - switch chatLocation { - case .peer: - let avatarNode = ChatAvatarNavigationNode() - avatarNode.chatController = self - avatarNode.contextAction = { [weak self] node, gesture in - guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer, peer.smallProfileImage != nil else { - return - } - let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in - }, synchronousLoad: true) - galleryController.setHintWillBePresentedInPreviewingContext(true) - - let items: [ContextMenuItem] = [ - .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in - f(.dismissWithoutContent) - self?.navigationButtonAction(.openChatInfo) - })) - ] - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: galleryController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) - strongSelf.presentInGlobalOverlay(contextController) - } - chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)! - /*case .group: - chatInfoButtonItem = UIBarButtonItem(customDisplayNode: ChatMultipleAvatarsNavigationNode())!*/ + self.navigationItem.titleView = self.chatTitleView + self.chatTitleView?.pressed = { [weak self] in + self?.navigationButtonAction(.openChatInfo(expandAvatar: false)) } - chatInfoButtonItem.target = self - chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction) - chatInfoButtonItem.accessibilityLabel = self.presentationData.strings.Conversation_Info - self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: chatInfoButtonItem) + + let buttonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationMoreIcon(presentationInterfaceState.theme), style: .plain, target: self, action: #selector(self.rightNavigationButtonAction)) + //buttonItem.accessibilityLabel = strings.Conversation_Search + chatInfoNavigationButton = ChatNavigationButton(action: .toggleInfoPanel, buttonItem: buttonItem) self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in if let botStart = botStart, case .interactive = botStart.behavior { @@ -2010,7 +1981,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { if let peer = peerViewMainPeer(peerView) { strongSelf.chatTitleView?.titleContent = .peer(peerView: peerView, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages) - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: peer.isDeleted ? .deletedIcon : .none) + strongSelf.chatTitleView?.avatarNode?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: peer.isDeleted ? .deletedIcon : .none) (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil } @@ -3545,7 +3516,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: nil, keepStack: .always)) } }, openPeerInfo: { [weak self] in - self?.navigationButtonAction(.openChatInfo) + self?.navigationButtonAction(.openChatInfo(expandAvatar: false)) }, togglePeerNotifications: { [weak self] in if let strongSelf = self, case let .peer(peerId) = strongSelf.chatLocation { let _ = togglePeerMuted(account: strongSelf.context.account, peerId: peerId).start() @@ -5319,18 +5290,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) } - case .openChatInfo: + case let .openChatInfo(expandAvatar): switch self.chatLocationInfoData { - case let .peer(peerView): - self.navigationActionDisposable.set((peerView.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && !strongSelf.presentationInterfaceState.isNotAccessible { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + case let .peer(peerView): + self.navigationActionDisposable.set((peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peerView in + if let strongSelf = self, let peer = peerView.peers[peerView.peerId], peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && !strongSelf.presentationInterfaceState.isNotAccessible { + if peer.id == strongSelf.context.account.peerId { + strongSelf.effectiveNavigationController?.pushViewController(PeerMediaCollectionController(context: strongSelf.context, peerId: strongSelf.context.account.peerId)) + } else { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: expandAvatar) { strongSelf.effectiveNavigationController?.pushViewController(infoController) } } - })) + } + })) } case .search: self.interfaceInteraction?.beginMessageSearch(.everything, "") @@ -5538,6 +5513,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) })) } + case .toggleInfoPanel: + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedTitlePanelContext { + if let index = $0.firstIndex(where: { + switch $0 { + case .chatInfo: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } else { + var updatedContexts = $0 + updatedContexts.append(.chatInfo) + return updatedContexts.sorted() + } + } + }) } } @@ -7025,11 +7021,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.effectiveNavigationController?.pushViewController(controller) } - private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: Message?) { + private func openPeer(peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer, fromMessage: Message?, expandAvatar: Bool = false) { if case let .peer(currentPeerId) = self.chatLocation, peerId == currentPeerId { switch navigation { case .info: - self.navigationButtonAction(.openChatInfo) + self.navigationButtonAction(.openChatInfo(expandAvatar: expandAvatar)) case let .chat(textInputState, _): if let textInputState = textInputState { self.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -7061,7 +7057,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, let peer = peer { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: expandAvatar) { strongSelf.effectiveNavigationController?.pushViewController(infoController) } } @@ -7478,7 +7474,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { strongSelf.effectiveNavigationController?.pushViewController(infoController) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift index bf3eafbf57..3e0f9c33a0 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -6,13 +6,14 @@ import SyncCore import TelegramPresentationData import AccountContext -enum ChatNavigationButtonAction { - case openChatInfo +enum ChatNavigationButtonAction: Equatable { + case openChatInfo(expandAvatar: Bool) case clearHistory case clearCache case cancelMessageSelection case search case dismiss + case toggleInfoPanel } struct ChatNavigationButton: Equatable { diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift index 271975abbe..c3b087ae3b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift @@ -659,7 +659,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if peer is TelegramChannel, let navigationController = strongSelf.getNavigationController() { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: true)) } else { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { strongSelf.pushController(infoController) } } @@ -681,7 +681,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self { if let peer = peer { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { strongSelf.pushController(infoController) } } diff --git a/submodules/TelegramUI/TelegramUI/ChatTitleView.swift b/submodules/TelegramUI/TelegramUI/ChatTitleView.swift index 04ae85c66b..bc988e496b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatTitleView.swift +++ b/submodules/TelegramUI/TelegramUI/ChatTitleView.swift @@ -15,6 +15,7 @@ import PeerPresenceStatusManager import ChatTitleActivityNode import LocalizedPeerData import PhoneNumberFormat +import ChatTitleActivityNode enum ChatTitleContent { case peer(peerView: PeerView, onlineMemberCount: Int32?, isScheduledMessages: Bool) @@ -92,14 +93,16 @@ final class ChatTitleView: UIView, NavigationBarTitleView { private var nameDisplayOrder: PresentationPersonNameOrder private let contentContainer: ASDisplayNode - private let titleNode: ImmediateTextNode - private let titleLeftIconNode: ASImageNode - private let titleRightIconNode: ASImageNode - private let titleCredibilityIconNode: ASImageNode - private let activityNode: ChatTitleActivityNode + let titleNode: ImmediateTextNode + let titleLeftIconNode: ASImageNode + let titleRightIconNode: ASImageNode + let titleCredibilityIconNode: ASImageNode + let activityNode: ChatTitleActivityNode private let button: HighlightTrackingButtonNode + let avatarNode: ChatAvatarNavigationNode? + private var validLayout: (CGSize, CGRect)? private var titleLeftIcon: ChatTitleIcon = .none @@ -136,7 +139,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } else { statusNode = ChatTitleNetworkStatusNode(theme: self.theme) self.networkStatusNode = statusNode - self.insertSubview(statusNode.view, belowSubview: self.button.view) + self.insertSubview(statusNode.view, aboveSubview: self.contentContainer.view) } switch self.networkState { case .waitingForNetwork: @@ -451,7 +454,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } } - init(account: Account, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, displayAvatar: Bool) { self.account = account self.theme = theme self.strings = strings @@ -482,6 +485,11 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.activityNode = ChatTitleActivityNode() self.button = HighlightTrackingButtonNode() + if displayAvatar { + self.avatarNode = ChatAvatarNavigationNode() + } else { + self.avatarNode = nil + } super.init(frame: CGRect()) @@ -492,12 +500,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.contentContainer.addSubnode(self.titleNode) self.contentContainer.addSubnode(self.activityNode) self.addSubnode(self.button) + self.avatarNode.flatMap(self.contentContainer.addSubnode) self.presenceManager = PeerPresenceStatusManager(update: { [weak self] in self?.updateStatus() }) - self.button.addTarget(self, action: #selector(buttonPressed), forControlEvents: [.touchUpInside]) + self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) self.button.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { @@ -558,7 +567,6 @@ final class ChatTitleView: UIView, NavigationBarTitleView { let transition: ContainedViewLayoutTransition = .immediate - self.button.frame = clearBounds self.contentContainer.frame = clearBounds var leftIconWidth: CGFloat = 0.0 @@ -592,35 +600,35 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.titleRightIconNode.removeFromSupernode() } + var leftInset: CGFloat = 12.0 + if let avatarNode = self.avatarNode { + let avatarSize = CGSize(width: 37.0, height: 37.0) + let avatarFrame = CGRect(origin: CGPoint(x: leftInset + 10.0, y: floor((size.height - avatarSize.height) / 2.0)), size: avatarSize) + avatarNode.frame = avatarFrame + leftInset += avatarSize.width + 10.0 + 8.0 + } + + self.button.frame = CGRect(origin: CGPoint(x: leftInset - 20.0, y: 0.0), size: CGSize(width: clearBounds.width - leftInset, height: size.height)) + let titleSideInset: CGFloat = 3.0 if size.height > 40.0 { var titleSize = self.titleNode.updateLayout(CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0, height: size.height)) titleSize.width += credibilityIconWidth - let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .center) + let activitySize = self.activityNode.updateLayout(clearBounds.size, alignment: .left) let titleInfoSpacing: CGFloat = 0.0 var titleFrame: CGRect if activitySize.height.isZero { - titleFrame = CGRect(origin: CGPoint(x: floor((clearBounds.width - titleSize.width) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) - if titleFrame.size.width < size.width { - titleFrame.origin.x = -clearBounds.minX + floor((size.width - titleFrame.width) / 2.0) - } + titleFrame = CGRect(origin: CGPoint(x: leftInset + leftIconWidth, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) self.titleNode.frame = titleFrame } else { let combinedHeight = titleSize.height + activitySize.height + titleInfoSpacing - titleFrame = CGRect(origin: CGPoint(x: floor((clearBounds.width - titleSize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) - if titleFrame.size.width < size.width { - titleFrame.origin.x = -clearBounds.minX + floor((size.width - titleFrame.width) / 2.0) - } - titleFrame.origin.x = max(titleFrame.origin.x, clearBounds.minX + leftIconWidth) + titleFrame = CGRect(origin: CGPoint(x: leftInset + leftIconWidth, y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) self.titleNode.frame = titleFrame - var activityFrame = CGRect(origin: CGPoint(x: floor((clearBounds.width - activitySize.width) / 2.0), y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: activitySize) - if activitySize.width < size.width { - activityFrame.origin.x = -clearBounds.minX + floor((size.width - activityFrame.width) / 2.0) - } + var activityFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: activitySize) self.activityNode.frame = activityFrame } @@ -662,13 +670,18 @@ final class ChatTitleView: UIView, NavigationBarTitleView { } @objc func buttonPressed() { - if let pressed = self.pressed { - pressed() - } + self.pressed?() } func animateLayoutTransition() { UIView.transition(with: self, duration: 0.25, options: [.transitionCrossDissolve], animations: { }, completion: nil) } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.button.frame.contains(point) { + return self.button.view + } + return super.hitTest(point, with: event) + } } diff --git a/submodules/TelegramUI/TelegramUI/OpenAddContact.swift b/submodules/TelegramUI/TelegramUI/OpenAddContact.swift index 964bd20156..98c49ce1ba 100644 --- a/submodules/TelegramUI/TelegramUI/OpenAddContact.swift +++ b/submodules/TelegramUI/TelegramUI/OpenAddContact.swift @@ -18,7 +18,7 @@ func openAddContactImpl(context: AccountContext, firstName: String = "", lastNam let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: label, value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") present(deviceContactInfoController(context: context, subject: .create(peer: nil, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in if let peer = peer { - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { pushController(infoController) } } else { diff --git a/submodules/TelegramUI/TelegramUI/OpenUrl.swift b/submodules/TelegramUI/TelegramUI/OpenUrl.swift index b7f18394e6..11f7c094e2 100644 --- a/submodules/TelegramUI/TelegramUI/OpenUrl.swift +++ b/submodules/TelegramUI/TelegramUI/OpenUrl.swift @@ -209,7 +209,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur case .info: let _ = (context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { context.sharedContext.applicationBindings.dismissNativeController() navigationController?.pushViewController(infoController) } @@ -491,7 +491,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: idValue)) } |> deliverOnMainQueue).start(next: { peer in - if let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { navigationController?.pushViewController(controller) } }) diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift b/submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift index e1f5116a47..06811f13db 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift @@ -18,6 +18,8 @@ import NotificationMuteSettingsUI import NotificationSoundSelectionUI import OverlayStatusController import ShareController +import PhotoResources +import PeerAvatarGalleryUI private let avatarFont = avatarPlaceholderFont(size: 28.0) @@ -26,6 +28,7 @@ private enum PeerInfoHeaderButtonKey: Hashable { case call case mute case more + case addMember } private enum PeerInfoHeaderButtonIcon { @@ -34,11 +37,13 @@ private enum PeerInfoHeaderButtonIcon { case mute case unmute case more + case addMember } private final class PeerInfoHeaderButtonNode: HighlightableButtonNode { let key: PeerInfoHeaderButtonKey private let action: (PeerInfoHeaderButtonNode) -> Void + let containerNode: ASDisplayNode private let backgroundNode: ASImageNode private let textNode: ImmediateTextNode @@ -49,6 +54,8 @@ private final class PeerInfoHeaderButtonNode: HighlightableButtonNode { self.key = key self.action = action + self.containerNode = ASDisplayNode() + self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true @@ -58,8 +65,9 @@ private final class PeerInfoHeaderButtonNode: HighlightableButtonNode { super.init() - self.addSubnode(self.backgroundNode) - self.addSubnode(self.textNode) + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.backgroundNode) + self.containerNode.addSubnode(self.textNode) self.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -88,20 +96,22 @@ private final class PeerInfoHeaderButtonNode: HighlightableButtonNode { context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.normal) + context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) let imageName: String switch icon { case .message: - imageName = "Chat/Context Menu/Message" + imageName = "Peer Info/ButtonMessage" case .call: - imageName = "Chat/Context Menu/Call" + imageName = "Peer Info/ButtonCall" case .mute: - imageName = "Chat/Context Menu/Muted" + imageName = "Peer Info/ButtonMute" case .unmute: - imageName = "Chat/Context Menu/Unmute" + imageName = "Peer Info/ButtonUnmute" case .more: - imageName = "Chat/Context Menu/More" + imageName = "Peer Info/ButtonMore" + case .addMember: + imageName = "Peer Info/ButtonAddMember" } if let image = UIImage(bundleImageName: imageName) { let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) @@ -114,8 +124,351 @@ private final class PeerInfoHeaderButtonNode: HighlightableButtonNode { self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor) let titleSize = self.textNode.updateLayout(CGSize(width: 120.0, height: .greatestFiniteMagnitude)) + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) transition.updateFrameAdditiveToCenter(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: size.height + 6.0), size: titleSize)) + transition.updateAlpha(node: self.textNode, alpha: isExpanded ? 0.0 : 1.0) + } +} + +private final class PeerInfoHeaderNavigationTransition { + let sourceNavigationBar: NavigationBar + let sourceTitleView: ChatTitleView + let sourceTitleFrame: CGRect + let sourceSubtitleFrame: CGRect + let fraction: CGFloat + + init(sourceNavigationBar: NavigationBar, sourceTitleView: ChatTitleView, sourceTitleFrame: CGRect, sourceSubtitleFrame: CGRect, fraction: CGFloat) { + self.sourceNavigationBar = sourceNavigationBar + self.sourceTitleView = sourceTitleView + self.sourceTitleFrame = sourceTitleFrame + self.sourceSubtitleFrame = sourceSubtitleFrame + self.fraction = fraction + } +} + +private enum PeerInfoAvatarListItem: Equatable { + case topImage([ImageRepresentationWithReference]) + case image(TelegramMediaImageReference?, [ImageRepresentationWithReference]) + + var id: WrappedMediaResourceId { + switch self { + case let .topImage(representations): + let representation = largestImageRepresentation(representations.map { $0.representation }) ?? representations[representations.count - 1].representation + return WrappedMediaResourceId(representation.resource.id) + case let .image(_, representations): + let representation = largestImageRepresentation(representations.map { $0.representation }) ?? representations[representations.count - 1].representation + return WrappedMediaResourceId(representation.resource.id) + } + } +} + +private final class PeerInfoAvatarListItemNode: ASDisplayNode { + private let imageNode: TransformImageNode + + let isReady = Promise() + private var didSetReady: Bool = false + + init(context: AccountContext, item: PeerInfoAvatarListItem) { + self.imageNode = TransformImageNode() + + super.init() + + self.addSubnode(self.imageNode) + let representations: [ImageRepresentationWithReference] + switch item { + case let .topImage(topRepresentations): + representations = topRepresentations + case let .image(_, imageRepresentations): + representations = imageRepresentations + } + self.imageNode.setSignal(chatAvatarGalleryPhoto(account: context.account, representations: representations, autoFetchFullSize: true), dispatchOnDisplayLink: false) + + self.imageNode.imageUpdated = { [weak self] _ in + guard let strongSelf = self else { + return + } + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf.isReady.set(.single(true)) + } + } + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + let makeLayout = self.imageNode.asyncLayout() + let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets())) + let _ = applyLayout() + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size)) + } +} + +private final class PeerInfoAvatarListContainerNode: ASDisplayNode { + private let context: AccountContext + + let contentNode: ASDisplayNode + private var items: [PeerInfoAvatarListItem] = [] + private var itemNodes: [WrappedMediaResourceId: PeerInfoAvatarListItemNode] = [:] + private var currentIndex: Int = 0 + private var transitionFraction: CGFloat = 0.0 + + private var validLayout: CGSize? + + private let disposable = MetaDisposable() + private var initializedList = false + + let isReady = Promise() + private var didSetReady = false + + init(context: AccountContext) { + self.context = context + + self.contentNode = ASDisplayNode() + + super.init() + + self.backgroundColor = .black + + self.addSubnode(self.contentNode) + + self.view.disablesInteractiveTransitionGestureRecognizer = true + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + } + + deinit { + self.disposable.dispose() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .changed: + let translation = recognizer.translation(in: self.view) + var transitionFraction = translation.x / self.bounds.width + if self.currentIndex <= 0 { + transitionFraction = min(0.0, transitionFraction) + } + if self.currentIndex >= self.items.count - 1 { + transitionFraction = max(0.0, transitionFraction) + } + self.transitionFraction = transitionFraction + if let size = self.validLayout { + self.updateItems(size: size, transition: .animated(duration: 0.3, curve: .spring)) + } + case .cancelled, .ended: + let translation = recognizer.translation(in: self.view) + let velocity = recognizer.velocity(in: self.view) + var directionIsToRight = false + if abs(velocity.x) > 10.0 { + directionIsToRight = velocity.x < 0.0 + } else { + directionIsToRight = translation.x > self.bounds.width / 2.0 + } + var updatedIndex = self.currentIndex + if directionIsToRight { + updatedIndex = min(updatedIndex + 1, self.items.count - 1) + } else { + updatedIndex = max(updatedIndex - 1, 0) + } + self.currentIndex = updatedIndex + self.transitionFraction = 0.0 + if let size = self.validLayout { + self.updateItems(size: size, transition: .animated(duration: 0.3, curve: .spring)) + } + default: + break + } + } + + func update(size: CGSize, peer: Peer?, transition: ContainedViewLayoutTransition) { + self.validLayout = size + if let peer = peer, !self.initializedList { + self.initializedList = true + self.disposable.set((fetchedAvatarGalleryEntries(account: self.context.account, peer: peer) + |> deliverOnMainQueue).start(next: { [weak self] entries in + guard let strongSelf = self else { + return + } + var items: [PeerInfoAvatarListItem] = [] + for entry in entries { + switch entry { + case let .topImage(representations, _): + items.append(.topImage(representations)) + case let .image(reference, representations, _, _, _, _): + items.append(.image(reference, representations)) + } + } + strongSelf.items = items + if let size = strongSelf.validLayout { + strongSelf.updateItems(size: size, transition: .immediate) + } + if items.isEmpty { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf.isReady.set(.single(true)) + } + } + })) + } + self.updateItems(size: size, transition: transition) + } + + private func updateItems(size: CGSize, transition: ContainedViewLayoutTransition) { + var validIds: [WrappedMediaResourceId] = [] + var addedItemNodesForAdditiveTransition: [PeerInfoAvatarListItemNode] = [] + var additiveTransitionOffset: CGFloat = 0.0 + if self.currentIndex >= 0 && self.currentIndex < self.items.count { + for i in max(0, self.currentIndex - 1) ... min(self.currentIndex + 1, self.items.count - 1) { + validIds.append(self.items[i].id) + let itemNode: PeerInfoAvatarListItemNode + var wasAdded = false + if let current = self.itemNodes[self.items[i].id] { + itemNode = current + } else { + wasAdded = true + itemNode = PeerInfoAvatarListItemNode(context: self.context, item: self.items[i]) + self.itemNodes[self.items[i].id] = itemNode + self.contentNode.addSubnode(itemNode) + } + let indexOffset = CGFloat(i - self.currentIndex) + let itemFrame = CGRect(origin: CGPoint(x: indexOffset * size.width + self.transitionFraction * size.width - size.width / 2.0, y: -size.height / 2.0), size: size) + + if wasAdded { + addedItemNodesForAdditiveTransition.append(itemNode) + itemNode.frame = itemFrame + itemNode.update(size: size, transition: .immediate) + } else { + additiveTransitionOffset = itemNode.frame.minX - itemFrame.minX + transition.updateFrame(node: itemNode, frame: itemFrame) + itemNode.update(size: size, transition: transition) + } + } + } + for itemNode in addedItemNodesForAdditiveTransition { + transition.animatePositionAdditive(node: itemNode, offset: CGPoint(x: additiveTransitionOffset, y: 0.0)) + } + var removeIds: [WrappedMediaResourceId] = [] + for (id, _) in self.itemNodes { + if !validIds.contains(id) { + removeIds.append(id) + } + } + for id in removeIds { + if let itemNode = self.itemNodes.removeValue(forKey: id) { + itemNode.removeFromSupernode() + } + } + + if let item = self.items.first, let itemNode = self.itemNodes[item.id] { + if !self.didSetReady { + self.didSetReady = true + self.isReady.set(itemNode.isReady.get()) + } + } + } +} + +private final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { + let context: AccountContext + let avatarNode: AvatarNode + + var tapped: (() -> Void)? + + private var isFirstAvatarLoading = true + + init(context: AccountContext) { + self.context = context + self.avatarNode = AvatarNode(font: avatarFont) + + super.init() + + self.addSubnode(self.avatarNode) + self.avatarNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0)) + + self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.tapped?() + } + } + + func update(peer: Peer?, theme: PresentationTheme) { + if let peer = peer { + self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: 100.0, height: 100.0)) + self.isFirstAvatarLoading = false + } + } +} + +private final class PeerInfoAvatarListNode: ASDisplayNode { + let avatarContainerNode: PeerInfoAvatarTransformContainerNode + let listContainerTransformNode: ASDisplayNode + let listContainerNode: PeerInfoAvatarListContainerNode + + let isReady = Promise() + + init(context: AccountContext, readyWhenGalleryLoads: Bool) { + self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context) + self.listContainerTransformNode = ASDisplayNode() + self.listContainerNode = PeerInfoAvatarListContainerNode(context: context) + self.listContainerNode.clipsToBounds = true + self.listContainerNode.isHidden = true + + super.init() + + self.addSubnode(self.avatarContainerNode) + self.listContainerTransformNode.addSubnode(self.listContainerNode) + self.addSubnode(self.listContainerTransformNode) + + let avatarReady = self.avatarContainerNode.avatarNode.ready + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(true)) + + let galleryReady = self.listContainerNode.isReady.get() + |> filter { $0 } + |> take(1) + + let combinedSignal: Signal + if readyWhenGalleryLoads { + combinedSignal = combineLatest(queue: .mainQueue(), + avatarReady, + galleryReady + ) + |> map { lhs, rhs in + return lhs && rhs + } + } else { + combinedSignal = avatarReady + } + + self.isReady.set(combinedSignal + |> filter { $0 } + |> take(1)) + } + + func update(size: CGSize, isExpanded: Bool, peer: Peer?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { + self.avatarContainerNode.update(peer: peer, theme: theme) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.listContainerNode.isHidden { + if let result = self.listContainerNode.view.hitTest(self.view.convert(point, to: self.listContainerNode.view), with: event) { + return result + } + } else { + if let result = self.avatarContainerNode.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarContainerNode.avatarNode.view), with: event) { + return result + } + } + + return super.hitTest(point, with: event) } } @@ -123,23 +476,37 @@ private final class PeerInfoHeaderNode: ASDisplayNode { private var context: AccountContext private var presentationData: PresentationData? - private let avatarNode: AvatarNode - private let titleNode: ImmediateTextNode - private let subtitleNode: ImmediateTextNode + private(set) var isAvatarExpanded: Bool + + private let avatarListNode: PeerInfoAvatarListNode + let titleNodeContainer: ASDisplayNode + let titleNodeRawContainer: ASDisplayNode + let titleNode: ImmediateTextNode + let subtitleNodeContainer: ASDisplayNode + let subtitleNodeRawContainer: ASDisplayNode + let subtitleNode: ImmediateTextNode private var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:] private let backgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode + let separatorNode: ASDisplayNode var performButtonAction: ((PeerInfoHeaderButtonKey) -> Void)? + var requestAvatarExpansion: (() -> Void)? - init(context: AccountContext) { + var navigationTransition: PeerInfoHeaderNavigationTransition? + + init(context: AccountContext, avatarInitiallyExpanded: Bool) { self.context = context + self.isAvatarExpanded = avatarInitiallyExpanded - self.avatarNode = AvatarNode(font: avatarFont) + self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded) + self.titleNodeContainer = ASDisplayNode() + self.titleNodeRawContainer = ASDisplayNode() self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false + self.subtitleNodeContainer = ASDisplayNode() + self.subtitleNodeRawContainer = ASDisplayNode() self.subtitleNode = ImmediateTextNode() self.subtitleNode.displaysAsynchronously = false @@ -153,18 +520,47 @@ private final class PeerInfoHeaderNode: ASDisplayNode { self.addSubnode(self.backgroundNode) self.addSubnode(self.separatorNode) - self.addSubnode(self.avatarNode) - self.addSubnode(self.titleNode) - self.addSubnode(self.subtitleNode) + self.addSubnode(self.avatarListNode) + self.titleNodeContainer.addSubnode(self.titleNode) + self.addSubnode(self.titleNodeContainer) + self.subtitleNodeContainer.addSubnode(self.subtitleNode) + self.addSubnode(self.subtitleNodeContainer) + + self.avatarListNode.avatarContainerNode.tapped = { [weak self] in + guard let strongSelf = self else { + return + } + if !strongSelf.isAvatarExpanded { + strongSelf.requestAvatarExpansion?() + } + } } - func update(width: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, presence: TelegramUserPresence?, transition: ContainedViewLayoutTransition) -> CGFloat { + func update(width: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, contentOffset: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, presence: TelegramUserPresence?, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { self.presentationData = presentationData - self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + var transitionSourceHeight: CGFloat = 0.0 + var transitionFraction: CGFloat = 0.0 + var transitionSourceAvatarFrame = CGRect() + var transitionSourceTitleFrame = CGRect() + var transitionSourceSubtitleFrame = CGRect() + if let navigationTransition = self.navigationTransition, let sourceAvatarNode = navigationTransition.sourceTitleView.avatarNode?.avatarNode { + transitionSourceHeight = navigationTransition.sourceNavigationBar.bounds.height + transitionFraction = navigationTransition.fraction + transitionSourceAvatarFrame = sourceAvatarNode.view.convert(sourceAvatarNode.view.bounds, to: navigationTransition.sourceNavigationBar.view) + transitionSourceTitleFrame = navigationTransition.sourceTitleFrame + transitionSourceSubtitleFrame = navigationTransition.sourceSubtitleFrame + + transition.updateBackgroundColor(node: self.backgroundNode, color: presentationData.theme.list.itemBlocksBackgroundColor.interpolateTo(presentationData.theme.rootController.navigationBar.backgroundColor, fraction: transitionFraction)!) + } else { + self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + + let backgroundTransitionFraction: CGFloat = max(0.0, min(1.0, contentOffset / (212.0))) + transition.updateBackgroundColor(node: self.backgroundNode, color: presentationData.theme.list.itemBlocksBackgroundColor.interpolateTo(presentationData.theme.rootController.navigationBar.backgroundColor, fraction: backgroundTransitionFraction)!) + } + self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - let avatarSize: CGFloat = 100.0 let defaultButtonSize: CGFloat = 40.0 let defaultMaxButtonSpacing: CGFloat = 40.0 @@ -176,9 +572,7 @@ private final class PeerInfoHeaderNode: ASDisplayNode { buttonKeys.append(.mute) buttonKeys.append(.more) - self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) - - self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.medium(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) let presence = presence ?? TelegramUserPresence(status: .none, lastActivity: 0) let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 @@ -193,24 +587,245 @@ private final class PeerInfoHeaderNode: ASDisplayNode { } let textSideInset: CGFloat = 16.0 + let expandedAvatarControlsHeight: CGFloat = 64.0 + let expandedAvatarHeight: CGFloat = width + expandedAvatarControlsHeight - var height: CGFloat = navigationHeight - height += 212.0 - + let avatarSize: CGFloat = 100.0 let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 10.0), size: CGSize(width: avatarSize, height: avatarSize)) - transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + let avatarCenter = CGPoint(x: (1.0 - transitionFraction) * avatarFrame.midX + transitionFraction * transitionSourceAvatarFrame.midX, y: (1.0 - transitionFraction) * avatarFrame.midY + transitionFraction * transitionSourceAvatarFrame.midY) let titleSize = self.titleNode.updateLayout(CGSize(width: width - textSideInset * 2.0, height: .greatestFiniteMagnitude)) let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: width - textSideInset * 2.0, height: .greatestFiniteMagnitude)) - let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: avatarFrame.maxY + 10.0), size: titleSize) - let subtitleFrame = CGRect(origin: CGPoint(x: floor((width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize) - transition.updateFrameAdditiveToCenter(node: self.titleNode, frame: titleFrame) - transition.updateFrameAdditiveToCenter(node: self.subtitleNode, frame: subtitleFrame) + let titleFrame: CGRect + let subtitleFrame: CGRect + if self.isAvatarExpanded { + titleFrame = CGRect(origin: CGPoint(x: 16.0, y: expandedAvatarHeight - expandedAvatarControlsHeight + 12.0), size: titleSize) + subtitleFrame = CGRect(origin: CGPoint(x: 16.0, y: titleFrame.maxY - 5.0), size: subtitleSize) + } else { + titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: avatarFrame.maxY + 10.0), size: titleSize) + subtitleFrame = CGRect(origin: CGPoint(x: floor((width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize) + } - let buttonSpacing: CGFloat = min(defaultMaxButtonSpacing, width - floor(CGFloat(buttonKeys.count) * defaultButtonSize / CGFloat(buttonKeys.count + 1))) + let titleLockOffset: CGFloat = 7.0 + let titleMaxLockOffset: CGFloat = 7.0 + let titleCollapseOffset = titleFrame.midY - statusBarHeight - titleLockOffset + let titleOffset = -min(titleCollapseOffset, contentOffset) + let titleCollapseFraction = max(0.0, min(1.0, contentOffset / titleCollapseOffset)) + + let titleMinScale: CGFloat = 0.7 + let subtitleMinScale: CGFloat = 0.8 + let avatarMinScale: CGFloat = 0.7 + + let apparentTitleLockOffset = (1.0 - titleCollapseFraction) * 0.0 + titleCollapseFraction * titleMaxLockOffset + + let avatarScale: CGFloat + let avatarOffset: CGFloat + if self.navigationTransition != nil { + avatarScale = ((1.0 - transitionFraction) * avatarFrame.width + transitionFraction * transitionSourceAvatarFrame.width) / avatarFrame.width + avatarOffset = 0.0 + } else { + avatarScale = 1.0 * (1.0 - titleCollapseFraction) + avatarMinScale * titleCollapseFraction + avatarOffset = apparentTitleLockOffset + 0.0 * (1.0 - titleCollapseFraction) + 10.0 * titleCollapseFraction + } + let avatarListFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: width)) + + if self.isAvatarExpanded { + self.avatarListNode.listContainerNode.isHidden = false + if !transitionSourceAvatarFrame.width.isZero { + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: transitionFraction * transitionSourceAvatarFrame.width / 2.0) + } else { + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 0.0) + } + } else if self.avatarListNode.listContainerNode.cornerRadius != 50.0 { + transition.updateCornerRadius(node: self.avatarListNode.listContainerNode, cornerRadius: 50.0, completion: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.avatarListNode.listContainerNode.isHidden = true + }) + } + + self.avatarListNode.update(size: CGSize(), isExpanded: self.isAvatarExpanded, peer: peer, theme: presentationData.theme, transition: transition) + if additive { + transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.avatarContainerNode, scale: avatarScale) + } else { + transition.updateSublayerTransformScale(node: self.avatarListNode.avatarContainerNode, scale: avatarScale) + } + let apparentAvatarFrame: CGRect + if self.isAvatarExpanded { + let expandedAvatarCenter = CGPoint(x: width / 2.0, y: width / 2.0 - contentOffset / 2.0) + apparentAvatarFrame = CGRect(origin: CGPoint(x: expandedAvatarCenter.x * (1.0 - transitionFraction) + transitionFraction * avatarCenter.x, y: expandedAvatarCenter.y * (1.0 - transitionFraction) + transitionFraction * avatarCenter.y), size: CGSize()) + } else { + apparentAvatarFrame = CGRect(origin: CGPoint(x: avatarCenter.x - avatarFrame.width / 2.0, y: -contentOffset + avatarOffset + avatarCenter.y - avatarFrame.height / 2.0), size: avatarFrame.size) + } + if case let .animated(duration, curve) = transition, !transitionSourceAvatarFrame.width.isZero { + let previousFrame = self.avatarListNode.frame + self.avatarListNode.frame = CGRect(origin: apparentAvatarFrame.center, size: CGSize()) + let horizontalTransition: ContainedViewLayoutTransition + let verticalTransition: ContainedViewLayoutTransition + if transitionFraction < .ulpOfOne { + horizontalTransition = .animated(duration: duration * 0.85, curve: curve) + verticalTransition = .animated(duration: duration * 1.15, curve: curve) + } else { + horizontalTransition = transition + verticalTransition = .animated(duration: duration * 0.6, curve: curve) + } + horizontalTransition.animatePositionAdditive(node: self.avatarListNode, offset: CGPoint(x: previousFrame.midX - apparentAvatarFrame.midX, y: 0.0)) + verticalTransition.animatePositionAdditive(node: self.avatarListNode, offset: CGPoint(x: 0.0, y: previousFrame.midY - apparentAvatarFrame.midY)) + } else { + transition.updateFrameAdditive(node: self.avatarListNode, frame: CGRect(origin: apparentAvatarFrame.center, size: CGSize())) + } + + let avatarListContainerFrame: CGRect + let avatarListContainerScale: CGFloat + if self.isAvatarExpanded { + if !transitionSourceAvatarFrame.width.isZero { + let neutralAvatarListContainerSize = CGSize(width: width, height: width) + let avatarListContainerSize = CGSize(width: neutralAvatarListContainerSize.width * (1.0 - transitionFraction) + transitionSourceAvatarFrame.width * transitionFraction, height: neutralAvatarListContainerSize.height * (1.0 - transitionFraction) + transitionSourceAvatarFrame.height * transitionFraction) + avatarListContainerFrame = CGRect(origin: CGPoint(x: -avatarListContainerSize.width / 2.0, y: -avatarListContainerSize.height / 2.0), size: avatarListContainerSize) + } else { + avatarListContainerFrame = CGRect(origin: CGPoint(x: -width / 2.0, y: -width / 2.0), size: CGSize(width: width, height: width)) + } + avatarListContainerScale = 1.0 + max(0.0, -contentOffset / avatarListContainerFrame.width) + } else { + avatarListContainerFrame = CGRect(origin: CGPoint(x: -apparentAvatarFrame.width / 2.0, y: -apparentAvatarFrame.height / 2.0), size: apparentAvatarFrame.size) + avatarListContainerScale = avatarScale + } + transition.updateFrame(node: self.avatarListNode.listContainerNode, frame: avatarListContainerFrame) + let innerScale = avatarListContainerFrame.width / width + let innerDelta = (avatarListContainerFrame.width - width) / 2.0 + transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerNode, scale: innerScale) + transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.contentNode, frame: CGRect(origin: CGPoint(x: innerDelta + width / 2.0, y: innerDelta + width / 2.0), size: CGSize())) + + if additive { + transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) + } else { + transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) + } + + self.avatarListNode.listContainerNode.update(size: CGSize(width: width, height: width), peer: peer, transition: transition) + + let buttonsCollapseStart = titleCollapseOffset + let buttonsCollapseEnd = 212.0 - (navigationHeight - statusBarHeight) + 10.0 + + let buttonsCollapseFraction = max(0.0, contentOffset - buttonsCollapseStart) / (buttonsCollapseEnd - buttonsCollapseStart) + + let rawHeight: CGFloat + let height: CGFloat + if self.isAvatarExpanded { + rawHeight = expandedAvatarHeight + height = max(navigationHeight, rawHeight - contentOffset) + } else { + rawHeight = navigationHeight + 212.0 + height = navigationHeight + max(0.0, 212.0 - contentOffset) + } + + let apparentHeight = (1.0 - transitionFraction) * height + transitionFraction * transitionSourceHeight + + if !titleSize.width.isZero && !titleSize.height.isZero { + if self.navigationTransition != nil { + var neutralTitleScale: CGFloat = 1.0 + var neutralSubtitleScale: CGFloat = 1.0 + if self.isAvatarExpanded { + neutralTitleScale = 0.7 + neutralSubtitleScale = 1.0 + } + + let titleScale = (transitionFraction * transitionSourceTitleFrame.height + (1.0 - transitionFraction) * titleFrame.height * neutralTitleScale) / (titleFrame.height) + let subtitleScale = (transitionFraction * transitionSourceSubtitleFrame.height + (1.0 - transitionFraction) * subtitleFrame.height * neutralSubtitleScale) / (subtitleFrame.height) + + let titleOrigin = CGPoint(x: transitionFraction * transitionSourceTitleFrame.minX + (1.0 - transitionFraction) * titleFrame.minX, y: transitionFraction * transitionSourceTitleFrame.minY + (1.0 - transitionFraction) * titleFrame.minY) + let subtitleOrigin = CGPoint(x: transitionFraction * transitionSourceSubtitleFrame.minX + (1.0 - transitionFraction) * subtitleFrame.minX, y: transitionFraction * transitionSourceSubtitleFrame.minY + (1.0 - transitionFraction) * subtitleFrame.minY) + + let rawTitleFrame = CGRect(origin: titleOrigin, size: titleFrame.size) + self.titleNodeRawContainer.frame = rawTitleFrame + transition.updateFrameAdditiveToCenter(node: self.titleNodeContainer, frame: rawTitleFrame.offsetBy(dx: rawTitleFrame.width * 0.5 * (titleScale - 1.0), dy: titleOffset + rawTitleFrame.height * 0.5 * (titleScale - 1.0))) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size)) + let rawSubtitleFrame = CGRect(origin: subtitleOrigin, size: subtitleFrame.size) + self.subtitleNodeRawContainer.frame = rawSubtitleFrame + transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: rawSubtitleFrame.offsetBy(dx: rawSubtitleFrame.width * 0.5 * (subtitleScale - 1.0), dy: titleOffset + rawSubtitleFrame.height * 0.5 * (subtitleScale - 1.0))) + transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: subtitleFrame.size)) + transition.updateSublayerTransformScale(node: self.titleNodeContainer, scale: titleScale) + transition.updateSublayerTransformScale(node: self.subtitleNodeContainer, scale: subtitleScale) + } else { + let titleScale: CGFloat + let subtitleScale: CGFloat + if self.isAvatarExpanded { + titleScale = 0.7 + subtitleScale = 1.0 + } else { + titleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * titleMinScale + subtitleScale = (1.0 - titleCollapseFraction) * 1.0 + titleCollapseFraction * subtitleMinScale + } + + let rawTitleFrame = titleFrame + self.titleNodeRawContainer.frame = rawTitleFrame + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(), size: titleFrame.size)) + let rawSubtitleFrame = subtitleFrame + self.subtitleNodeRawContainer.frame = rawSubtitleFrame + if self.isAvatarExpanded { + transition.updateFrameAdditive(node: self.titleNodeContainer, frame: rawTitleFrame.offsetBy(dx: 0.0, dy: titleOffset + apparentTitleLockOffset).offsetBy(dx: rawTitleFrame.width * 0.5 * (titleScale - 1.0), dy: rawTitleFrame.height * 0.5 * (titleScale - 1.0))) + transition.updateFrameAdditive(node: self.subtitleNodeContainer, frame: rawSubtitleFrame.offsetBy(dx: 0.0, dy: titleOffset).offsetBy(dx: rawSubtitleFrame.width * 0.5 * (subtitleScale - 1.0), dy: rawSubtitleFrame.height * 0.5 * (subtitleScale - 1.0))) + } else { + transition.updateFrameAdditiveToCenter(node: self.titleNodeContainer, frame: rawTitleFrame.offsetBy(dx: 0.0, dy: titleOffset + apparentTitleLockOffset)) + transition.updateFrameAdditiveToCenter(node: self.subtitleNodeContainer, frame: rawSubtitleFrame.offsetBy(dx: 0.0, dy: titleOffset)) + } + transition.updateFrame(node: self.subtitleNode, frame: CGRect(origin: CGPoint(), size: subtitleFrame.size)) + transition.updateSublayerTransformScaleAdditive(node: self.titleNodeContainer, scale: titleScale) + transition.updateSublayerTransformScaleAdditive(node: self.subtitleNodeContainer, scale: subtitleScale) + } + } + + let buttonSpacing: CGFloat + if self.isAvatarExpanded { + buttonSpacing = 16.0 + } else { + buttonSpacing = min(defaultMaxButtonSpacing, width - floor(CGFloat(buttonKeys.count) * defaultButtonSize / CGFloat(buttonKeys.count + 1))) + } + + let expandedButtonSize: CGFloat = 32.0 let buttonsWidth = buttonSpacing * CGFloat(buttonKeys.count - 1) + CGFloat(buttonKeys.count) * defaultButtonSize - var buttonRightOrigin = CGPoint(x: floor((width - buttonsWidth) / 2.0) + buttonsWidth, y: height - 74.0) + var buttonRightOrigin: CGPoint + if self.isAvatarExpanded { + buttonRightOrigin = CGPoint(x: width - 16.0, y: apparentHeight - 74.0) + } else { + buttonRightOrigin = CGPoint(x: floor((width - buttonsWidth) / 2.0) + buttonsWidth, y: apparentHeight - 74.0) + } + let buttonsScale: CGFloat + let buttonsAlpha: CGFloat + let apparentButtonSize: CGFloat + let buttonsVerticalOffset: CGFloat + if self.navigationTransition != nil { + if self.isAvatarExpanded { + apparentButtonSize = expandedButtonSize + } else { + apparentButtonSize = defaultButtonSize + } + let neutralButtonsScale = apparentButtonSize / defaultButtonSize + buttonsScale = (1.0 - transitionFraction) * neutralButtonsScale + 0.2 * transitionFraction + buttonsAlpha = 1.0 - transitionFraction + + let neutralButtonsOffset: CGFloat + if self.isAvatarExpanded { + neutralButtonsOffset = 74.0 - 15.0 - defaultButtonSize + (defaultButtonSize - apparentButtonSize) / 2.0 + } else { + neutralButtonsOffset = (1.0 - buttonsScale) * apparentButtonSize + } + + buttonsVerticalOffset = (1.0 - transitionFraction) * neutralButtonsOffset + ((1.0 - buttonsScale) * apparentButtonSize) * transitionFraction + } else { + apparentButtonSize = self.isAvatarExpanded ? expandedButtonSize : defaultButtonSize + if self.isAvatarExpanded { + buttonsScale = apparentButtonSize / defaultButtonSize + buttonsVerticalOffset = 74.0 - 15.0 - defaultButtonSize + (defaultButtonSize - apparentButtonSize) / 2.0 + } else { + buttonsScale = (1.0 - buttonsCollapseFraction) * 1.0 + 0.2 * buttonsCollapseFraction + buttonsVerticalOffset = (1.0 - buttonsScale) * apparentButtonSize + } + buttonsAlpha = 1.0 - buttonsCollapseFraction + } + let buttonsScaledOffset = (defaultButtonSize - apparentButtonSize) / 2.0 for buttonKey in buttonKeys.reversed() { let buttonNode: PeerInfoHeaderButtonNode var wasAdded = false @@ -225,10 +840,15 @@ private final class PeerInfoHeaderNode: ASDisplayNode { self.addSubnode(buttonNode) } - let buttonFrame = CGRect(origin: CGPoint(x: buttonRightOrigin.x - defaultButtonSize, y: buttonRightOrigin.y), size: CGSize(width: defaultButtonSize, height: defaultButtonSize)) - buttonRightOrigin.x -= defaultButtonSize + buttonSpacing + let buttonFrame = CGRect(origin: CGPoint(x: buttonRightOrigin.x - defaultButtonSize + buttonsScaledOffset, y: buttonRightOrigin.y), size: CGSize(width: defaultButtonSize, height: defaultButtonSize)) let buttonTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition - buttonTransition.updateFrame(node: buttonNode, frame: buttonFrame) + + let apparentButtonFrame = buttonFrame.offsetBy(dx: 0.0, dy: buttonsVerticalOffset) + if additive { + buttonTransition.updateFrameAdditiveToCenter(node: buttonNode, frame: apparentButtonFrame) + } else { + buttonTransition.updateFrame(node: buttonNode, frame: apparentButtonFrame) + } let buttonText: String let buttonIcon: PeerInfoHeaderButtonIcon switch buttonKey { @@ -249,8 +869,32 @@ private final class PeerInfoHeaderNode: ASDisplayNode { case .more: buttonText = "More" buttonIcon = .more + case .addMember: + buttonText = "Add Member" + buttonIcon = .addMember + } + buttonNode.update(size: buttonFrame.size, text: buttonText, icon: buttonIcon, isExpanded: self.isAvatarExpanded, presentationData: presentationData, transition: buttonTransition) + transition.updateSublayerTransformScaleAdditive(node: buttonNode, scale: buttonsScale) + + transition.updateAlpha(node: buttonNode, alpha: buttonsAlpha) + if self.isAvatarExpanded, case .mute = buttonKey { + if case let .animated(duration, curve) = transition { + ContainedViewLayoutTransition.animated(duration: duration * 0.3, curve: curve).updateAlpha(node: buttonNode.containerNode, alpha: 0.0) + } else { + transition.updateAlpha(node: buttonNode.containerNode, alpha: 0.0) + } + } else { + if case .mute = buttonKey, buttonNode.containerNode.alpha.isZero, additive { + if case let .animated(duration, curve) = transition { + ContainedViewLayoutTransition.animated(duration: duration * 0.3, curve: curve).updateAlpha(node: buttonNode.containerNode, alpha: 1.0) + } else { + transition.updateAlpha(node: buttonNode.containerNode, alpha: 1.0) + } + } else { + transition.updateAlpha(node: buttonNode.containerNode, alpha: 1.0) + } + buttonRightOrigin.x -= apparentButtonSize + buttonSpacing } - buttonNode.update(size: buttonFrame.size, text: buttonText, icon: buttonIcon, isExpanded: false, presentationData: presentationData, transition: buttonTransition) } for key in self.buttonNodes.keys { @@ -262,15 +906,43 @@ private final class PeerInfoHeaderNode: ASDisplayNode { } } - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -1000.0), size: CGSize(width: width, height: 1000.0 + height))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: height), size: CGSize(width: width, height: UIScreenPixel))) + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + apparentHeight), size: CGSize(width: width, height: 2000.0)) + let separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: apparentHeight), size: CGSize(width: width, height: UIScreenPixel)) + if additive { + transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrameAdditive(node: self.separatorNode, frame: separatorFrame) + } else { + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.separatorNode, frame: separatorFrame) + } - return height + if self.isAvatarExpanded { + return width + expandedAvatarControlsHeight + } else { + return 212.0 + navigationHeight + } } private func buttonPressed(_ buttonNode: PeerInfoHeaderButtonNode) { self.performButtonAction?(buttonNode.key) } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.backgroundNode.frame.contains(point) { + return nil + } + guard let result = super.hitTest(point, with: event) else { + return nil + } + if result == self.view { + return nil + } + return result + } + + func updateIsAvatarExpanded(_ isAvatarExpanded: Bool) { + self.isAvatarExpanded = isAvatarExpanded + } } protocol PeerInfoPaneNode: ASDisplayNode { @@ -511,7 +1183,7 @@ private final class PeerInfoPaneContainerNode: ASDisplayNode { let isReady = Promise() var didSetIsReady = false - private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? + private var currentParams: (size: CGSize, expansionFraction: CGFloat, presentationData: PresentationData)? private var availablePanes: [PeerInfoPaneKey] = [] private var currentPaneKey: PeerInfoPaneKey? @@ -581,8 +1253,8 @@ private final class PeerInfoPaneContainerNode: ASDisplayNode { let disposable = MetaDisposable() strongSelf.candidatePane = (PeerInfoPaneWrapper(key: key, node: paneNode), disposable) - if let (size, isScrollingLockedAtTop, presentationData) = strongSelf.currentParams { - strongSelf.update(size: size, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, transition: .immediate) + if let (size, expansionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, expansionFraction: expansionFraction, presentationData: presentationData, transition: .immediate) } disposable.set((paneNode.isReady @@ -597,8 +1269,8 @@ private final class PeerInfoPaneContainerNode: ASDisplayNode { strongSelf.currentPaneKey = candidatePane.key strongSelf.currentPane = candidatePane - if let (size, isScrollingLockedAtTop, presentationData) = strongSelf.currentParams { - strongSelf.update(size: size, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, transition: .animated(duration: 0.35, curve: .spring)) + if let (size, expansionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, expansionFraction: expansionFraction, presentationData: presentationData, transition: .animated(duration: 0.35, curve: .spring)) if let previousPane = previousPane { let directionToRight: Bool @@ -641,10 +1313,10 @@ private final class PeerInfoPaneContainerNode: ASDisplayNode { return self.currentPane?.node.transitionNodeForGallery(messageId: messageId, media: media) } - func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { - self.currentParams = (size, isScrollingLockedAtTop, presentationData) + func update(size: CGSize, expansionFraction: CGFloat, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, expansionFraction, presentationData) - transition.updateAlpha(node: self.coveringBackgroundNode, alpha: isScrollingLockedAtTop ? 0.0 : 1.0) + transition.updateAlpha(node: self.coveringBackgroundNode, alpha: expansionFraction) self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor self.coveringBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor @@ -654,7 +1326,7 @@ private final class PeerInfoPaneContainerNode: ASDisplayNode { let tabsHeight: CGFloat = 48.0 transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: tabsHeight))) + transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) transition.updateFrame(node: self.tapsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) @@ -714,12 +1386,12 @@ private final class PeerInfoPaneContainerNode: ASDisplayNode { let paneTransition: ContainedViewLayoutTransition = paneWasAdded ? .immediate : transition paneTransition.updateFrame(node: currentPane.node, frame: paneFrame) - currentPane.update(size: paneFrame.size, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) + currentPane.update(size: paneFrame.size, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) } if let (candidatePane, _) = self.candidatePane { let paneTransition: ContainedViewLayoutTransition = .immediate paneTransition.updateFrame(node: candidatePane.node, frame: paneFrame) - candidatePane.update(size: paneFrame.size, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: true, transition: paneTransition) + candidatePane.update(size: paneFrame.size, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: paneTransition) } if !self.didSetIsReady { self.didSetIsReady = true @@ -930,99 +1602,27 @@ private func peerInfoSectionItems(data: PeerInfoScreenData?, presentationData: P return items } -private final class PeerInfoNavigationNode: ASDisplayNode { - private let backgroundNode: ASDisplayNode - private let separatorContainerNode: ASDisplayNode - private let separatorCoveringNode: ASDisplayNode - private let separatorNode: ASDisplayNode - private let titleNode: ImmediateTextNode - - private var currentParams: (PresentationData, Peer?)? - - override init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.separatorContainerNode = ASDisplayNode() - self.separatorContainerNode.isLayerBacked = true - self.separatorContainerNode.clipsToBounds = true - - self.separatorCoveringNode = ASDisplayNode() - self.separatorCoveringNode.isLayerBacked = true - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - - self.titleNode = ImmediateTextNode() - - super.init() - - self.addSubnode(self.backgroundNode) - - self.separatorContainerNode.addSubnode(self.separatorNode) - self.separatorContainerNode.addSubnode(self.separatorCoveringNode) - self.addSubnode(self.separatorContainerNode) - - self.addSubnode(self.titleNode) - } - - func update(size: CGSize, statusBarHeight: CGFloat, navigationHeight: CGFloat, offset: CGFloat, paneContainerOffset: CGFloat, presentationData: PresentationData, peer: Peer?, transition: ContainedViewLayoutTransition) { - if let (currentPresentationData, currentPeer) = self.currentParams { - if currentPresentationData !== presentationData || currentPeer !== peer { - if let peer = peer { - self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(17.0), textColor: presentationData.theme.rootController.navigationBar.primaryTextColor) - } - } - } - - if self.currentParams?.0.theme !== presentationData.theme { - self.backgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor - self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor - self.separatorCoveringNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor - } - - self.currentParams = (presentationData, peer) - - let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 100.0, height: .greatestFiniteMagnitude)) - let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: statusBarHeight + floor((navigationHeight - statusBarHeight - titleSize.height) / 2.0)), size: titleSize) - transition.updateFrameAdditiveToCenter(node: self.titleNode, frame: titleFrame) - - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) - transition.updateFrame(node: self.separatorContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))) - transition.updateFrame(node: self.separatorCoveringNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -offset + paneContainerOffset - size.height), size: CGSize(width: size.width, height: 10.0 + UIScreenPixel))) - - let revealOffset: CGFloat = 100.0 - let progress: CGFloat = max(0.0, min(1.0, offset / revealOffset)) - - transition.updateAlpha(node: self.backgroundNode, alpha: progress) - transition.updateAlpha(node: self.separatorNode, alpha: progress) - transition.updateAlpha(node: self.titleNode, alpha: progress) - } -} - private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { private weak var controller: PeerInfoScreen? private let context: AccountContext private let peerId: PeerId private var presentationData: PresentationData - private let scrollNode: ASScrollNode + let scrollNode: ASScrollNode - private let navigationNode: PeerInfoNavigationNode - private let headerNode: PeerInfoHeaderNode + let headerNode: PeerInfoHeaderNode private let infoSection: PeerInfoScreenItemSectionContainerNode private let paneContainerNode: PeerInfoPaneContainerNode - private var isPaneAreaExpanded: Bool = false private var ignoreScrolling: Bool = false + private var hapticFeedback: HapticFeedback? private var _interaction: PeerInfoInteraction? private var interaction: PeerInfoInteraction { return self._interaction! } - private var validLayout: (ContainerViewLayout, CGFloat)? - private var data: PeerInfoScreenData? + private(set) var validLayout: (ContainerViewLayout, CGFloat)? + private(set) var data: PeerInfoScreenData? private var dataDisposable: Disposable? private let _ready = Promise() @@ -1031,7 +1631,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } private var didSetReady = false - init(controller: PeerInfoScreen, context: AccountContext, peerId: PeerId) { + init(controller: PeerInfoScreen, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool) { self.controller = controller self.context = context self.peerId = peerId @@ -1039,8 +1639,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.scrollNode = ASScrollNode() - self.navigationNode = PeerInfoNavigationNode() - self.headerNode = PeerInfoHeaderNode(context: context) + self.headerNode = PeerInfoHeaderNode(context: context, avatarInitiallyExpanded: avatarInitiallyExpanded) self.infoSection = PeerInfoScreenItemSectionContainerNode(id: 0) self.paneContainerNode = PeerInfoPaneContainerNode(context: context, peerId: peerId) @@ -1064,11 +1663,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.scrollNode.view.scrollsToTop = false self.scrollNode.view.delegate = self self.addSubnode(self.scrollNode) - self.addSubnode(self.navigationNode) - - self.scrollNode.addSubnode(self.headerNode) self.scrollNode.addSubnode(self.infoSection) self.scrollNode.addSubnode(self.paneContainerNode) + self.addSubnode(self.headerNode) self.paneContainerNode.openMessage = { [weak self] id in return self?.openMessage(id: id) ?? false @@ -1078,6 +1675,20 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self?.performButtonAction(key: key) } + self.headerNode.requestAvatarExpansion = { [weak self] in + guard let strongSelf = self else { + return + } + let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + + strongSelf.headerNode.updateIsAvatarExpanded(true) + strongSelf.updateNavigationExpansionPresentation(isExpanded: true, animated: true) + + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition, additive: true) + } + } + self.dataDisposable = (peerInfoScreenData(context: context, peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] data in guard let strongSelf = self else { @@ -1103,13 +1714,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } func scrollToTop() { - if self.isPaneAreaExpanded { - if !self.paneContainerNode.scrollToTop() { - - } - } else { - self.scrollNode.view.setContentOffset(CGPoint(), animated: true) - } + self.scrollNode.view.setContentOffset(CGPoint(), animated: true) } private func openMessage(id: MessageId) -> Bool { @@ -1149,7 +1754,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD switch key { case .message: if let navigationController = controller.navigationController as? NavigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId))) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId))) } case .call: self.requestCall() @@ -1206,6 +1811,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) controller.present(actionSheet, in: .window(.root)) + case .addMember: + break } } @@ -1373,47 +1980,62 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }) } - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition, additive: Bool = false) { self.validLayout = (layout, navigationHeight) self.ignoreScrolling = true - transition.updateFrame(node: self.navigationNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: navigationHeight))) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) let sectionSpacing: CGFloat = 24.0 var contentHeight: CGFloat = 0.0 - let headerHeight = self.headerNode.update(width: layout.size.width, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, presence: self.data?.presence, transition: transition) - transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight))) + let headerHeight = self.headerNode.update(width: layout.size.width, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, contentOffset: self.scrollNode.view.contentOffset.y, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, presence: self.data?.presence, transition: transition, additive: additive) + let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: headerHeight)) + if additive { + transition.updateFrameAdditive(node: self.headerNode, frame: headerFrame) + } else { + transition.updateFrame(node: self.headerNode, frame: headerFrame) + } contentHeight += headerHeight contentHeight += sectionSpacing let infoSectionHeight = self.infoSection.update(width: layout.size.width, presentationData: self.presentationData, items: peerInfoSectionItems(data: self.data, presentationData: self.presentationData, interaction: self.interaction), transition: transition) let infoSectionFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: infoSectionHeight)) - transition.updateFrame(node: self.infoSection, frame: infoSectionFrame) + if additive { + transition.updateFrameAdditive(node: self.infoSection, frame: infoSectionFrame) + } else { + transition.updateFrame(node: self.infoSection, frame: infoSectionFrame) + } contentHeight += infoSectionHeight contentHeight += sectionSpacing let paneContainerSize = CGSize(width: layout.size.width, height: layout.size.height - navigationHeight) - self.paneContainerNode.update(size: paneContainerSize, isScrollingLockedAtTop: !self.isPaneAreaExpanded, presentationData: self.presentationData, transition: transition) - transition.updateFrame(node: self.paneContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: paneContainerSize)) - contentHeight += layout.size.height - navigationHeight - - self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: contentHeight) - - if self.isPaneAreaExpanded { - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentHeight - self.scrollNode.bounds.height), size: self.scrollNode.bounds.size)) - } else { - let maxOffsetY = max(0.0, contentHeight - floor(self.scrollNode.bounds.height * 1.5)) - if self.scrollNode.view.contentOffset.y > maxOffsetY { - //transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: maxOffsetY), size: self.scrollNode.bounds.size)) - } + var restoreContentOffset: CGPoint? + if additive { + restoreContentOffset = self.scrollNode.view.contentOffset + } + self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: contentHeight + paneContainerSize.height) + if let restoreContentOffset = restoreContentOffset { + self.scrollNode.view.contentOffset = restoreContentOffset } + let paneAreaExpansionDistance: CGFloat = 32.0 + var paneAreaExpansionDelta = (contentHeight - navigationHeight) - self.scrollNode.view.contentOffset.y + paneAreaExpansionDelta = max(0.0, min(paneAreaExpansionDelta, paneAreaExpansionDistance)) + let paneAreaExpansionFraction: CGFloat = 1.0 - paneAreaExpansionDelta / paneAreaExpansionDistance + + let paneContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: paneContainerSize) + if additive { + transition.updateFrameAdditive(node: self.paneContainerNode, frame: paneContainerFrame) + } else { + transition.updateFrame(node: self.paneContainerNode, frame: paneContainerFrame) + } + contentHeight += layout.size.height - navigationHeight + self.ignoreScrolling = false - self.updateNavigation(transition: transition) + self.updateNavigation(transition: transition, additive: additive) if !self.didSetReady && self.data != nil { self.didSetReady = true @@ -1421,91 +2043,113 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } - private func updateNavigation(transition: ContainedViewLayoutTransition) { + private func updateNavigation(transition: ContainedViewLayoutTransition, additive: Bool) { let offsetY = self.scrollNode.view.contentOffset.y - if offsetY <= 1.0 { + if offsetY <= 50.0 { self.scrollNode.view.bounces = true } else { self.scrollNode.view.bounces = false } if let (layout, navigationHeight) = self.validLayout { - self.navigationNode.update(size: CGSize(width: layout.size.width, height: navigationHeight), statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, offset: offsetY, paneContainerOffset: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.peer, transition: transition) + if !additive { + self.headerNode.update(width: layout.size.width, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, contentOffset: offsetY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, presence: self.data?.presence, transition: transition, additive: additive) + } + + let paneAreaExpansionDistance: CGFloat = 32.0 + var paneAreaExpansionDelta = (self.paneContainerNode.frame.minY - navigationHeight) - self.scrollNode.view.contentOffset.y + paneAreaExpansionDelta = max(0.0, min(paneAreaExpansionDelta, paneAreaExpansionDistance)) + let paneAreaExpansionFraction: CGFloat = 1.0 - paneAreaExpansionDelta / paneAreaExpansionDistance + + transition.updateAlpha(node: self.headerNode.separatorNode, alpha: 1.0 - paneAreaExpansionFraction) + + self.paneContainerNode.update(size: self.paneContainerNode.bounds.size, expansionFraction: paneAreaExpansionFraction, presentationData: self.presentationData, transition: transition) } } + private var canUpdateAvatarExpansion = false + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.canUpdateAvatarExpansion = true + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { if self.ignoreScrolling { return } - self.updateNavigation(transition: .immediate) + self.updateNavigation(transition: .immediate, additive: false) + + if scrollView.isDragging && scrollView.isTracking { + let offsetY = self.scrollNode.view.contentOffset.y + var shouldBeExpanded: Bool? + if offsetY <= -32.0 { + shouldBeExpanded = true + } else if offsetY >= 4.0 { + shouldBeExpanded = false + } + if let shouldBeExpanded = shouldBeExpanded, self.canUpdateAvatarExpansion, shouldBeExpanded != self.headerNode.isAvatarExpanded { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + + if self.hapticFeedback == nil { + self.hapticFeedback = HapticFeedback() + } + if shouldBeExpanded { + self.hapticFeedback?.impact() + } else { + self.hapticFeedback?.tap() + } + + self.headerNode.updateIsAvatarExpanded(shouldBeExpanded) + self.updateNavigationExpansionPresentation(isExpanded: shouldBeExpanded, animated: true) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition, additive: true) + } + + if !shouldBeExpanded { + //scrollView.setContentOffset(CGPoint(), animated: true) + } + } + } + } + + private func updateNavigationExpansionPresentation(isExpanded: Bool, animated: Bool) { + if let controller = self.controller { + controller.statusBar.updateStatusBarStyle(isExpanded ? .White : self.presentationData.theme.rootController.statusBarStyle.style, animated: animated) + + let baseNavigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) + let navigationBarPresentationData = NavigationBarPresentationData( + theme: NavigationBarTheme( + buttonColor: isExpanded ? .white : baseNavigationBarPresentationData.theme.buttonColor, + disabledButtonColor: baseNavigationBarPresentationData.theme.disabledButtonColor, + primaryTextColor: baseNavigationBarPresentationData.theme.primaryTextColor, + backgroundColor: .clear, + separatorColor: .clear, + badgeBackgroundColor: baseNavigationBarPresentationData.theme.badgeBackgroundColor, + badgeStrokeColor: baseNavigationBarPresentationData.theme.badgeStrokeColor, + badgeTextColor: baseNavigationBarPresentationData.theme.badgeTextColor + ), strings: baseNavigationBarPresentationData.strings) + + if let navigationBar = controller.navigationBar { + if animated { + UIView.transition(with: navigationBar.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { + }, completion: nil) + } + navigationBar.updatePresentationData(navigationBarPresentationData) + } + } } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { guard let (_, navigationHeight) = self.validLayout else { return } - let snapDurationFactor = max(0.5, min(1.5, abs(velocity.y) * 0.8)) - - var snapToOffset: CGFloat? - let offset = targetContentOffset.pointee.y - - let headerMaxOffset = self.headerNode.bounds.height - navigationHeight - let collapsedPanesOffset = max(0.0, scrollView.contentSize.height - floor(scrollNode.bounds.height * 1.5)) - let expandedPanesOffset = scrollView.contentSize.height - self.scrollNode.bounds.height - - if offset > collapsedPanesOffset { - if velocity.y < 0.0 { - var targetOffset = collapsedPanesOffset - if targetOffset < headerMaxOffset { - targetOffset = 0.0 - } - snapToOffset = targetOffset + if targetContentOffset.pointee.y < 212.0 { + if targetContentOffset.pointee.y < 212.0 / 2.0 { + targetContentOffset.pointee.y = 0.0 } else { - snapToOffset = expandedPanesOffset - } - } else if offset < headerMaxOffset && offset > 0.0 { - let directionIsDown: Bool - if abs(velocity.y) > 0.2 { - directionIsDown = velocity.y >= 0.0 - } else { - directionIsDown = offset >= headerMaxOffset / 2.0 - } - - if directionIsDown { - snapToOffset = headerMaxOffset - } else { - snapToOffset = 0.0 - } - } else if self.isPaneAreaExpanded && offset < expandedPanesOffset { - let directionIsDown: Bool - if abs(velocity.y) > 0.2 { - directionIsDown = velocity.y >= 0.0 - } else { - directionIsDown = offset >= headerMaxOffset / 2.0 - } - - if directionIsDown { - snapToOffset = headerMaxOffset - } else { - snapToOffset = 0.0 - } - } - - if let snapToOffset = snapToOffset { - targetContentOffset.pointee = scrollView.contentOffset - DispatchQueue.main.async { - let isPaneAreaExpanded = abs(snapToOffset - expandedPanesOffset) < CGFloat.ulpOfOne ? true : false - self.isPaneAreaExpanded = isPaneAreaExpanded - let currentOffset = scrollView.contentOffset - let transition: ContainedViewLayoutTransition = .animated(duration: 0.3 * Double(1.0 / snapDurationFactor), curve: .spring) - self.ignoreScrolling = true - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: snapToOffset), size: self.scrollNode.bounds.size)) - self.ignoreScrolling = false - if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition) - } + targetContentOffset.pointee.y = 212.0 } } } @@ -1541,6 +2185,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD public final class PeerInfoScreen: ViewController { private let context: AccountContext private let peerId: PeerId + private let avatarInitiallyExpanded: Bool private var presentationData: PresentationData @@ -1553,16 +2198,17 @@ public final class PeerInfoScreen: ViewController { return self._ready } - public init(context: AccountContext, peerId: PeerId) { + public init(context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool = false) { self.context = context self.peerId = peerId + self.avatarInitiallyExpanded = avatarInitiallyExpanded self.presentationData = context.sharedContext.currentPresentationData.with { $0 } let baseNavigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) super.init(navigationBarPresentationData: NavigationBarPresentationData( theme: NavigationBarTheme( - buttonColor: baseNavigationBarPresentationData.theme.buttonColor, + buttonColor: avatarInitiallyExpanded ? .white : baseNavigationBarPresentationData.theme.buttonColor, disabledButtonColor: baseNavigationBarPresentationData.theme.disabledButtonColor, primaryTextColor: baseNavigationBarPresentationData.theme.primaryTextColor, backgroundColor: .clear, @@ -1571,8 +2217,20 @@ public final class PeerInfoScreen: ViewController { badgeStrokeColor: baseNavigationBarPresentationData.theme.badgeStrokeColor, badgeTextColor: baseNavigationBarPresentationData.theme.badgeTextColor ), strings: baseNavigationBarPresentationData.strings)) + self.navigationBar?.makeCustomTransitionNode = { [weak self] other in + guard let strongSelf = self else { + return nil + } + if strongSelf.controllerNode.scrollNode.view.contentOffset.y > .ulpOfOne { + return nil + } + if let tag = other.userInfo as? PeerInfoNavigationSourceTag, tag.peerId == peerId { + return PeerInfoNavigationTransitionNode(screenNode: strongSelf.controllerNode, presentationData: strongSelf.presentationData, headerNode: strongSelf.controllerNode.headerNode) + } + return nil + } - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + self.statusBar.statusBarStyle = avatarInitiallyExpanded ? .White : self.presentationData.theme.rootController.statusBarStyle.style self.scrollToTop = { [weak self] in self?.controllerNode.scrollToTop() @@ -1584,7 +2242,7 @@ public final class PeerInfoScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId) + self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded) self._ready.set(self.controllerNode.ready.get()) @@ -1616,3 +2274,162 @@ private func getUserPeer(postbox: Postbox, peerId: PeerId) -> Signal<(Peer?, Cac return (resultPeer, resultPeer.flatMap({ transaction.getPeerCachedData(peerId: $0.id) })) } } + +final class PeerInfoNavigationSourceTag { + let peerId: PeerId + + init(peerId: PeerId) { + self.peerId = peerId + } +} + +private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavigationTransitionNode { + private let screenNode: PeerInfoScreenNode + private let presentationData: PresentationData + + private var topNavigationBar: NavigationBar? + private var bottomNavigationBar: NavigationBar? + + private let headerNode: PeerInfoHeaderNode + + private var previousBackButtonArrow: ASDisplayNode? + private var currentBackButtonArrow: ASDisplayNode? + private var previousBackButtonBadge: ASDisplayNode? + private var previousRightButton: ASDisplayNode? + private var currentBackButton: ASDisplayNode? + + private var previousTitleNode: (ASDisplayNode, TextNode)? + private var previousStatusNode: (ASDisplayNode, ASDisplayNode)? + + private var didSetup: Bool = false + + init(screenNode: PeerInfoScreenNode, presentationData: PresentationData, headerNode: PeerInfoHeaderNode) { + self.screenNode = screenNode + self.presentationData = presentationData + self.headerNode = headerNode + + super.init() + + self.addSubnode(headerNode) + } + + func setup(topNavigationBar: NavigationBar, bottomNavigationBar: NavigationBar) { + self.topNavigationBar = topNavigationBar + self.bottomNavigationBar = bottomNavigationBar + + topNavigationBar.isHidden = true + bottomNavigationBar.isHidden = true + + if let previousBackButtonArrow = bottomNavigationBar.makeTransitionBackArrowNode(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { + self.previousBackButtonArrow = previousBackButtonArrow + self.addSubnode(previousBackButtonArrow) + } + if self.screenNode.headerNode.isAvatarExpanded, let currentBackButtonArrow = topNavigationBar.makeTransitionBackArrowNode(accentColor: self.screenNode.headerNode.isAvatarExpanded ? .white : self.presentationData.theme.rootController.navigationBar.accentTextColor) { + self.currentBackButtonArrow = currentBackButtonArrow + self.addSubnode(currentBackButtonArrow) + } + if let previousBackButtonBadge = bottomNavigationBar.makeTransitionBadgeNode() { + self.previousBackButtonBadge = previousBackButtonBadge + self.addSubnode(previousBackButtonBadge) + } + if let previousRightButton = bottomNavigationBar.makeTransitionRightButtonNode(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { + self.previousRightButton = previousRightButton + self.addSubnode(previousRightButton) + } + if let currentBackButton = topNavigationBar.makeTransitionBackButtonNode(accentColor: self.screenNode.headerNode.isAvatarExpanded ? .white : self.presentationData.theme.rootController.navigationBar.accentTextColor) { + self.currentBackButton = currentBackButton + self.addSubnode(currentBackButton) + } + if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView { + let previousTitleNode = previousTitleView.titleNode.makeCopy() + let previousTitleContainerNode = ASDisplayNode() + previousTitleContainerNode.addSubnode(previousTitleNode) + self.previousTitleNode = (previousTitleContainerNode, previousTitleNode) + self.addSubnode(previousTitleContainerNode) + + let previousStatusNode = previousTitleView.activityNode.makeCopy() + let previousStatusContainerNode = ASDisplayNode() + previousStatusContainerNode.addSubnode(previousStatusNode) + self.previousStatusNode = (previousStatusContainerNode, previousStatusNode) + self.addSubnode(previousStatusContainerNode) + } + } + + func update(containerSize: CGSize, fraction: CGFloat, transition: ContainedViewLayoutTransition) { + guard let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar else { + return + } + + if let previousBackButtonArrow = self.previousBackButtonArrow { + let previousBackButtonArrowFrame = bottomNavigationBar.backButtonArrow.view.convert(bottomNavigationBar.backButtonArrow.view.bounds, to: bottomNavigationBar.view) + previousBackButtonArrow.frame = previousBackButtonArrowFrame + } + + if let currentBackButtonArrow = self.currentBackButtonArrow { + let currentBackButtonArrowFrame = topNavigationBar.backButtonArrow.view.convert(topNavigationBar.backButtonArrow.view.bounds, to: topNavigationBar.view) + currentBackButtonArrow.frame = currentBackButtonArrowFrame + + transition.updateAlpha(node: currentBackButtonArrow, alpha: 1.0 - fraction) + if let previousBackButtonArrow = self.previousBackButtonArrow { + transition.updateAlpha(node: previousBackButtonArrow, alpha: fraction) + } + } + + if let previousBackButtonBadge = self.previousBackButtonBadge { + let previousBackButtonBadgeFrame = bottomNavigationBar.badgeNode.view.convert(bottomNavigationBar.badgeNode.view.bounds, to: bottomNavigationBar.view) + previousBackButtonBadge.frame = previousBackButtonBadgeFrame + + transition.updateAlpha(node: previousBackButtonBadge, alpha: fraction) + } + + if let previousRightButton = self.previousRightButton { + let previousRightButtonFrame = bottomNavigationBar.rightButtonNode.view.convert(bottomNavigationBar.rightButtonNode.view.bounds, to: bottomNavigationBar.view) + previousRightButton.frame = previousRightButtonFrame + transition.updateAlpha(node: previousRightButton, alpha: fraction) + } + + if let currentBackButton = self.currentBackButton { + let currentBackButtonFrame = topNavigationBar.backButtonNode.view.convert(topNavigationBar.backButtonNode.view.bounds, to: topNavigationBar.view) + transition.updateFrame(node: currentBackButton, frame: currentBackButtonFrame.offsetBy(dx: fraction * 12.0, dy: 0.0)) + + transition.updateAlpha(node: currentBackButton, alpha: (1.0 - fraction)) + } + + if let previousTitleView = bottomNavigationBar.titleView as? ChatTitleView, let avatarNode = previousTitleView.avatarNode, let (previousTitleContainerNode, previousTitleNode) = self.previousTitleNode, let (previousStatusContainerNode, previousStatusNode) = self.previousStatusNode { + let previousTitleFrame = previousTitleView.titleNode.view.convert(previousTitleView.titleNode.bounds, to: bottomNavigationBar.view) + let previousStatusFrame = previousTitleView.activityNode.view.convert(previousTitleView.activityNode.bounds, to: bottomNavigationBar.view) + + self.headerNode.navigationTransition = PeerInfoHeaderNavigationTransition(sourceNavigationBar: bottomNavigationBar, sourceTitleView: previousTitleView, sourceTitleFrame: previousTitleFrame, sourceSubtitleFrame: previousStatusFrame, fraction: fraction) + if let (layout, navigationHeight) = self.screenNode.validLayout { + self.headerNode.update(width: layout.size.width, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: topNavigationBar.bounds.height, contentOffset: 0.0, presentationData: self.presentationData, peer: self.screenNode.data?.peer, cachedData: self.screenNode.data?.cachedData, notificationSettings: self.screenNode.data?.notificationSettings, presence: self.screenNode.data?.presence, transition: transition, additive: false) + } + + let titleScale = (fraction * previousTitleNode.bounds.height + (1.0 - fraction) * self.headerNode.titleNode.bounds.height) / previousTitleNode.bounds.height + let subtitleScale = (fraction * previousStatusNode.bounds.height + (1.0 - fraction) * self.headerNode.subtitleNode.bounds.height) / previousStatusNode.bounds.height + + transition.updateFrame(node: previousTitleContainerNode, frame: CGRect(origin: self.headerNode.titleNodeRawContainer.frame.origin.offsetBy(dx: previousTitleFrame.size.width * 0.5 * (titleScale - 1.0), dy: previousTitleFrame.size.height * 0.5 * (titleScale - 1.0)), size: previousTitleFrame.size)) + transition.updateFrame(node: previousTitleNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: previousTitleFrame.size)) + transition.updateFrame(node: previousStatusContainerNode, frame: CGRect(origin: self.headerNode.subtitleNodeRawContainer.frame.origin.offsetBy(dx: previousStatusFrame.size.width * 0.5 * (subtitleScale - 1.0), dy: previousStatusFrame.size.height * 0.5 * (subtitleScale - 1.0)), size: previousStatusFrame.size)) + transition.updateFrame(node: previousStatusNode, frame: CGRect(origin: CGPoint(), size: previousStatusFrame.size)) + + transition.updateSublayerTransformScale(node: previousTitleContainerNode, scale: titleScale) + transition.updateSublayerTransformScale(node: previousStatusContainerNode, scale: subtitleScale) + + transition.updateAlpha(node: self.headerNode.titleNode, alpha: (1.0 - fraction)) + transition.updateAlpha(node: previousTitleNode, alpha: fraction) + transition.updateAlpha(node: self.headerNode.subtitleNode, alpha: (1.0 - fraction)) + transition.updateAlpha(node: previousStatusNode, alpha: fraction) + } + } + + func restore() { + guard let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar else { + return + } + + topNavigationBar.isHidden = false + bottomNavigationBar.isHidden = false + self.headerNode.navigationTransition = nil + self.screenNode.insertSubnode(self.headerNode, aboveSubnode: self.screenNode.scrollNode) + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift index 6863ff66a1..7374fc9577 100644 --- a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift @@ -759,7 +759,7 @@ public class PeerMediaCollectionController: TelegramBaseController { |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) } } diff --git a/submodules/TelegramUI/TelegramUI/PollResultsController.swift b/submodules/TelegramUI/TelegramUI/PollResultsController.swift index 177ca29828..a1a24b756a 100644 --- a/submodules/TelegramUI/TelegramUI/PollResultsController.swift +++ b/submodules/TelegramUI/TelegramUI/PollResultsController.swift @@ -303,7 +303,7 @@ public func pollResultsController(context: AccountContext, messageId: MessageId, }) }, openPeer: { peer in if let peer = peer.peers[peer.peerId] { - if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { pushControllerImpl?(controller) } } diff --git a/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift b/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift index 160fa6cfcc..50cdd8a69d 100644 --- a/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift +++ b/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift @@ -1004,8 +1004,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { handleTextLinkActionImpl(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink) } - public func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode) -> ViewController? { - let controller = peerInfoControllerImpl(context: context, peer: peer, mode: mode) + public func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool) -> ViewController? { + let controller = peerInfoControllerImpl(context: context, peer: peer, mode: mode, avatarInitiallyExpanded: avatarInitiallyExpanded) controller?.navigationPresentation = .modalInLargeLayout return controller } @@ -1245,7 +1245,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let defaultChatControllerInteraction = ChatControllerInteraction.default -private func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode) -> ViewController? { +private func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool) -> ViewController? { if let _ = peer as? TelegramGroup { return groupInfoController(context: context, peerId: peer.id) } else if let channel = peer as? TelegramChannel { @@ -1255,7 +1255,7 @@ private func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: P return channelInfoController(context: context, peerId: peer.id) } } else if peer is TelegramUser { - return PeerInfoScreen(context: context, peerId: peer.id) + return PeerInfoScreen(context: context, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded) } else if peer is TelegramSecretChat { return userInfoController(context: context, peerId: peer.id, mode: mode) } diff --git a/submodules/TelegramUI/TelegramUI/SharedWakeupManager.swift b/submodules/TelegramUI/TelegramUI/SharedWakeupManager.swift index 962ac8a234..638751b63f 100644 --- a/submodules/TelegramUI/TelegramUI/SharedWakeupManager.swift +++ b/submodules/TelegramUI/TelegramUI/SharedWakeupManager.swift @@ -315,7 +315,7 @@ public final class SharedWakeupManager { if let taskId = self.beginBackgroundTask("background-wakeup", { handleExpiration() }) { - let timer = SwiftSignalKit.Timer(timeout: min(30.0, self.backgroundTimeRemaining()), repeat: false, completion: { + let timer = SwiftSignalKit.Timer(timeout: min(30.0, max(0.0, self.backgroundTimeRemaining() - 5.0)), repeat: false, completion: { handleExpiration() }, queue: Queue.mainQueue()) self.currentTask = (taskId, currentTime, timer) diff --git a/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift b/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift index cc46c4172e..d8d1fd0fb2 100644 --- a/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift +++ b/submodules/TelegramUI/TelegramUI/TextLinkHandling.swift @@ -32,7 +32,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate peerSignal = context.account.postbox.loadedPeerWithId(peerId) |> map(Optional.init) navigateDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { peer in if let controller = controller, let peer = peer { - if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false) { (controller.navigationController as? NavigationController)?.pushViewController(infoController) } }