From 8afd80c31d1103ca0fd42464fc1be0964f6e58b2 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sat, 1 Feb 2020 00:15:18 +0400 Subject: [PATCH 1/2] Basic implementation of the redesigned profiles --- .../AvatarNode/Sources/PeerAvatar.swift | 12 +- .../ContainedViewLayoutTransition.swift | 18 + submodules/Display/Display/ListView.swift | 2 +- .../Display/Display/ListViewScroller.swift | 16 +- .../Sources/PeerInfoController.swift | 14 - .../ButtonMessage.imageset/Contents.json | 20 + .../TelegramUI/PeerInfoFilesPane.swift | 174 ++ .../TelegramUI/PeerInfoScreen.swift | 1618 +++++++++++++++++ .../PeerInfoScreenLabeledValueItem.swift | 123 ++ ...erInfoScreenSelectableBackgroundNode.swift | 55 + .../TelegramUI/PeerInfoVisualMediaPane.swift | 458 +++++ .../TelegramUI/SharedAccountContext.swift | 17 + 12 files changed, 2500 insertions(+), 27 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json create mode 100644 submodules/TelegramUI/TelegramUI/PeerInfoFilesPane.swift create mode 100644 submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift create mode 100644 submodules/TelegramUI/TelegramUI/PeerInfoScreenLabeledValueItem.swift create mode 100644 submodules/TelegramUI/TelegramUI/PeerInfoScreenSelectableBackgroundNode.swift create mode 100644 submodules/TelegramUI/TelegramUI/PeerInfoVisualMediaPane.swift diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index 183a06137a..f9dee458bd 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -77,10 +77,18 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { context.clear(CGRect(origin: CGPoint(), size: displayDimensions)) context.setBlendMode(.copy) + + if round && displayDimensions.width != 60.0 { + context.addEllipse(in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + context.clip() + } + context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) if round { - context.setBlendMode(.destinationOut) - context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + if displayDimensions.width == 60.0 { + context.setBlendMode(.destinationOut) + context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) + } } } else { if let emptyColor = emptyColor { diff --git a/submodules/Display/Display/ContainedViewLayoutTransition.swift b/submodules/Display/Display/ContainedViewLayoutTransition.swift index 45510880e9..3df59e854a 100644 --- a/submodules/Display/Display/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Display/ContainedViewLayoutTransition.swift @@ -141,6 +141,24 @@ public extension ContainedViewLayoutTransition { } } + func updateFrameAdditiveToCenter(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + if node.frame.equalTo(frame) && !force { + completion?(true) + } else { + switch self { + case .immediate: + node.frame = frame + 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)) + } + } + } + func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.bounds.equalTo(bounds) && !force { completion?(true) diff --git a/submodules/Display/Display/ListView.swift b/submodules/Display/Display/ListView.swift index 59fc67d902..7a65d3b39e 100644 --- a/submodules/Display/Display/ListView.swift +++ b/submodules/Display/Display/ListView.swift @@ -125,7 +125,7 @@ public enum GeneralScrollDirection { } open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGestureRecognizerDelegate { - final let scroller: ListViewScroller + public final let scroller: ListViewScroller private final var visibleSize: CGSize = CGSize() public private(set) final var insets = UIEdgeInsets() public final var visualInsets: UIEdgeInsets? diff --git a/submodules/Display/Display/ListViewScroller.swift b/submodules/Display/Display/ListViewScroller.swift index 7e8921f718..c77b2207cb 100644 --- a/submodules/Display/Display/ListViewScroller.swift +++ b/submodules/Display/Display/ListViewScroller.swift @@ -1,29 +1,27 @@ import UIKit -class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate { - override init(frame: CGRect) { +public final class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate { + override public init(frame: CGRect) { super.init(frame: frame) - #if os(iOS) self.scrollsToTop = false if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.contentInsetAdjustmentBehavior = .never } - #endif } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if otherGestureRecognizer is ListViewTapGestureRecognizer { return true } return false } - override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer is UIPanGestureRecognizer, let gestureRecognizers = gestureRecognizer.view?.gestureRecognizers { for otherGestureRecognizer in gestureRecognizers { if otherGestureRecognizer !== gestureRecognizer, let panGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer, panGestureRecognizer.minimumNumberOfTouches == 2 { @@ -36,9 +34,7 @@ class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate { } } - #if os(iOS) - override func touchesShouldCancel(in view: UIView) -> Bool { + override public func touchesShouldCancel(in view: UIView) -> Bool { return true } - #endif } diff --git a/submodules/PeerInfoUI/Sources/PeerInfoController.swift b/submodules/PeerInfoUI/Sources/PeerInfoController.swift index f34ef9a845..62e5703def 100644 --- a/submodules/PeerInfoUI/Sources/PeerInfoController.swift +++ b/submodules/PeerInfoUI/Sources/PeerInfoController.swift @@ -7,17 +7,3 @@ import TelegramCore import SyncCore import AccountContext -public func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode) -> ViewController? { - if let _ = peer as? TelegramGroup { - return groupInfoController(context: context, peerId: peer.id) - } else if let channel = peer as? TelegramChannel { - if case .group = channel.info { - return groupInfoController(context: context, peerId: peer.id) - } else { - return channelInfoController(context: context, peerId: peer.id) - } - } else if peer is TelegramUser || peer is TelegramSecretChat { - return userInfoController(context: context, peerId: peer.id, mode: mode) - } - return nil -} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json new file mode 100644 index 0000000000..f8f827e40b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoFilesPane.swift b/submodules/TelegramUI/TelegramUI/PeerInfoFilesPane.swift new file mode 100644 index 0000000000..112682fe33 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfoFilesPane.swift @@ -0,0 +1,174 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import AccountContext +import ContextUI +import PhotoResources +import TelegramUIPreferences + +final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode { + private let context: AccountContext + private let peerId: PeerId + + private let listNode: ChatHistoryListNode + + private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? + + private let ready = Promise() + private var didSetReady: Bool = false + var isReady: Signal { + return self.ready.get() + } + + private var hiddenMediaDisposable: Disposable? + + init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId, tagMask: MessageTags) { + self.context = context + self.peerId = peerId + + var openMessageImpl: ((MessageId) -> Bool)? + let controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in + return openMessageImpl?(message.id) ?? false + }, openPeer: { _, _, _ in + }, openPeerMention: { _ in + }, openMessageContextMenu: { _, _, _, _, _ in + }, openMessageContextActions: { _, _, _, _ in + }, navigateToMessage: { _, _ in + }, tapMessage: nil, clickThroughMessage: { + }, toggleMessagesSelection: { _, _ in + }, sendCurrentMessage: { _ in + }, sendMessage: { _ in + }, sendSticker: { _, _, _, _ in + return false + }, sendGif: { _, _, _ in + return false + }, requestMessageActionCallback: { _, _, _ in + }, requestMessageActionUrlAuth: { _, _, _ in + }, activateSwitchInline: { _, _ in + }, openUrl: { _, _, _, _ in + }, shareCurrentLocation: { + }, shareAccountContact: { + }, sendBotCommand: { _, _ in + }, openInstantPage: { _, _ in + }, openWallpaper: { _ in + }, openTheme: {_ in + }, openHashtag: { _, _ in + }, updateInputState: { _ in + }, updateInputMode: { _ in + }, openMessageShareMenu: { _ in + }, presentController: { _, _ in + }, navigationController: { + return nil + }, chatControllerNode: { + return nil + }, reactionContainerNode: { + return nil + }, presentGlobalOverlayController: { _, _ in + }, callPeer: { _ in + }, longTap: { _, _ in + }, openCheckoutOrReceipt: { _ in + }, openSearch: { + }, setupReply: { _ in + }, canSetupReply: { _ in + return false + }, navigateToFirstDateMessage: { _ in + }, requestRedeliveryOfFailedMessages: { _ in + }, addContact: { _ in + }, rateCall: { _, _ in + }, requestSelectMessagePollOptions: { _, _ in + }, requestOpenMessagePollResults: { _, _ in + }, openAppStorePage: { + }, displayMessageTooltip: { _, _, _, _ in + }, seekToTimecode: { _, _, _ in + }, scheduleCurrentMessage: { + }, sendScheduledMessagesNow: { _ in + }, editScheduledMessagesTime: { _ in + }, performTextSelectionAction: { _, _, _ in + }, updateMessageReaction: { _, _ in + }, openMessageReactions: { _ in + }, displaySwipeToReplyHint: { + }, dismissReplyMarkupMessage: { _ in + }, openMessagePollResults: { _, _ in + }, openPollCreation: { _ in + }, requestMessageUpdate: { _ in + }, cancelInteractiveKeyboardGestures: { + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) + + self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: false)) + + super.init() + + openMessageImpl = { id in + return openMessage(id) + } + + self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in + guard let strongSelf = self else { + return + } + var hiddenMedia: [MessageId: [Media]] = [:] + for id in ids { + if case let .chat(accountId, messageId, media) = id, accountId == strongSelf.context.account.id { + hiddenMedia[messageId] = [media] + } + } + controllerInteraction.hiddenMedia = hiddenMedia + strongSelf.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListMessageNode { + itemNode.updateHiddenMedia() + } + } + }) + + self.listNode.preloadPages = true + self.addSubnode(self.listNode) + + self.ready.set(self.listNode.historyState.get() + |> take(1) + |> map { _ -> Bool in true }) + } + + deinit { + self.hiddenMediaDisposable?.dispose() + } + + func scrollToTop() -> Bool { + let offset = self.listNode.visibleContentOffset() + switch offset { + case let .known(value) where value <= CGFloat.ulpOfOne: + return false + default: + self.listNode.scrollToEndOfHistory() + return true + } + } + + func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, isScrollingLockedAtTop, presentationData) + + transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(), duration: duration, curve: curve)) + self.listNode.scrollEnabled = !isScrollingLockedAtTop + } + + func findLoadedMessage(id: MessageId) -> Message? { + self.listNode.messageInCurrentHistoryView(id) + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + var transitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListMessageNode { + if let result = itemNode.transitionNode(id: messageId, media: media) { + transitionNode = result + } + } + } + return transitionNode + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift b/submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift new file mode 100644 index 0000000000..e1f5116a47 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift @@ -0,0 +1,1618 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SyncCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import TelegramUIPreferences +import AvatarNode +import TelegramStringFormatting +import PhoneNumberFormat +import AppBundle +import PresentationDataUtils +import NotificationMuteSettingsUI +import NotificationSoundSelectionUI +import OverlayStatusController +import ShareController + +private let avatarFont = avatarPlaceholderFont(size: 28.0) + +private enum PeerInfoHeaderButtonKey: Hashable { + case message + case call + case mute + case more +} + +private enum PeerInfoHeaderButtonIcon { + case message + case call + case mute + case unmute + case more +} + +private final class PeerInfoHeaderButtonNode: HighlightableButtonNode { + let key: PeerInfoHeaderButtonKey + private let action: (PeerInfoHeaderButtonNode) -> Void + private let backgroundNode: ASImageNode + private let textNode: ImmediateTextNode + + private var theme: PresentationTheme? + private var icon: PeerInfoHeaderButtonIcon? + + init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderButtonNode) -> Void) { + self.key = key + self.action = action + + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.layer.removeAnimation(forKey: "opacity") + strongSelf.alpha = 0.4 + } else { + strongSelf.alpha = 1.0 + strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + @objc private func buttonPressed() { + self.action(self) + } + + func update(size: CGSize, text: String, icon: PeerInfoHeaderButtonIcon, isExpanded: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + if self.theme != presentationData.theme || self.icon != icon { + self.theme = presentationData.theme + self.icon = icon + self.backgroundNode.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in + 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) + let imageName: String + switch icon { + case .message: + imageName = "Chat/Context Menu/Message" + case .call: + imageName = "Chat/Context Menu/Call" + case .mute: + imageName = "Chat/Context Menu/Muted" + case .unmute: + imageName = "Chat/Context Menu/Unmute" + case .more: + imageName = "Chat/Context Menu/More" + } + 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) + context.clip(to: imageRect, mask: image.cgImage!) + context.fill(imageRect) + } + }) + } + + 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.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)) + } +} + +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 var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:] + private let backgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + + var performButtonAction: ((PeerInfoHeaderButtonKey) -> Void)? + + init(context: AccountContext) { + self.context = context + + self.avatarNode = AvatarNode(font: avatarFont) + + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + + self.subtitleNode = ImmediateTextNode() + self.subtitleNode.displaysAsynchronously = false + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.avatarNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode) + } + + func update(width: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, presence: TelegramUserPresence?, transition: ContainedViewLayoutTransition) -> CGFloat { + self.presentationData = presentationData + + self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let avatarSize: CGFloat = 100.0 + let defaultButtonSize: CGFloat = 40.0 + let defaultMaxButtonSpacing: CGFloat = 40.0 + + var buttonKeys: [PeerInfoHeaderButtonKey] = [] + + if let peer = peer { + buttonKeys.append(.message) + buttonKeys.append(.call) + 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) + + let presence = presence ?? TelegramUserPresence(status: .none, lastActivity: 0) + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (subtitleString, activity) = stringAndActivityForUserPresence(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, presence: presence, relativeTo: Int32(timestamp), expanded: true) + let subtitleColor: UIColor + if activity { + subtitleColor = presentationData.theme.list.itemAccentColor + } else { + subtitleColor = presentationData.theme.list.itemSecondaryTextColor + } + self.subtitleNode.attributedText = NSAttributedString(string: subtitleString, font: Font.regular(15.0), textColor: subtitleColor) + } + + let textSideInset: CGFloat = 16.0 + + var height: CGFloat = navigationHeight + height += 212.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 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 buttonSpacing: CGFloat = min(defaultMaxButtonSpacing, width - floor(CGFloat(buttonKeys.count) * defaultButtonSize / CGFloat(buttonKeys.count + 1))) + 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) + for buttonKey in buttonKeys.reversed() { + let buttonNode: PeerInfoHeaderButtonNode + var wasAdded = false + if let current = self.buttonNodes[buttonKey] { + buttonNode = current + } else { + wasAdded = true + buttonNode = PeerInfoHeaderButtonNode(key: buttonKey, action: { [weak self] buttonNode in + self?.buttonPressed(buttonNode) + }) + self.buttonNodes[buttonKey] = buttonNode + 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 buttonTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition + buttonTransition.updateFrame(node: buttonNode, frame: buttonFrame) + let buttonText: String + let buttonIcon: PeerInfoHeaderButtonIcon + switch buttonKey { + case .message: + buttonText = "Message" + buttonIcon = .message + case .call: + buttonText = "Call" + buttonIcon = .call + case .mute: + if let notificationSettings = notificationSettings, case .muted = notificationSettings.muteState { + buttonText = "Unmute" + buttonIcon = .unmute + } else { + buttonText = "Mute" + buttonIcon = .mute + } + case .more: + buttonText = "More" + buttonIcon = .more + } + buttonNode.update(size: buttonFrame.size, text: buttonText, icon: buttonIcon, isExpanded: false, presentationData: presentationData, transition: buttonTransition) + } + + for key in self.buttonNodes.keys { + if !buttonKeys.contains(key) { + if let buttonNode = self.buttonNodes[key] { + self.buttonNodes.removeValue(forKey: key) + buttonNode.removeFromSupernode() + } + } + } + + 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))) + + return height + } + + private func buttonPressed(_ buttonNode: PeerInfoHeaderButtonNode) { + self.performButtonAction?(buttonNode.key) + } +} + +protocol PeerInfoPaneNode: ASDisplayNode { + var isReady: Signal { get } + + func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) + func scrollToTop() -> Bool + func findLoadedMessage(id: MessageId) -> Message? + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? +} + +private final class PeerInfoPaneWrapper { + let key: PeerInfoPaneKey + let node: PeerInfoPaneNode + private var appliedParams: (CGSize, Bool, PresentationData)? + + init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) { + self.key = key + self.node = node + } + + func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + if let (currentSize, currentIsScrollingLockedAtTop, currentPresentationData) = self.appliedParams { + if currentSize == size && currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentPresentationData === presentationData { + return + } + } + self.appliedParams = (size, isScrollingLockedAtTop, presentationData) + self.node.update(size: size, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: synchronous, transition: transition) + } +} + +private enum PeerInfoPaneKey { + case media + case files + case links + case music +} + +private final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { + private let pressed: () -> Void + + private let titleNode: ImmediateTextNode + private let buttonNode: HighlightTrackingButtonNode + + init(pressed: @escaping () -> Void) { + self.pressed = pressed + + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + + self.buttonNode = HighlightTrackingButtonNode() + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.titleNode.alpha = 0.4 + } else { + strongSelf.titleNode.alpha = 1.0 + strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + @objc private func buttonPressed() { + self.pressed() + } + + func updateText(_ title: String, isSelected: Bool, presentationData: PresentationData) { + self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) + } + + func updateLayout(height: CGFloat) -> CGFloat { + let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize) + return titleSize.width + } + + func updateArea(size: CGSize, sideInset: CGFloat) { + self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height)) + } +} + +private struct PeerInfoPaneSpecifier: Equatable { + var key: PeerInfoPaneKey + var title: String +} + +private final class PeerInfoPaneTabsContainerNode: ASDisplayNode { + private let scrollNode: ASScrollNode + private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:] + private let selectedLineNode: ASImageNode + + private var currentParams: ([PeerInfoPaneSpecifier], PeerInfoPaneKey?, PresentationData)? + + var requestSelectPane: ((PeerInfoPaneKey) -> Void)? + + override init() { + self.scrollNode = ASScrollNode() + + self.selectedLineNode = ASImageNode() + self.selectedLineNode.displaysAsynchronously = false + self.selectedLineNode.displayWithoutProcessing = true + + super.init() + + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.scrollsToTop = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + + self.addSubnode(self.scrollNode) + self.scrollNode.addSubnode(self.selectedLineNode) + } + + func update(size: CGSize, presentationData: PresentationData, paneList: [PeerInfoPaneSpecifier], selectedPane: PeerInfoPaneKey?, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) + + if self.currentParams?.2.theme !== presentationData.theme { + self.selectedLineNode.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))) + })?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 1) + } + + if self.currentParams?.0 != paneList || self.currentParams?.1 != selectedPane || self.currentParams?.2 !== presentationData { + self.currentParams = (paneList, selectedPane, presentationData) + for specifier in paneList { + let paneNode: PeerInfoPaneTabsContainerPaneNode + var wasAdded = false + if let current = self.paneNodes[specifier.key] { + paneNode = current + } else { + wasAdded = true + paneNode = PeerInfoPaneTabsContainerPaneNode(pressed: { [weak self] in + self?.paneSelected(specifier.key) + }) + self.paneNodes[specifier.key] = paneNode + self.scrollNode.addSubnode(paneNode) + } + paneNode.updateText(specifier.title, isSelected: selectedPane == specifier.key, presentationData: presentationData) + } + var removeKeys: [PeerInfoPaneKey] = [] + for (key, _) in self.paneNodes { + if !paneList.contains(where: { $0.key == key }) { + removeKeys.append(key) + } + } + for key in removeKeys { + if let paneNode = self.paneNodes.removeValue(forKey: key) { + paneNode.removeFromSupernode() + } + } + } + + var tabSizes: [(CGSize, PeerInfoPaneTabsContainerPaneNode)] = [] + var totalRawTabSize: CGFloat = 0.0 + + var selectedFrame: CGRect? + for specifier in paneList { + guard let paneNode = self.paneNodes[specifier.key] else { + continue + } + let paneNodeWidth = paneNode.updateLayout(height: size.height) + let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height) + tabSizes.append((paneNodeSize, paneNode)) + totalRawTabSize += paneNodeSize.width + } + + let spacing: CGFloat = 32.0 + if totalRawTabSize + CGFloat(tabSizes.count + 1) * spacing <= size.width { + let singleTabSpace = floor((size.width - spacing * 2.0) / CGFloat(tabSizes.count)) + + for i in 0 ..< tabSizes.count { + let (paneNodeSize, paneNode) = tabSizes[i] + let leftOffset = spacing + CGFloat(i) * singleTabSpace + floor((singleTabSpace - paneNodeSize.width) / 2.0) + + let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + paneNode.frame = paneFrame + let areaSideInset = floor((singleTabSpace - paneFrame.size.width) / 2.0) + paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) + paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) + + if paneList[i].key == selectedPane { + selectedFrame = paneFrame + } + } + self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height) + } else { + let sideInset: CGFloat = 16.0 + var leftOffset: CGFloat = sideInset + for i in 0 ..< tabSizes.count { + let (paneNodeSize, paneNode) = tabSizes[i] + let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + paneNode.frame = paneFrame + paneNode.updateArea(size: paneFrame.size, sideInset: spacing) + paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -spacing, bottom: 0.0, right: -spacing) + if paneList[i].key == selectedPane { + selectedFrame = paneFrame + } + leftOffset += paneNodeSize.width + spacing + } + self.scrollNode.view.contentSize = CGSize(width: leftOffset + sideInset, height: size.height) + } + + if let selectedFrame = selectedFrame { + self.selectedLineNode.isHidden = false + transition.updateFrame(node: self.selectedLineNode, frame: CGRect(origin: CGPoint(x: selectedFrame.minX, y: size.height - 4.0), size: CGSize(width: selectedFrame.width, height: 4.0))) + } else { + self.selectedLineNode.isHidden = true + } + } + + private func paneSelected(_ key: PeerInfoPaneKey) { + self.requestSelectPane?(key) + } +} + +private final class PeerInfoPaneContainerNode: ASDisplayNode { + private let context: AccountContext + private let peerId: PeerId + + private let coveringBackgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let tabsContainerNode: PeerInfoPaneTabsContainerNode + private let tapsSeparatorNode: ASDisplayNode + + let isReady = Promise() + var didSetIsReady = false + + private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? + + private var availablePanes: [PeerInfoPaneKey] = [] + private var currentPaneKey: PeerInfoPaneKey? + private var currentPane: PeerInfoPaneWrapper? + + private var candidatePane: (PeerInfoPaneWrapper, Disposable)? + + var openMessage: ((MessageId) -> Bool)? + + init(context: AccountContext, peerId: PeerId) { + self.context = context + self.peerId = peerId + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.coveringBackgroundNode = ASDisplayNode() + self.coveringBackgroundNode.isLayerBacked = true + + self.tabsContainerNode = PeerInfoPaneTabsContainerNode() + + self.tapsSeparatorNode = ASDisplayNode() + self.tapsSeparatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.separatorNode) + self.addSubnode(self.coveringBackgroundNode) + self.addSubnode(self.tabsContainerNode) + self.addSubnode(self.tapsSeparatorNode) + + self.availablePanes = [.media, .files, .links, .music] + self.currentPaneKey = .media + + self.tabsContainerNode.requestSelectPane = { [weak self] key in + guard let strongSelf = self else { + return + } + if strongSelf.currentPaneKey == key { + return + } + + let paneNode: PeerInfoPaneNode + switch key { + case .media: + paneNode = PeerInfoVisualMediaPaneNode(context: strongSelf.context, openMessage: { id in + return self?.openMessage?(id) ?? false + }, peerId: strongSelf.peerId) + case .files: + paneNode = PeerInfoListPaneNode(context: strongSelf.context, openMessage: { id in + return self?.openMessage?(id) ?? false + }, peerId: strongSelf.peerId, tagMask: .file) + case .links: + paneNode = PeerInfoListPaneNode(context: strongSelf.context, openMessage: { id in + return self?.openMessage?(id) ?? false + }, peerId: strongSelf.peerId, tagMask: .webPage) + case .music: + paneNode = PeerInfoListPaneNode(context: strongSelf.context, openMessage: { id in + return self?.openMessage?(id) ?? false + }, peerId: strongSelf.peerId, tagMask: .music) + } + + if let (_, disposable) = strongSelf.candidatePane { + disposable.dispose() + } + + 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) + } + + disposable.set((paneNode.isReady + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + guard let strongSelf = self else { + return + } + if let (candidatePane, _) = strongSelf.candidatePane { + let previousPane = strongSelf.currentPane + strongSelf.candidatePane = nil + 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 previousPane = previousPane { + let directionToRight: Bool + if let previousIndex = strongSelf.availablePanes.index(of: previousPane.key), let updatedIndex = strongSelf.availablePanes.index(of: candidatePane.key) { + directionToRight = previousIndex < updatedIndex + } else { + directionToRight = false + } + + let offset: CGFloat = directionToRight ? previousPane.node.bounds.width : -previousPane.node.bounds.width + candidatePane.node.layer.animatePosition(from: CGPoint(x: offset, y: 0.0), to: CGPoint(), duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + let previousNode = previousPane.node + previousNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -offset, y: 0.0), duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak previousNode] _ in + previousNode?.removeFromSupernode() + }) + } + } else { + if let previousPane = previousPane { + previousPane.node.removeFromSupernode() + } + } + } + })) + } + } + + func scrollToTop() -> Bool { + if let currentPane = self.currentPane { + return currentPane.node.scrollToTop() + } else { + return false + } + } + + func findLoadedMessage(id: MessageId) -> Message? { + return self.currentPane?.node.findLoadedMessage(id: id) + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return self.currentPane?.node.transitionNodeForGallery(messageId: messageId, media: media) + } + + func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, isScrollingLockedAtTop, presentationData) + + transition.updateAlpha(node: self.coveringBackgroundNode, alpha: isScrollingLockedAtTop ? 0.0 : 1.0) + + self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + self.coveringBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.tapsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + 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.tapsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + + transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: tabsHeight))) + self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: self.availablePanes.map { key in + let title: String + switch key { + case .media: + title = "Media" + case .files: + title = "Files" + case .links: + title = "Links" + case .music: + title = "Audio" + } + return PeerInfoPaneSpecifier(key: key, title: title) + }, selectedPane: self.currentPaneKey, transition: transition) + + let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight)) + + if self.currentPane?.key != self.currentPaneKey { + if let currentPane = self.currentPane { + currentPane.node.removeFromSupernode() + self.currentPane = nil + } + + if let currentPaneKey = self.currentPaneKey { + let paneNode: PeerInfoPaneNode + switch currentPaneKey { + case .media: + paneNode = PeerInfoVisualMediaPaneNode(context: self.context, openMessage: { [weak self] id in + return self?.openMessage?(id) ?? false + }, peerId: self.peerId) + case .files: + paneNode = PeerInfoListPaneNode(context: self.context, openMessage: { [weak self] id in + return self?.openMessage?(id) ?? false + }, peerId: self.peerId, tagMask: .file) + case .links: + paneNode = PeerInfoListPaneNode(context: self.context, openMessage: { [weak self] id in + return self?.openMessage?(id) ?? false + }, peerId: self.peerId, tagMask: .webPage) + case .music: + paneNode = PeerInfoListPaneNode(context: self.context, openMessage: { [weak self] id in + return self?.openMessage?(id) ?? false + }, peerId: self.peerId, tagMask: .music) + } + self.currentPane = PeerInfoPaneWrapper(key: currentPaneKey, node: paneNode) + } + } + + if let currentPane = self.currentPane { + let paneWasAdded = currentPane.node.supernode == nil + if paneWasAdded { + self.addSubnode(currentPane.node) + } + + 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) + } + 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) + } + if !self.didSetIsReady { + self.didSetIsReady = true + if let currentPane = self.currentPane { + self.isReady.set(currentPane.node.isReady) + } else { + self.isReady.set(.single(true)) + } + } + } +} + +protocol PeerInfoScreenItem: class { + var id: AnyHashable { get } + func node() -> PeerInfoScreenItemNode +} + +class PeerInfoScreenItemNode: ASDisplayNode { + var bringToFrontForHighlight: (() -> Void)? + + func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + preconditionFailure() + } +} + +private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { + let id: AnyHashable + + private let backgroundNode: ASDisplayNode + private let topSeparatorNode: ASDisplayNode + private let bottomSeparatorNode: ASDisplayNode + + private var currentItems: [PeerInfoScreenItem] = [] + private var itemNodes: [AnyHashable: PeerInfoScreenItemNode] = [:] + + init(id: AnyHashable) { + self.id = id + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.topSeparatorNode = ASDisplayNode() + self.topSeparatorNode.isLayerBacked = true + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.topSeparatorNode) + self.addSubnode(self.bottomSeparatorNode) + } + + func update(width: CGFloat, presentationData: PresentationData, items: [PeerInfoScreenItem], transition: ContainedViewLayoutTransition) -> CGFloat { + self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + self.topSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + var contentHeight: CGFloat = 0.0 + + for i in 0 ..< items.count { + let item = items[i] + + let itemNode: PeerInfoScreenItemNode + var wasAdded = false + if let current = self.itemNodes[item.id] { + itemNode = current + } else { + wasAdded = true + itemNode = item.node() + self.itemNodes[item.id] = itemNode + self.addSubnode(itemNode) + itemNode.bringToFrontForHighlight = { [weak self, weak itemNode] in + guard let strongSelf = self, let itemNode = itemNode else { + return + } + strongSelf.view.bringSubviewToFront(itemNode.view) + } + } + + let itemTransition: ContainedViewLayoutTransition = wasAdded ? .immediate : transition + + let itemHeight = itemNode.update(width: width, presentationData: presentationData, item: item, topItem: i == 0 ? nil : items[i - 1], bottomItem: (i == items.count - 1) ? nil : items[i + 1], transition: itemTransition) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight)) + itemTransition.updateFrame(node: itemNode, frame: itemFrame) + if wasAdded { + itemNode.alpha = 0.0 + transition.updateAlpha(node: itemNode, alpha: 1.0) + } + contentHeight += itemHeight + } + + var removeIds: [AnyHashable] = [] + for (id, _) in self.itemNodes { + if !items.contains(where: { $0.id == id }) { + removeIds.append(id) + } + } + for id in removeIds { + if let itemNode = self.itemNodes[id] { + transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in + itemNode?.removeFromSupernode() + }) + } + } + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: contentHeight))) + transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: UIScreenPixel))) + + return contentHeight + } +} + +private final class PeerInfoScreenData { + let peer: Peer? + let cachedData: CachedPeerData? + let presence: TelegramUserPresence? + let notificationSettings: TelegramPeerNotificationSettings? + + init( + peer: Peer?, + cachedData: CachedPeerData?, + presence: TelegramUserPresence?, + notificationSettings: TelegramPeerNotificationSettings? + ) { + self.peer = peer + self.cachedData = cachedData + self.presence = presence + self.notificationSettings = notificationSettings + } +} + +private enum PeerInfoScreenInputData: Equatable { + case none + case user +} + +private func peerInfoScreenData(context: AccountContext, peerId: PeerId) -> Signal { + return context.account.postbox.combinedView(keys: [.basicPeer(peerId)]) + |> map { view -> PeerInfoScreenInputData in + guard let peer = (view.views[.basicPeer(peerId)] as? BasicPeerView)?.peer else { + return .none + } + if let _ = peer as? TelegramUser { + return .user + } else { + preconditionFailure() + } + } + |> distinctUntilChanged + |> mapToSignal { inputData -> Signal in + switch inputData { + case .none: + return .single(PeerInfoScreenData( + peer: nil, + cachedData: nil, + presence: nil, + notificationSettings: nil + )) + case .user: + return context.account.viewTracker.peerView(peerId, updateData: true) + |> map { view -> PeerInfoScreenData in + return PeerInfoScreenData( + peer: view.peers[peerId], + cachedData: view.cachedData, + presence: view.peerPresences[peerId] as? TelegramUserPresence, + notificationSettings: view.notificationSettings as? TelegramPeerNotificationSettings + ) + } + } + } +} + +private final class PeerInfoInteraction { + let openUsername: (String) -> Void + let openPhone: (String) -> Void + + init( + openUsername: @escaping (String) -> Void, + openPhone: @escaping (String) -> Void + ) { + self.openUsername = openUsername + self.openPhone = openPhone + } +} + +private func peerInfoSectionItems(data: PeerInfoScreenData?, presentationData: PresentationData, interaction: PeerInfoInteraction) -> [PeerInfoScreenItem] { + var items: [PeerInfoScreenItem] = [] + if let user = data?.peer as? TelegramUser { + if let cachedData = data?.cachedData as? CachedUserData { + if let about = cachedData.about { + items.append(PeerInfoScreenLabeledValueItem(id: 0, label: "bio", text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 10), action: nil)) + } + } + if let username = user.username { + items.append(PeerInfoScreenLabeledValueItem(id: 1, label: "username", text: "@\(username)", textColor: .accent, action: { + interaction.openUsername(username) + })) + } + if let phone = user.phone { + items.append(PeerInfoScreenLabeledValueItem(id: 2, label: "mobile", text: "\(formatPhoneNumber(phone))", textColor: .accent, action: { + interaction.openPhone(phone) + })) + } + } + 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 + + private let navigationNode: PeerInfoNavigationNode + private let headerNode: PeerInfoHeaderNode + private let infoSection: PeerInfoScreenItemSectionContainerNode + private let paneContainerNode: PeerInfoPaneContainerNode + private var isPaneAreaExpanded: Bool = false + private var ignoreScrolling: Bool = false + + private var _interaction: PeerInfoInteraction? + private var interaction: PeerInfoInteraction { + return self._interaction! + } + + private var validLayout: (ContainerViewLayout, CGFloat)? + private var data: PeerInfoScreenData? + private var dataDisposable: Disposable? + + private let _ready = Promise() + var ready: Promise { + return self._ready + } + private var didSetReady = false + + init(controller: PeerInfoScreen, context: AccountContext, peerId: PeerId) { + self.controller = controller + self.context = context + self.peerId = peerId + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.scrollNode = ASScrollNode() + + self.navigationNode = PeerInfoNavigationNode() + self.headerNode = PeerInfoHeaderNode(context: context) + self.infoSection = PeerInfoScreenItemSectionContainerNode(id: 0) + self.paneContainerNode = PeerInfoPaneContainerNode(context: context, peerId: peerId) + + super.init() + + self._interaction = PeerInfoInteraction( + openUsername: { [weak self] value in + self?.openUsername(value: value) + }, + openPhone: { [weak self] value in + self?.openPhone(value: value) + } + ) + + self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + + self.scrollNode.view.showsVerticalScrollIndicator = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + 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.paneContainerNode.openMessage = { [weak self] id in + return self?.openMessage(id: id) ?? false + } + + self.headerNode.performButtonAction = { [weak self] key in + self?.performButtonAction(key: key) + } + + self.dataDisposable = (peerInfoScreenData(context: context, peerId: peerId) + |> deliverOnMainQueue).start(next: { [weak self] data in + guard let strongSelf = self else { + return + } + strongSelf.updateData(data) + }) + } + + deinit { + self.dataDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + } + + private func updateData(_ data: PeerInfoScreenData) { + self.data = data + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } + } + + func scrollToTop() { + if self.isPaneAreaExpanded { + if !self.paneContainerNode.scrollToTop() { + + } + } else { + self.scrollNode.view.setContentOffset(CGPoint(), animated: true) + } + } + + private func openMessage(id: MessageId) -> Bool { + guard let galleryMessage = self.paneContainerNode.findLoadedMessage(id: id), let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return false + } + self.view.endEditing(true) + + return self.context.sharedContext.openChatMessage(OpenChatMessageParams(context: self.context, message: galleryMessage, standalone: false, reverseMessageGalleryOrder: true, navigationController: navigationController, dismissInput: { [weak self] in + self?.view.endEditing(true) + }, present: { [weak self] c, a in + self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true) + }, transitionNode: { [weak self] messageId, media in + guard let strongSelf = self else { + return nil + } + return strongSelf.paneContainerNode.transitionNodeForGallery(messageId: messageId, media: media) + }, addToTransitionSurface: { [weak self] view in + guard let strongSelf = self else { + return + } + strongSelf.view.addSubview(view) + }, openUrl: { url in + //self?.openUrl(url) + }, openPeer: { peer, navigation in + //self?.controllerInteraction?.openPeer(peer.id, navigation, nil) + }, callPeer: { peerId in + //self?.controllerInteraction?.callPeer(peerId) + }, enqueueMessage: { _ in + }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in })) + } + + private func performButtonAction(key: PeerInfoHeaderButtonKey) { + guard let controller = self.controller else { + return + } + switch key { + case .message: + if let navigationController = controller.navigationController as? NavigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.peerId))) + } + case .call: + self.requestCall() + case .mute: + let peerId = self.peerId + let _ = (self.context.account.postbox.transaction { transaction -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in + let peerSettings: TelegramPeerNotificationSettings = (transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings + let globalSettings: GlobalNotificationSettings = (transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings + return (peerSettings, globalSettings) + } + |> deliverOnMainQueue).start(next: { [weak self] peerSettings, globalSettings in + guard let strongSelf = self else { + return + } + let soundSettings: NotificationSoundSettings? + if case .default = peerSettings.messageSound { + soundSettings = NotificationSoundSettings(value: nil) + } else { + soundSettings = NotificationSoundSettings(value: peerSettings.messageSound) + } + let muteSettingsController = notificationMuteSettingsController(presentationData: strongSelf.presentationData, notificationSettings: globalSettings.effective.groupChats, soundSettings: soundSettings, openSoundSettings: { + guard let strongSelf = self else { + return + } + let soundController = notificationSoundSelectionController(context: strongSelf.context, isModal: true, currentSound: peerSettings.messageSound, defaultSound: globalSettings.effective.groupChats.sound, completion: { sound in + guard let strongSelf = self else { + return + } + let _ = updatePeerNotificationSoundInteractive(account: strongSelf.context.account, peerId: strongSelf.peerId, sound: sound).start() + }) + strongSelf.controller?.present(soundController, in: .window(.root)) + }, updateSettings: { value in + guard let strongSelf = self else { + return + } + let _ = updatePeerMuteSetting(account: strongSelf.context.account, peerId: strongSelf.peerId, muteInterval: value).start() + }) + strongSelf.controller?.present(muteSettingsController, in: .window(.root)) + }) + case .more: + let actionSheet = ActionSheetController(presentationData: self.presentationData) + let dismissAction: () -> Void = { [weak actionSheet] in + actionSheet?.dismissAnimated() + } + var reportSpam = false + var deleteChat = false + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.UserInfo_StartSecretChat, color: .accent, action: { [weak self] in + dismissAction() + self?.openStartSecretChat() + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + controller.present(actionSheet, in: .window(.root)) + } + } + + private func openStartSecretChat() { + let peerId = self.peerId + let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, PeerId?) in + let peer = transaction.getPeer(peerId) + let filteredPeerIds = Array(transaction.getAssociatedPeerIds(peerId)).filter { $0.namespace == Namespaces.Peer.SecretChat } + var activeIndices: [ChatListIndex] = [] + for associatedId in filteredPeerIds { + if let state = (transaction.getPeer(associatedId) as? TelegramSecretChat)?.embeddedState { + switch state { + case .active, .handshake: + if let (_, index) = transaction.getPeerChatListIndex(associatedId) { + activeIndices.append(index) + } + default: + break + } + } + } + activeIndices.sort() + if let index = activeIndices.last { + return (peer, index.messageIndex.id.peerId) + } else { + return (peer, nil) + } + } + |> deliverOnMainQueue).start(next: { [weak self] peer, currentPeerId in + guard let strongSelf = self else { + return + } + if let currentPeerId = currentPeerId { + if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(currentPeerId))) + } + } else if let controller = strongSelf.controller { + let displayTitle = peer?.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder) ?? "" + controller.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.UserInfo_StartSecretChatConfirmation(displayTitle).0, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.UserInfo_StartSecretChatStart, action: { + guard let strongSelf = self else { + return + } + var createSignal = createSecretChat(account: strongSelf.context.account, peerId: peerId) + var cancelImpl: (() -> Void)? + let progressSignal = Signal { subscriber in + if let strongSelf = self { + let statusController = OverlayStatusController(theme: strongSelf.presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + strongSelf.controller?.present(statusController, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } else { + return EmptyDisposable + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = progressSignal.start() + + createSignal = createSignal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + let createSecretChatDisposable = MetaDisposable() + cancelImpl = { + createSecretChatDisposable.set(nil) + } + + createSecretChatDisposable.set((createSignal + |> deliverOnMainQueue).start(next: { peerId in + guard let strongSelf = self else { + return + } + if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId))) + } + }, error: { _ in + guard let strongSelf = self else { + return + } + strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + })) + })]), in: .window(.root)) + } + }) + } + + private func openUsername(value: String) { + let shareController = ShareController(context: context, subject: .url("\(value)")) + self.controller?.present(shareController, in: .window(.root)) + } + + private func requestCall() { + guard let peer = self.data?.peer as? TelegramUser, let cachedUserData = self.data?.cachedData as? CachedUserData else { + return + } + if cachedUserData.callsPrivate { + self.controller?.present(textAlertController(context: self.context, title: self.presentationData.strings.Call_ConnectionErrorTitle, text: self.presentationData.strings.Call_PrivacyErrorMessage(peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + return + } + + let callResult = self.context.sharedContext.callManager?.requestCall(account: self.context.account, peerId: peer.id, endCurrentIfAny: false) + if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { + if currentPeerId == peer.id { + self.context.sharedContext.navigateToCurrentCall() + } else { + let _ = (self.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in + return (transaction.getPeer(peer.id), transaction.getPeer(currentPeerId)) + } + |> deliverOnMainQueue).start(next: { [weak self] peer, current in + guard let strongSelf = self else { + return + } + if let peer = peer, let current = current { + strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Call_CallInProgressTitle, text: strongSelf.presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: { + guard let strongSelf = self else { + return + } + let _ = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peer.id, endCurrentIfAny: true) + })]), in: .window(.root)) + } + }) + } + } + } + + private func openPhone(value: String) { + let _ = (getUserPeer(postbox: self.context.account.postbox, peerId: peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer, _ in + guard let strongSelf = self else { + return + } + if let peer = peer as? TelegramUser, let peerPhoneNumber = peer.phone, formatPhoneNumber(value) == formatPhoneNumber(peerPhoneNumber) { + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + let dismissAction: () -> Void = { [weak actionSheet] in + actionSheet?.dismissAnimated() + } + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.UserInfo_TelegramCall, action: { + dismissAction() + self?.requestCall() + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.UserInfo_PhoneCall, action: { + dismissAction() + + guard let strongSelf = self else { + return + } + strongSelf.context.sharedContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(value).replacingOccurrences(of: " ", with: ""))") + }), + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + } else { + strongSelf.context.sharedContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(value).replacingOccurrences(of: " ", with: ""))") + } + }) + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { + 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))) + 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) + 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)) + } + } + + self.ignoreScrolling = false + self.updateNavigation(transition: transition) + + if !self.didSetReady && self.data != nil { + self.didSetReady = true + self._ready.set(self.paneContainerNode.isReady.get()) + } + } + + private func updateNavigation(transition: ContainedViewLayoutTransition) { + let offsetY = self.scrollNode.view.contentOffset.y + + if offsetY <= 1.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) + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + self.updateNavigation(transition: .immediate) + } + + 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 + } 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) + } + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + var currentParent: UIView? = result + var enableScrolling = true + while true { + if currentParent == nil || currentParent === self.view { + break + } + if let scrollView = currentParent as? UIScrollView { + if scrollView === self.scrollNode.view { + break + } + if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top { + return self.scrollNode.view + } + } else if let listView = currentParent as? ListViewBackingView, let listNode = listView.target { + if listNode.scroller.isDecelerating && listNode.scroller.contentOffset.y < listNode.scroller.contentInset.top { + return self.scrollNode.view + } + } + currentParent = currentParent?.superview + } + return result + } +} + +public final class PeerInfoScreen: ViewController { + private let context: AccountContext + private let peerId: PeerId + + private var presentationData: PresentationData + + private var controllerNode: PeerInfoScreenNode { + return self.displayNode as! PeerInfoScreenNode + } + + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + + public init(context: AccountContext, peerId: PeerId) { + self.context = context + self.peerId = peerId + + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let baseNavigationBarPresentationData = NavigationBarPresentationData(presentationData: self.presentationData) + super.init(navigationBarPresentationData: NavigationBarPresentationData( + theme: NavigationBarTheme( + buttonColor: 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)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + + self.scrollToTop = { [weak self] in + self?.controllerNode.scrollToTop() + } + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId) + + self._ready.set(self.controllerNode.ready.get()) + + super.displayNodeDidLoad() + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition) + } +} + +private func getUserPeer(postbox: Postbox, peerId: PeerId) -> Signal<(Peer?, CachedPeerData?), NoError> { + return postbox.transaction { transaction -> (Peer?, CachedPeerData?) in + guard let peer = transaction.getPeer(peerId) else { + return (nil, nil) + } + var resultPeer: Peer? + if let peer = peer as? TelegramSecretChat { + resultPeer = transaction.getPeer(peer.regularPeerId) + } else { + resultPeer = peer + } + return (resultPeer, resultPeer.flatMap({ transaction.getPeerCachedData(peerId: $0.id) })) + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfoScreenLabeledValueItem.swift new file mode 100644 index 0000000000..d7f382389f --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfoScreenLabeledValueItem.swift @@ -0,0 +1,123 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData + +enum PeerInfoScreenLabeledValueTextColor { + case primary + case accent +} + +enum PeerInfoScreenLabeledValueTextBehavior: Equatable { + case singleLine + case multiLine(maxLines: Int) +} + +final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { + let id: AnyHashable + let label: String + let text: String + let textColor: PeerInfoScreenLabeledValueTextColor + let textBehavior: PeerInfoScreenLabeledValueTextBehavior + let action: (() -> Void)? + + init(id: AnyHashable, label: String, text: String, textColor: PeerInfoScreenLabeledValueTextColor = .primary, textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine, action: (() -> Void)?) { + self.id = id + self.label = label + self.text = text + self.textColor = textColor + self.textBehavior = textBehavior + self.action = action + } + + func node() -> PeerInfoScreenItemNode { + return PeerInfoScreenLabeledValueItemNode() + } +} + +private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { + private let selectionNode: PeerInfoScreenSelectableBackgroundNode + private let labelNode: ImmediateTextNode + private let textNode: ImmediateTextNode + private let bottomSeparatorNode: ASDisplayNode + + private var item: PeerInfoScreenLabeledValueItem? + + override init() { + var bringToFrontForHighlightImpl: (() -> Void)? + self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() }) + + self.labelNode = ImmediateTextNode() + self.labelNode.displaysAsynchronously = false + self.labelNode.isUserInteractionEnabled = false + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + self.bottomSeparatorNode = ASDisplayNode() + self.bottomSeparatorNode.isLayerBacked = true + + super.init() + + bringToFrontForHighlightImpl = { [weak self] in + self?.bringToFrontForHighlight?() + } + + self.addSubnode(self.bottomSeparatorNode) + self.addSubnode(self.selectionNode) + self.addSubnode(self.labelNode) + self.addSubnode(self.textNode) + } + + override func update(width: CGFloat, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let item = item as? PeerInfoScreenLabeledValueItem else { + return 10.0 + } + + self.item = item + + self.selectionNode.pressed = item.action + + let sideInset: CGFloat = 16.0 + + self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let textColorValue: UIColor + switch item.textColor { + case .primary: + textColorValue = presentationData.theme.list.itemPrimaryTextColor + case .accent: + textColorValue = presentationData.theme.list.itemAccentColor + } + + self.labelNode.attributedText = NSAttributedString(string: item.label, font: Font.regular(14.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + + switch item.textBehavior { + case .singleLine: + self.textNode.maximumNumberOfLines = 1 + case let .multiLine(maxLines): + self.textNode.maximumNumberOfLines = maxLines + } + self.textNode.attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: textColorValue) + + let labelSize = self.labelNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude)) + + let labelFrame = CGRect(origin: CGPoint(x: sideInset, y: 11.0), size: labelSize) + let textFrame = CGRect(origin: CGPoint(x: sideInset, y: labelFrame.maxY + 3.0), size: textSize) + + transition.updateFrame(node: self.labelNode, frame: labelFrame) + transition.updateFrame(node: self.textNode, frame: textFrame) + + let height = labelSize.height + 3.0 + textSize.height + 22.0 + + let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel + self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition) + transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset))) + + transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel))) + transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0) + + return height + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoScreenSelectableBackgroundNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfoScreenSelectableBackgroundNode.swift new file mode 100644 index 0000000000..08b0c6e17a --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfoScreenSelectableBackgroundNode.swift @@ -0,0 +1,55 @@ +import AsyncDisplayKit +import Display +import TelegramPresentationData + +final class PeerInfoScreenSelectableBackgroundNode: ASDisplayNode { + private let backgroundNode: ASDisplayNode + private let buttonNode: HighlightTrackingButtonNode + + let bringToFrontForHighlight: () -> Void + + var pressed: (() -> Void)? { + didSet { + self.buttonNode.isUserInteractionEnabled = self.pressed != nil + } + } + + init(bringToFrontForHighlight: @escaping () -> Void) { + self.bringToFrontForHighlight = bringToFrontForHighlight + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.alpha = 0.0 + + self.buttonNode = HighlightTrackingButtonNode() + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + self.buttonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.bringToFrontForHighlight() + strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backgroundNode.alpha = 1.0 + } else { + strongSelf.backgroundNode.alpha = 0.0 + strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + } + } + + @objc private func buttonPressed() { + self.pressed?() + } + + func update(size: CGSize, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { + self.backgroundNode.backgroundColor = theme.list.itemHighlightedBackgroundColor + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(), size: size)) + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoVisualMediaPane.swift b/submodules/TelegramUI/TelegramUI/PeerInfoVisualMediaPane.swift new file mode 100644 index 0000000000..f49d9978e6 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfoVisualMediaPane.swift @@ -0,0 +1,458 @@ +import AsyncDisplayKit +import Display +import TelegramCore +import SyncCore +import SwiftSignalKit +import Postbox +import TelegramPresentationData +import AccountContext +import ContextUI +import PhotoResources + +private final class VisualMediaItemInteraction { + let openMessage: (MessageId) -> Void + var hiddenMedia: [MessageId: [Media]] = [:] + + init(openMessage: @escaping (MessageId) -> Void) { + self.openMessage = openMessage + } +} + +private final class VisualMediaItemNode: ASDisplayNode { + private let context: AccountContext + private let interaction: VisualMediaItemInteraction + + private let containerNode: ContextControllerSourceNode + private let imageNode: TransformImageNode + + private let fetchStatusDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private var resourceStatus: MediaResourceStatus? + + private var item: (VisualMediaItem, Media?, CGSize, CGSize?)? + + init(context: AccountContext, interaction: VisualMediaItemInteraction) { + self.context = context + self.interaction = interaction + + self.containerNode = ContextControllerSourceNode() + self.imageNode = TransformImageNode() + + super.init() + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.imageNode) + + self.containerNode.isGestureEnabled = false + } + + deinit { + self.fetchStatusDisposable.dispose() + self.fetchDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let (item, _, _, _) = self.item { + self.interaction.openMessage(item.message.id) + } + } + } + + func update(size: CGSize, item: VisualMediaItem, theme: PresentationTheme, synchronousLoad: Bool) { + if item === self.item?.0 && size == self.item?.2 { + return + } + var media: Media? + for value in item.message.media { + if let image = value as? TelegramMediaImage { + media = image + break + } else if let file = value as? TelegramMediaFile { + media = file + break + } + } + + if let media = media, (self.item?.1 == nil || !media.isEqual(to: self.item!.1!)) { + var mediaDimensions: CGSize? + if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions { + mediaDimensions = largestSize.cgSize + + self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, photoReference: .message(message: MessageReference(item.message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true) + + self.fetchStatusDisposable.set(nil) + /*self.statusNode.transitionToState(.none, completion: { [weak self] in + self?.statusNode.isHidden = true + })*/ + //self.mediaBadgeNode.isHidden = true + self.resourceStatus = nil + } else if let file = media as? TelegramMediaFile, file.isVideo { + mediaDimensions = file.dimensions?.cgSize + self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad) + + /*self.mediaBadgeNode.isHidden = false + + self.resourceStatus = nil + self.fetchStatusDisposable.set((messageMediaFileStatus(context: context, messageId: messageId, file: file) |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self, let item = strongSelf.item { + strongSelf.resourceStatus = status + + let isStreamable = isMediaStreamable(message: item.message, media: file) + + let statusState: RadialStatusNodeState + if isStreamable { + statusState = .none + } else { + switch status { + case let .Fetching(_, progress): + let adjustedProgress = max(progress, 0.027) + statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true) + case .Local: + statusState = .none + case .Remote: + statusState = .download(.white) + } + } + + switch statusState { + case .none: + break + default: + strongSelf.statusNode.isHidden = false + } + + strongSelf.statusNode.transitionToState(statusState, animated: true, completion: { + if let strongSelf = self { + if case .none = statusState { + strongSelf.statusNode.isHidden = true + } + } + }) + + if let duration = file.duration { + let durationString = stringForDuration(duration) + + var badgeContent: ChatMessageInteractiveMediaBadgeContent? + var mediaDownloadState: ChatMessageInteractiveMediaDownloadState? + + if isStreamable { + switch status { + case let .Fetching(_, progress): + let progressString = String(format: "%d%%", Int(progress * 100.0)) + badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString)) + mediaDownloadState = .compactFetching(progress: 0.0) + case .Local: + badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + case .Remote: + badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + mediaDownloadState = .compactRemote + } + } else { + badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString)) + } + + strongSelf.mediaBadgeNode.update(theme: item.theme, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false) + } + } + })) + if self.statusNode.supernode == nil { + self.imageNode.addSubnode(self.statusNode) + }*/ + } else { + //self.mediaBadgeNode.isHidden = true + } + self.item = (item, media, size, mediaDimensions) + + self.updateHiddenMedia() + } + + if let (item, media, _, mediaDimensions) = self.item { + self.item = (item, media, size, mediaDimensions) + + let imageFrame = CGRect(origin: CGPoint(), size: size) + + self.containerNode.frame = imageFrame + self.imageNode.frame = imageFrame + + if let mediaDimensions = mediaDimensions { + let imageSize = mediaDimensions.aspectFilled(imageFrame.size) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: theme.list.mediaPlaceholderColor))() + } + } + } + + func transitionNode() -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + let imageNode = self.imageNode + return (self.imageNode, self.imageNode.bounds, { [weak self, weak imageNode] in + var statusNodeHidden = false + var accessoryHidden = false + if let strongSelf = self { + //statusNodeHidden = strongSelf.statusNode.isHidden + //accessoryHidden = strongSelf.mediaBadgeNode.isHidden + //strongSelf.statusNode.isHidden = true + //strongSelf.mediaBadgeNode.isHidden = true + } + let view = imageNode?.view.snapshotContentTree(unhide: true) + if let strongSelf = self { + //strongSelf.statusNode.isHidden = statusNodeHidden + //strongSelf.mediaBadgeNode.isHidden = accessoryHidden + } + return (view, nil) + }) + } + + func updateHiddenMedia() { + if let (item, _, _, _) = self.item { + if let _ = self.interaction.hiddenMedia[item.message.id] { + self.isHidden = true + } else { + self.isHidden = false + } + } else { + self.isHidden = false + } + } +} + +private final class VisualMediaItem { + let message: Message + + init(message: Message) { + self.message = message + } +} + +final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate { + private let context: AccountContext + private let peerId: PeerId + private let scrollNode: ASScrollNode + + private var _itemInteraction: VisualMediaItemInteraction? + private var itemInteraction: VisualMediaItemInteraction { + return self._itemInteraction! + } + + private var currentParams: (size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? + + private let ready = Promise() + private var didSetReady: Bool = false + var isReady: Signal { + return self.ready.get() + } + + private let listDisposable = MetaDisposable() + private var hiddenMediaDisposable: Disposable? + private var mediaItems: [VisualMediaItem] = [] + private var visibleMediaItems: [UInt32: VisualMediaItemNode] = [:] + + private var numberOfItemsToRequest: Int = 50 + private var currentView: MessageHistoryView? + private var isRequestingView: Bool = false + private var isFirstHistoryView: Bool = true + + init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId) { + self.context = context + self.peerId = peerId + + self.scrollNode = ASScrollNode() + + super.init() + + self._itemInteraction = VisualMediaItemInteraction(openMessage: { id in + openMessage(id) + }) + + self.scrollNode.view.showsVerticalScrollIndicator = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + self.scrollNode.view.scrollsToTop = false + self.scrollNode.view.delegate = self + + self.addSubnode(self.scrollNode) + + self.requestHistoryAroundVisiblePosition() + + self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in + guard let strongSelf = self else { + return + } + var hiddenMedia: [MessageId: [Media]] = [:] + for id in ids { + if case let .chat(accountId, messageId, media) = id, accountId == strongSelf.context.account.id { + hiddenMedia[messageId] = [media] + } + } + strongSelf.itemInteraction.hiddenMedia = hiddenMedia + for (_, itemNode) in strongSelf.visibleMediaItems { + itemNode.updateHiddenMedia() + } + }) + } + + deinit { + self.listDisposable.dispose() + self.hiddenMediaDisposable?.dispose() + } + + private func requestHistoryAroundVisiblePosition() { + if self.isRequestingView { + return + } + self.isRequestingView = true + self.listDisposable.set((self.context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(self.peerId), index: .upperBound, anchorIndex: .upperBound, count: self.numberOfItemsToRequest, fixedCombinedReadStates: nil, tagMask: .photoOrVideo) + |> deliverOnMainQueue).start(next: { [weak self] (view, updateType, _) in + guard let strongSelf = self else { + return + } + strongSelf.updateHistory(view: view, updateType: updateType) + strongSelf.isRequestingView = false + })) + } + + private func updateHistory(view: MessageHistoryView, updateType: ViewUpdateType) { + self.currentView = view + + self.mediaItems.removeAll() + switch updateType { + case .FillHole: + self.requestHistoryAroundVisiblePosition() + default: + for entry in view.entries.reversed() { + self.mediaItems.append(VisualMediaItem(message: entry.message)) + } + + let wasFirstHistoryView = self.isFirstHistoryView + self.isFirstHistoryView = false + + if let (size, isScrollingLockedAtTop, presentationData) = self.currentParams { + self.update(size: size, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate) + if !self.didSetReady { + self.didSetReady = true + self.ready.set(.single(true)) + } + } + } + } + + func scrollToTop() -> Bool { + if self.scrollNode.view.contentOffset.y > 0.0 { + self.scrollNode.view.setContentOffset(CGPoint(), animated: true) + return true + } else { + return false + } + } + + func findLoadedMessage(id: MessageId) -> Message? { + for item in self.mediaItems { + if item.message.id == id { + return item.message + } + } + return nil + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + for item in self.mediaItems { + if item.message.id == messageId { + if let itemNode = self.visibleMediaItems[item.message.stableId] { + return itemNode.transitionNode() + } + break + } + } + return nil + } + + func update(size: CGSize, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, isScrollingLockedAtTop, presentationData) + + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) + + let itemSpacing: CGFloat = 1.0 + let itemsInRow: Int = max(3, min(6, Int(size.width / 100.0))) + let itemSize: CGFloat = floor(size.width / CGFloat(itemsInRow)) + + let rowCount: Int = self.mediaItems.count / itemsInRow + (self.mediaItems.count % itemsInRow == 0 ? 0 : 1) + let contentHeight = CGFloat(rowCount + 1) * itemSpacing + CGFloat(rowCount) * itemSize + + self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight) + self.updateVisibleItems(size: size, theme: presentationData.theme, synchronousLoad: synchronous) + + if isScrollingLockedAtTop { + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)) + } + self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if let (size, _, presentationData) = self.currentParams { + self.updateVisibleItems(size: size, theme: presentationData.theme, synchronousLoad: false) + + if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.height * 2.0, let currentView = self.currentView, currentView.earlierId != nil { + if !self.isRequestingView { + self.numberOfItemsToRequest += 50 + self.requestHistoryAroundVisiblePosition() + } + } + } + } + + private func updateVisibleItems(size: CGSize, theme: PresentationTheme, synchronousLoad: Bool) { + let itemSpacing: CGFloat = 1.0 + let itemsInRow: Int = max(3, min(6, Int(size.width / 100.0))) + let itemSize: CGFloat = floor(size.width / CGFloat(itemsInRow)) + + let rowCount: Int = self.mediaItems.count / itemsInRow + (self.mediaItems.count % itemsInRow == 0 ? 0 : 1) + + let visibleRect = self.scrollNode.view.bounds + var minVisibleRow = Int(floor((visibleRect.minY - itemSpacing) / (itemSize + itemSpacing))) + minVisibleRow = max(0, minVisibleRow) + var maxVisibleRow = Int(ceil((visibleRect.maxY - itemSpacing) / (itemSize + itemSpacing))) + maxVisibleRow = min(rowCount - 1, maxVisibleRow) + + let minVisibleIndex = minVisibleRow * itemsInRow + let maxVisibleIndex = min(self.mediaItems.count - 1, maxVisibleRow * itemsInRow - 1) + + var validIds = Set() + if minVisibleIndex < maxVisibleIndex { + for i in minVisibleIndex ... maxVisibleIndex { + let stableId = self.mediaItems[i].message.stableId + validIds.insert(stableId) + let rowIndex = i / Int(itemsInRow) + let columnIndex = i % Int(itemsInRow) + let itemOrigin = CGPoint(x: CGFloat(columnIndex) * (itemSize + itemSpacing), y: itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing)) + let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (size.width - itemOrigin.x) : itemSize, height: itemSize)) + let itemNode: VisualMediaItemNode + if let current = self.visibleMediaItems[stableId] { + itemNode = current + } else { + itemNode = VisualMediaItemNode(context: self.context, interaction: self.itemInteraction) + self.visibleMediaItems[stableId] = itemNode + self.scrollNode.addSubnode(itemNode) + } + itemNode.frame = itemFrame + itemNode.update(size: itemFrame.size, item: self.mediaItems[i], theme: theme, synchronousLoad: synchronousLoad) + } + } + var removeKeys: [UInt32] = [] + for (id, _) in self.visibleMediaItems { + if !validIds.contains(id) { + removeKeys.append(id) + } + } + for id in removeKeys { + if let itemNode = self.visibleMediaItems.removeValue(forKey: id) { + itemNode.removeFromSupernode() + } + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift b/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift index f6fa41cde3..160fa6cfcc 100644 --- a/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift +++ b/submodules/TelegramUI/TelegramUI/SharedAccountContext.swift @@ -1244,3 +1244,20 @@ public final class SharedAccountContextImpl: SharedAccountContext { } private let defaultChatControllerInteraction = ChatControllerInteraction.default + +private func peerInfoControllerImpl(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode) -> ViewController? { + if let _ = peer as? TelegramGroup { + return groupInfoController(context: context, peerId: peer.id) + } else if let channel = peer as? TelegramChannel { + if case .group = channel.info { + return groupInfoController(context: context, peerId: peer.id) + } else { + return channelInfoController(context: context, peerId: peer.id) + } + } else if peer is TelegramUser { + return PeerInfoScreen(context: context, peerId: peer.id) + } else if peer is TelegramSecretChat { + return userInfoController(context: context, peerId: peer.id, mode: mode) + } + return nil +} From 6758002adef00d3a93d41a21117ffbf8dcec0c2a Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 5 Feb 2020 01:38:55 +0000 Subject: [PATCH 2/2] User info screen design update --- .../Sources/AccountContext.swift | 2 +- .../Sources/CallListController.swift | 2 +- .../ChatTitleActivityContentNode.swift | 9 + .../Sources/ChatTitleActivityNode.swift | 9 + .../Sources/ContactsController.swift | 2 +- .../ContainedViewLayoutTransition.swift | 47 +- .../Display/Display/ImmediateTextNode.swift | 7 + .../Display/Display/NavigationBar.swift | 71 +- .../Display/Display/NavigationBarBadge.swift | 6 +- .../Display/NavigationButtonNode.swift | 64 +- .../NavigationTransitionCoordinator.swift | 77 +- submodules/Display/Display/TextNode.swift | 2 +- .../Sources/InstantPageControllerNode.swift | 2 +- .../Sources/SecureIdAuthController.swift | 2 +- .../Sources/AvatarGalleryController.swift | 40 +- .../Sources/ChannelBlacklistController.swift | 2 +- .../Sources/ChannelMembersController.swift | 4 +- .../ChannelPermissionsController.swift | 2 +- .../Sources/GroupInfoController.swift | 4 +- .../BlockedPeersController.swift | 2 +- .../Recent Sessions/ItemListWebsiteItem.swift | 2 +- ...ectivePrivacySettingsPeersController.swift | 2 +- .../Resources/PresentationResourceKey.swift | 1 + .../PresentationResourcesRootController.swift | 13 + .../ButtonAddMember.imageset/Contents.json | 12 + .../ic_pf_addmember.pdf | Bin 0 -> 4256 bytes .../ButtonCall.imageset/Contents.json | 12 + .../ButtonCall.imageset/ic_pf_call.pdf | Bin 0 -> 4226 bytes .../ButtonMessage.imageset/Contents.json | 10 +- .../ButtonMessage.imageset/ic_pf_message.pdf | Bin 0 -> 3918 bytes .../ButtonMore.imageset/Contents.json | 12 + .../ButtonMore.imageset/ic_pf_more.pdf | Bin 0 -> 3933 bytes .../ButtonMute.imageset/Contents.json | 12 + .../ButtonMute.imageset/ic_pf_mute.pdf | Bin 0 -> 4295 bytes .../ButtonUnmute.imageset/Contents.json | 12 + .../ButtonUnmute.imageset/ic_pf_unmute.pdf | Bin 0 -> 4154 bytes .../TelegramUI/ChatAvatarNavigationNode.swift | 17 +- .../TelegramUI/ChatController.swift | 150 +- .../ChatInterfaceStateNavigationButtons.swift | 5 +- .../ChatRecentActionsControllerNode.swift | 4 +- .../TelegramUI/TelegramUI/ChatTitleView.swift | 65 +- .../TelegramUI/OpenAddContact.swift | 2 +- .../TelegramUI/TelegramUI/OpenUrl.swift | 4 +- .../TelegramUI/PeerInfoScreen.swift | 1275 ++++++++++++++--- .../PeerMediaCollectionController.swift | 2 +- .../TelegramUI/PollResultsController.swift | 2 +- .../TelegramUI/SharedAccountContext.swift | 8 +- .../TelegramUI/SharedWakeupManager.swift | 2 +- .../TelegramUI/TextLinkHandling.swift | 2 +- 49 files changed, 1543 insertions(+), 439 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonAddMember.imageset/ic_pf_addmember.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonCall.imageset/ic_pf_call.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMessage.imageset/ic_pf_message.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMore.imageset/ic_pf_more.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonMute.imageset/ic_pf_mute.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Peer Info/ButtonUnmute.imageset/ic_pf_unmute.pdf 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 0000000000000000000000000000000000000000..ad2274b415c52c63ed481651d2e2e4bfeaf11c03 GIT binary patch literal 4256 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 C>`R0I literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..64fe2bdbd1711e147dd0893688c4c74b5391ef0d GIT binary patch literal 3918 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 C$6J42aC!9M`gGiv*5&>mph?~1B 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 O(Q*)RaZNof$iD#q0JEY1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2cabbef68d7202673f0c93e4e5865267e3d7fad4 GIT binary patch literal 4295 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 b|F_Fe0r`>`x%{!wa3z=`SWHaESQq?nA8~9Z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..617622f59ab7767dea94e224b007cedf9eaee1bc GIT binary patch literal 4154 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 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) } }