diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 27052bb641..49065df51e 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -371,7 +371,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let _ = (updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { settings in var preset = preset if currentPreset == nil { - preset.id = max(1, settings.filters.map({ $0.id }).max() ?? 1) + preset.id = max(2, settings.filters.map({ $0.id }).max() ?? 2) } var settings = settings settings.filters = settings.filters.filter { $0 != preset && $0 != currentPreset } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 79fa26f3f7..19cb9e585f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -193,8 +193,10 @@ private enum ChatListRecentEntry: Comparable, Identifiable { } }, setPeerIdWithRevealedOptions: setPeerIdWithRevealedOptions, deletePeer: deletePeer, contextAction: peerContextAction.flatMap { peerContextAction in return { node, gesture in - if let chatPeer = peer.peer.peers[peer.peer.peerId] { + if let chatPeer = peer.peer.peers[peer.peer.peerId], chatPeer.id.namespace != Namespaces.Peer.SecretChat { peerContextAction(chatPeer, .recentSearch, node, gesture) + } else { + gesture?.cancel() } } }) @@ -415,7 +417,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { interaction.peerSelected(peer) }, contextAction: peerContextAction.flatMap { peerContextAction in return { node, gesture in - if let chatPeer = chatPeer { + if let chatPeer = chatPeer, chatPeer.id.namespace != Namespaces.Peer.SecretChat { peerContextAction(chatPeer, .search, node, gesture) } else { gesture?.cancel() diff --git a/submodules/Display/Display/ContainedViewLayoutTransition.swift b/submodules/Display/Display/ContainedViewLayoutTransition.swift index 0200f6f212..7635ad88ba 100644 --- a/submodules/Display/Display/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Display/ContainedViewLayoutTransition.swift @@ -567,6 +567,30 @@ public extension ContainedViewLayoutTransition { } } + func animateTransformScale(view: UIView, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) { + let t = view.layer.transform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + if currentScale.isEqual(to: fromScale) { + if let completion = completion { + completion(true) + } + return + } + + switch self { + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + view.layer.animateScale(from: fromScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + if let completion = completion { + completion(result) + } + }) + } + } + func updateTransformScale(node: ASDisplayNode, scale: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { let t = node.layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) diff --git a/submodules/Display/Display/ImmediateTextNode.swift b/submodules/Display/Display/ImmediateTextNode.swift index 38488ecda3..25d8fb69e7 100644 --- a/submodules/Display/Display/ImmediateTextNode.swift +++ b/submodules/Display/Display/ImmediateTextNode.swift @@ -38,6 +38,20 @@ public class ImmediateTextNode: TextNode { let node = TextNode() node.cachedLayout = self.cachedLayout node.frame = self.frame + if let subnodes = self.subnodes { + for subnode in subnodes { + if let subnode = subnode as? ASImageNode { + let copySubnode = ASImageNode() + copySubnode.isLayerBacked = subnode.isLayerBacked + copySubnode.image = subnode.image + copySubnode.displaysAsynchronously = false + copySubnode.displayWithoutProcessing = true + copySubnode.frame = subnode.frame + copySubnode.alpha = subnode.alpha + node.addSubnode(copySubnode) + } + } + } return node } diff --git a/submodules/Display/Display/Navigation/NavigationContainer.swift b/submodules/Display/Display/Navigation/NavigationContainer.swift index ec6f8803d5..fe79956916 100644 --- a/submodules/Display/Display/Navigation/NavigationContainer.swift +++ b/submodules/Display/Display/Navigation/NavigationContainer.swift @@ -133,6 +133,13 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { }*/ } + func hasNonReadyControllers() -> Bool { + if let pending = self.state.pending, !pending.isReady { + return true + } + return false + } + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return false } @@ -185,6 +192,17 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { if let strongSelf = self { if let top = strongSelf.state.top { strongSelf.syncKeyboard(leftEdge: top.value.displayNode.frame.minX, transition: transition) + + var updatedStatusBarStyle = strongSelf.statusBarStyle + if let childTransition = strongSelf.state.transition, childTransition.coordinator.progress >= 0.3 { + updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle + } else { + updatedStatusBarStyle = top.value.statusBar.statusBarStyle + } + if strongSelf.statusBarStyle != updatedStatusBarStyle { + strongSelf.statusBarStyle = updatedStatusBarStyle + strongSelf.statusBarStyleUpdated?(.animated(duration: 0.3, curve: .easeInOut)) + } } } }) @@ -337,7 +355,11 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate { } } self.applyLayout(layout: updatedLayout, to: top, isMaster: true, transition: transition) - updatedStatusBarStyle = top.value.statusBar.statusBarStyle + if let childTransition = self.state.transition, childTransition.coordinator.progress >= 0.3 { + updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle + } else { + updatedStatusBarStyle = top.value.statusBar.statusBarStyle + } } else { updatedStatusBarStyle = .Ignore } diff --git a/submodules/Display/Display/Navigation/NavigationController.swift b/submodules/Display/Display/Navigation/NavigationController.swift index 037b2fc65d..b709a8a5fd 100644 --- a/submodules/Display/Display/Navigation/NavigationController.swift +++ b/submodules/Display/Display/Navigation/NavigationController.swift @@ -274,6 +274,18 @@ open class NavigationController: UINavigationController, ContainableController, return true } } + if let rootContainer = self.rootContainer { + switch rootContainer { + case let .flat(container): + if container.hasNonReadyControllers() { + return true + } + case let .split(splitContainer): + if splitContainer.hasNonReadyControllers() { + return true + } + } + } return false } @@ -1001,27 +1013,8 @@ open class NavigationController: UINavigationController, ContainableController, } public func pushViewController(_ controller: ViewController, animated: Bool = true, completion: @escaping () -> Void) { - let navigateAction: () -> Void = { [weak self] in - guard let strongSelf = self else { - return - } - - if !controller.hasActiveInput { - //strongSelf.view.endEditing(true) - } - /*strongSelf.scheduleAfterLayout({ - guard let strongSelf = self else { - return - }*/ - strongSelf.pushViewController(controller, animated: animated) - completion() - //}) - } - - /*if let lastController = self.viewControllers.last as? ViewController, !lastController.attemptNavigation(navigateAction) { - } else {*/ - navigateAction() - //} + self.pushViewController(controller, animated: animated) + completion() } open override func pushViewController(_ viewController: UIViewController, animated: Bool) { diff --git a/submodules/Display/Display/Navigation/NavigationSplitContainer.swift b/submodules/Display/Display/Navigation/NavigationSplitContainer.swift index 155fd2a186..ac6c7772db 100644 --- a/submodules/Display/Display/Navigation/NavigationSplitContainer.swift +++ b/submodules/Display/Display/Navigation/NavigationSplitContainer.swift @@ -57,6 +57,16 @@ final class NavigationSplitContainer: ASDisplayNode { self.view.addSubview(self.detailScrollToTopView) } + func hasNonReadyControllers() -> Bool { + if self.masterContainer.hasNonReadyControllers() { + return true + } + if self.detailContainer.hasNonReadyControllers() { + return true + } + return false + } + func updateTheme(theme: NavigationControllerTheme) { self.separator.backgroundColor = theme.navigationBar.separatorColor } diff --git a/submodules/Display/Display/ViewController.swift b/submodules/Display/Display/ViewController.swift index 392644c300..a8d2ca9516 100644 --- a/submodules/Display/Display/ViewController.swift +++ b/submodules/Display/Display/ViewController.swift @@ -93,6 +93,8 @@ public enum ViewControllerNavigationPresentation { } } + var blocksInteractionUntilReady: Bool = false + public final var isOpaqueWhenInOverlay: Bool = false public final var blocksBackgroundWhenInOverlay: Bool = false public final var automaticallyControlPresentationContextLayout: Bool = true diff --git a/submodules/LocalMediaResources/BUCK b/submodules/LocalMediaResources/BUCK index 6739a9b6cf..12b32376f0 100644 --- a/submodules/LocalMediaResources/BUCK +++ b/submodules/LocalMediaResources/BUCK @@ -15,6 +15,8 @@ static_library( frameworks = [ "$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/UIKit.framework", - "$SDKROOT/System/Library/Frameworks/Photos.framework", + ], + weak_frameworks = [ + "Photos", ], ) diff --git a/submodules/PeerAvatarGalleryUI/BUCK b/submodules/PeerAvatarGalleryUI/BUCK index bde6746427..f61fd2951b 100644 --- a/submodules/PeerAvatarGalleryUI/BUCK +++ b/submodules/PeerAvatarGalleryUI/BUCK @@ -26,6 +26,8 @@ static_library( "$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/UIKit.framework", "$SDKROOT/System/Library/Frameworks/QuickLook.framework", - "$SDKROOT/System/Library/Frameworks/Photos.framework", + ], + weak_frameworks = [ + "Photos", ], ) diff --git a/submodules/SaveToCameraRoll/BUCK b/submodules/SaveToCameraRoll/BUCK index d594ecc651..b75130a540 100644 --- a/submodules/SaveToCameraRoll/BUCK +++ b/submodules/SaveToCameraRoll/BUCK @@ -18,6 +18,8 @@ static_library( "$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/UIKit.framework", "$SDKROOT/System/Library/Frameworks/MobileCoreServices.framework", - "$SDKROOT/System/Library/Frameworks/Photos.framework", + ], + weak_frameworks = [ + "Photos", ], ) diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index 66d5fd6e9b..d3c6254589 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -22,7 +22,7 @@ public final class SearchDisplayController { private var isSearchingDisposable: Disposable? - public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { + public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, placeholder: String? = nil, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: false), strings: presentationData.strings, fieldStyle: .modern) self.mode = mode self.contentNode = contentNode @@ -48,6 +48,9 @@ public final class SearchDisplayController { self?.searchBar.prefixString = prefix self?.searchBar.text = query } + if let placeholder = placeholder { + self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + } self.contentNode.setPlaceholder = { [weak self] string in guard string != self?.searchBar.placeholderString?.string else { return @@ -153,6 +156,7 @@ public final class SearchDisplayController { if let placeholder = placeholder { self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } else { + self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) } } diff --git a/submodules/SettingsUI/BUCK b/submodules/SettingsUI/BUCK index df9f9d6aa7..49fcfda198 100644 --- a/submodules/SettingsUI/BUCK +++ b/submodules/SettingsUI/BUCK @@ -92,8 +92,10 @@ static_library( "$SDKROOT/System/Library/Frameworks/UIKit.framework", "$SDKROOT/System/Library/Frameworks/MessageUI.framework", "$SDKROOT/System/Library/Frameworks/LocalAuthentication.framework", - "$SDKROOT/System/Library/Frameworks/Photos.framework", "$SDKROOT/System/Library/Frameworks/QuickLook.framework", "$SDKROOT/System/Library/Frameworks/CoreTelephony.framework", ], + weak_frameworks = [ + "Photos", + ], ) diff --git a/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift index 30d69469e3..4e63ec7863 100644 --- a/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatAvatarNavigationNode.swift @@ -80,12 +80,20 @@ final class ChatAvatarNavigationNode: ASDisplayNode { (self.view as? ChatAvatarNavigationNodeView)?.targetNode = self (self.view as? ChatAvatarNavigationNodeView)?.chatController = self.chatController - self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.avatarTapGesture(_:)))) + let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.avatarTapGesture(_:))) + self.avatarNode.view.addGestureRecognizer(tapRecognizer) } - @objc private func avatarTapGesture(_ recognizer: UITapGestureRecognizer) { + @objc private func avatarTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { if case .ended = recognizer.state { - self.tapped?() + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + self.tapped?() + default: + break + } + } } } diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 09d4a6ebdb..b2fa3154bc 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -1866,12 +1866,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.controllerInteraction = controllerInteraction - var displayNavigationAvatar = false - if case let .peer(peerId) = chatLocation, peerId != context.account.peerId { - displayNavigationAvatar = true + if case let .peer(peerId) = chatLocation, peerId != context.account.peerId, subject != .scheduledMessages { 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) + self.chatTitleView = ChatTitleView(account: self.context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, displayAvatar: true) if let avatarNode = self.chatTitleView?.avatarNode { avatarNode.chatController = self avatarNode.contextAction = { [weak self] node, gesture in @@ -1988,8 +1986,16 @@ 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.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 + let imageOverride: AvatarNodeImageOverride? + if strongSelf.context.account.peerId == peer.id { + imageOverride = .savedMessagesIcon + } else if peer.isDeleted { + imageOverride = .deletedIcon + } else { + imageOverride = nil + } + strongSelf.chatTitleView?.avatarNode?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: imageOverride) + strongSelf.chatTitleView?.avatarNode?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil && imageOverride == nil && peer.smallProfileImage != nil } if strongSelf.peerView === peerView && strongSelf.reportIrrelvantGeoNotice == peerReportNotice && strongSelf.hasScheduledMessages == hasScheduledMessages { @@ -5075,6 +5081,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.leftNavigationButton = nil } + self.chatTitleView?.displayAvatar = updatedChatPresentationInterfaceState.interfaceState.selectionState == nil + if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState, strings: updatedChatPresentationInterfaceState.strings, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) { if self.rightNavigationButton != button { var animated = transition.isAnimated diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift index 3e0f9c33a0..12964d2a8b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -72,7 +72,12 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch } } + if presentationInterfaceState.isScheduledMessages { + return nil + } + if case .standard(true) = presentationInterfaceState.mode { + return nil } else if let peer = presentationInterfaceState.renderedPeer?.peer { if presentationInterfaceState.accountPeerId == peer.id { if presentationInterfaceState.isScheduledMessages { diff --git a/submodules/TelegramUI/TelegramUI/ChatTitleView.swift b/submodules/TelegramUI/TelegramUI/ChatTitleView.swift index 27802c7185..d399138c28 100644 --- a/submodules/TelegramUI/TelegramUI/ChatTitleView.swift +++ b/submodules/TelegramUI/TelegramUI/ChatTitleView.swift @@ -179,6 +179,15 @@ final class ChatTitleView: UIView, NavigationBarTitleView { var pressed: (() -> Void)? + var displayAvatar: Bool = true { + didSet { + if self.displayAvatar != oldValue { + self.avatarNode?.isHidden = !self.displayAvatar + self.setNeedsLayout() + } + } + } + var titleContent: ChatTitleContent? { didSet { if let titleContent = self.titleContent { @@ -529,19 +538,13 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if highlighted { strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") strongSelf.activityNode.layer.removeAnimation(forKey: "opacity") - strongSelf.titleLeftIconNode.layer.removeAnimation(forKey: "opacity") - strongSelf.titleRightIconNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleCredibilityIconNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleNode.alpha = 0.4 strongSelf.activityNode.alpha = 0.4 - strongSelf.titleLeftIconNode.alpha = 0.4 - strongSelf.titleRightIconNode.alpha = 0.4 strongSelf.titleCredibilityIconNode.alpha = 0.4 } else { strongSelf.titleNode.alpha = 1.0 strongSelf.activityNode.alpha = 1.0 - strongSelf.titleLeftIconNode.alpha = 1.0 - strongSelf.titleRightIconNode.alpha = 1.0 strongSelf.titleCredibilityIconNode.alpha = 1.0 strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.activityNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) @@ -592,7 +595,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleLeftIconNode.image { if self.titleLeftIconNode.supernode == nil { - self.contentContainer.addSubnode(self.titleLeftIconNode) + self.titleNode.addSubnode(self.titleLeftIconNode) } leftIconWidth = image.size.width + 6.0 } else if self.titleLeftIconNode.supernode != nil { @@ -610,7 +613,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { if let image = self.titleRightIconNode.image { if self.titleRightIconNode.supernode == nil { - self.contentContainer.addSubnode(self.titleRightIconNode) + self.titleNode.addSubnode(self.titleRightIconNode) } rightIconWidth = image.size.width + 3.0 } else if self.titleRightIconNode.supernode != nil { @@ -622,68 +625,43 @@ final class ChatTitleView: UIView, NavigationBarTitleView { 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 + if self.displayAvatar { + 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 - leftInset, height: size.height)) - titleSize.width += credibilityIconWidth - 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: 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: leftInset + leftIconWidth, y: floor((size.height - combinedHeight) / 2.0)), size: titleSize) - self.titleNode.frame = titleFrame - - var activityFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: activitySize) - self.activityNode.frame = activityFrame - } - - if let image = self.titleLeftIconNode.image { - self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX - image.size.width - 3.0 - UIScreenPixel, y: titleFrame.minY + 4.0), size: image.size) - } - if let image = self.titleCredibilityIconNode.image { - self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 2.0), size: image.size) - } - if let image = self.titleRightIconNode.image { - self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 3.0, y: titleFrame.minY + 6.0), size: image.size) - } - } else { - let titleSize = self.titleNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0 - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0), height: size.height)) - let activitySize = self.activityNode.updateLayout(CGSize(width: floor(clearBounds.width / 2.0), height: size.height), alignment: .center) - - let titleInfoSpacing: CGFloat = 8.0 - let combinedWidth = titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + activitySize.width + titleInfoSpacing - - let titleFrame = CGRect(origin: CGPoint(x: leftIconWidth + floor((clearBounds.width - combinedWidth) / 2.0), y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) + var titleSize = self.titleNode.updateLayout(CGSize(width: clearBounds.width - leftIconWidth - credibilityIconWidth - rightIconWidth - titleSideInset * 2.0 - leftInset, height: size.height)) + titleSize.width += credibilityIconWidth + 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: leftInset + leftIconWidth, y: floor((size.height - titleSize.height) / 2.0)), size: titleSize) self.titleNode.frame = titleFrame - self.activityNode.frame = CGRect(origin: CGPoint(x: floor((clearBounds.width - combinedWidth) / 2.0 + titleSize.width + leftIconWidth + credibilityIconWidth + rightIconWidth + titleInfoSpacing), y: floor((size.height - activitySize.height) / 2.0)), size: activitySize) + } else { + let combinedHeight = titleSize.height + activitySize.height + titleInfoSpacing - if let image = self.titleLeftIconNode.image { - self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.minY + 4.0), size: image.size) - } - if let image = self.titleCredibilityIconNode.image { - self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 6.0), size: image.size) - } - if let image = self.titleRightIconNode.image { - self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 6.0), size: image.size) - } + 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: leftInset, y: floor((size.height - combinedHeight) / 2.0) + titleSize.height + titleInfoSpacing), size: activitySize) + self.activityNode.frame = activityFrame } - /*if let networkStatusNode = self.networkStatusNode { - transition.updateFrame(node: networkStatusNode, frame: CGRect(origin: CGPoint(), size: size)) - networkStatusNode.updateLayout(size: size, transition: transition) - }*/ + if let image = self.titleLeftIconNode.image { + self.titleLeftIconNode.frame = CGRect(origin: CGPoint(x: -image.size.width - 3.0 - UIScreenPixel, y: 4.0), size: image.size) + } + if let image = self.titleCredibilityIconNode.image { + self.titleCredibilityIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX - image.size.width - 1.0, y: titleFrame.minY + 2.0), size: image.size) + } + if let image = self.titleRightIconNode.image { + self.titleRightIconNode.frame = CGRect(origin: CGPoint(x: titleFrame.width + 3.0, y: 6.0), size: image.size) + } } @objc func buttonPressed() { diff --git a/submodules/TelegramUI/TelegramUI/ListMessageFileItemNode.swift b/submodules/TelegramUI/TelegramUI/ListMessageFileItemNode.swift index 57daa968c1..43ce473a32 100644 --- a/submodules/TelegramUI/TelegramUI/ListMessageFileItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ListMessageFileItemNode.swift @@ -658,7 +658,7 @@ final class ListMessageFileItemNode: ListMessageNode { strongSelf.addSubnode(waveformScrubbingNode) } - strongSelf.waveformScrubbingNode?.frame = CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 10.0), size: CGSize(width: params.width - (leftOffset + leftInset) - 16.0, height: 12.0)) + transition.updateFrame(node: waveformScrubbingNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: 10.0), size: CGSize(width: params.width - leftInset - 16.0, height: 12.0))) waveformNode.setup(color: item.theme.list.controlSecondaryColor, waveform: waveform) waveformForegroundNode.setup(color: item.theme.list.itemAccentColor, waveform: waveform) @@ -809,7 +809,7 @@ final class ListMessageFileItemNode: ListMessageNode { } } self.waveformScrubbingNode?.enableScrubbing = enableScrubbing - if let musicIsPlaying = musicIsPlaying, !isVoice { + if let musicIsPlaying = musicIsPlaying, !isVoice, !isInstantVideo { if self.playbackOverlayNode == nil { let playbackOverlayNode = ListMessagePlaybackOverlayNode() playbackOverlayNode.frame = self.iconImageNode.frame diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoScreenActionItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenActionItem.swift similarity index 100% rename from submodules/TelegramUI/TelegramUI/PeerInfoScreenActionItem.swift rename to submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenActionItem.swift diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift similarity index 100% rename from submodules/TelegramUI/TelegramUI/PeerInfoScreenDisclosureItem.swift rename to submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenDisclosureItem.swift diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift similarity index 100% rename from submodules/TelegramUI/TelegramUI/PeerInfoScreenLabeledValueItem.swift rename to submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenLabeledValueItem.swift diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoScreenSelectableBackgroundNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSelectableBackgroundNode.swift similarity index 100% rename from submodules/TelegramUI/TelegramUI/PeerInfoScreenSelectableBackgroundNode.swift rename to submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSelectableBackgroundNode.swift diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoScreenSwitchItem.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSwitchItem.swift similarity index 100% rename from submodules/TelegramUI/TelegramUI/PeerInfoScreenSwitchItem.swift rename to submodules/TelegramUI/TelegramUI/PeerInfo/ListItems/PeerInfoScreenSwitchItem.swift diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift similarity index 77% rename from submodules/TelegramUI/TelegramUI/PeerInfoGroupsInCommonPaneNode.swift rename to submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift index 89477a9ce3..04db539477 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -35,24 +35,24 @@ private struct GroupsInCommonListEntry: Comparable, Identifiable { return lhs.index < rhs.index } - func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem { let peer = self.peer return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: self.peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { openPeer(peer) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, contextAction: { node, gesture in - //arguments.contextAction(peer, node, gesture) + openPeerContextAction(peer, node, gesture) }, hasTopStripe: false, noInsets: true) } } -private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void) -> GroupsInCommonListTransaction { +private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> GroupsInCommonListTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer, openPeerContextAction: openPeerContextAction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, openPeer: openPeer, openPeerContextAction: openPeerContextAction), directionHint: nil) } return GroupsInCommonListTransaction(deletions: deletions, insertions: insertions, updates: updates) } @@ -60,7 +60,8 @@ private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext private let peerId: PeerId - private let paneInteraction: PeerInfoPaneInteraction + private let chatControllerInteraction: ChatControllerInteraction + private let openPeerContextAction: (Peer, ASDisplayNode, ContextGesture?) -> Void private let listNode: ListView private var peers: [Peer] = [] @@ -75,10 +76,11 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { return self.ready.get() } - init(context: AccountContext, peerId: PeerId, interaction: PeerInfoPaneInteraction, peers: [Peer]) { + init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, peers: [Peer]) { self.context = context self.peerId = peerId - self.paneInteraction = interaction + self.chatControllerInteraction = chatControllerInteraction + self.openPeerContextAction = openPeerContextAction self.listNode = ListView() @@ -102,14 +104,14 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { } } - func update(size: CGSize, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.currentParams == nil self.currentParams = (size, isScrollingLockedAtTop, presentationData) transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), headerInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.scrollEnabled = !isScrollingLockedAtTop @@ -124,7 +126,9 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { entries.append(GroupsInCommonListEntry(index: entries.count, peer: peer)) } let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, openPeer: { [weak self] peer in - self?.paneInteraction.openPeer(peer) + self?.chatControllerInteraction.openPeer(peer.id, .default, nil) + }, openPeerContextAction: { [weak self] peer, node, gesture in + self?.openPeerContextAction(peer, node, gesture) }) self.currentEntries = entries self.enqueuedTransactions.append(transaction) @@ -156,13 +160,22 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { return nil } + func updateHiddenMedia() { + } + func transferVelocity(_ velocity: CGFloat) { + if velocity > 0.0 { + self.listNode.transferVelocity(velocity) + } } func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } + func addToTransitionSurface(view: UIView) { + } + func updateSelectedMessages(animated: Bool) { } } diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoListPaneNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoListPaneNode.swift new file mode 100644 index 0000000000..b021f9542c --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoListPaneNode.swift @@ -0,0 +1,125 @@ +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 chatControllerInteraction: ChatControllerInteraction + + 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 let selectedMessagesPromise = Promise?>(nil) + private var selectedMessages: Set? { + didSet { + if self.selectedMessages != oldValue { + self.selectedMessagesPromise.set(.single(self.selectedMessages)) + } + } + } + + private var hiddenMediaDisposable: Disposable? + + init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) { + self.context = context + self.peerId = peerId + self.chatControllerInteraction = chatControllerInteraction + + self.selectedMessages = chatControllerInteraction.selectionState.flatMap { $0.selectedIds } + self.selectedMessagesPromise.set(.single(self.selectedMessages)) + + self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false)) + + super.init() + + 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, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, 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(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve)) + self.listNode.scrollEnabled = !isScrollingLockedAtTop + } + + func findLoadedMessage(id: MessageId) -> Message? { + self.listNode.messageInCurrentHistoryView(id) + } + + func updateHiddenMedia() { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ListMessageNode { + itemNode.updateHiddenMedia() + } + } + } + + func transferVelocity(_ velocity: CGFloat) { + if velocity > 0.0 { + self.listNode.transferVelocity(velocity) + } + } + + 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 + } + + func addToTransitionSurface(view: UIView) { + self.view.addSubview(view) + } + + func updateSelectedMessages(animated: Bool) { + self.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateSelectionState(animated: animated) + } + } + self.selectedMessages = self.chatControllerInteraction.selectionState.flatMap { $0.selectedIds } + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoVisualMediaPane.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift similarity index 82% rename from submodules/TelegramUI/TelegramUI/PeerInfoVisualMediaPane.swift rename to submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index 395b578a67..f374730a20 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfoVisualMediaPane.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -16,17 +16,20 @@ private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6) private let mediaBadgeTextColor = UIColor.white private final class VisualMediaItemInteraction { - let openMessage: (MessageId) -> Void - let toggleSelection: (MessageId) -> Void + let openMessage: (Message) -> Void + let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void + let toggleSelection: (MessageId, Bool) -> Void var hiddenMedia: [MessageId: [Media]] = [:] var selectedMessageIds: Set? init( - openMessage: @escaping (MessageId) -> Void, - toggleSelection: @escaping (MessageId) -> Void + openMessage: @escaping (Message) -> Void, + openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, + toggleSelection: @escaping (MessageId, Bool) -> Void ) { self.openMessage = openMessage + self.openMessageContextActions = openMessageContextActions self.toggleSelection = toggleSelection } } @@ -68,7 +71,12 @@ private final class VisualMediaItemNode: ASDisplayNode { self.containerNode.addSubnode(self.imageNode) self.containerNode.addSubnode(self.mediaBadgeNode) - self.containerNode.isGestureEnabled = false + self.containerNode.activated = { [weak self] gesture in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + strongSelf.interaction.openMessageContextActions(item.0.message, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture) + } } deinit { @@ -79,13 +87,17 @@ private final class VisualMediaItemNode: ASDisplayNode { override func didLoad() { super.didLoad() - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.view.addGestureRecognizer(TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + @objc func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { if case .ended = recognizer.state { - if let (item, _, _, _) = self.item { - self.interaction.openMessage(item.message.id) + if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + if case .tap = gesture { + if let (item, _, _, _) = self.item { + self.interaction.openMessage(item.message) + } + } } } } @@ -132,8 +144,8 @@ private final class VisualMediaItemNode: ASDisplayNode { let isStreamable = isMediaStreamable(message: item.message, media: file) - let statusState: RadialStatusNodeState - if isStreamable { + let statusState: RadialStatusNodeState = .none + /*if isStreamable { statusState = .none } else { switch status { @@ -145,7 +157,7 @@ private final class VisualMediaItemNode: ASDisplayNode { case .Remote: statusState = .download(.white) } - } + }*/ switch statusState { case .none: @@ -234,7 +246,11 @@ private final class VisualMediaItemNode: ASDisplayNode { } else { let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in if let strongSelf = self, let messageId = strongSelf.item?.0.message.id { - strongSelf.interaction.toggleSelection(messageId) + var toggledValue = true + if let selectedMessageIds = strongSelf.interaction.selectedMessageIds, selectedMessageIds.contains(messageId) { + toggledValue = false + } + strongSelf.interaction.toggleSelection(messageId, toggledValue) } }) @@ -305,7 +321,7 @@ private final class VisualMediaItem { final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate { private let context: AccountContext private let peerId: PeerId - private let interaction: PeerInfoPaneInteraction + private let chatControllerInteraction: ChatControllerInteraction private let scrollNode: ASScrollNode @@ -314,7 +330,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro return self._itemInteraction! } - private var currentParams: (size: CGSize, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? + private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)? private let ready = Promise() private var didSetReady: Bool = false @@ -334,24 +350,27 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro private var decelerationAnimator: ConstantDisplayLinkAnimator? - init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId, interaction: PeerInfoPaneInteraction) { + init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId) { self.context = context self.peerId = peerId - self.interaction = interaction + self.chatControllerInteraction = chatControllerInteraction self.scrollNode = ASScrollNode() super.init() self._itemInteraction = VisualMediaItemInteraction( - openMessage: { id in - openMessage(id) + openMessage: { [weak self] message in + self?.chatControllerInteraction.openMessage(message, .default) }, - toggleSelection: { id in - interaction.toggleMessageSelected(id) + openMessageContextActions: { [weak self] message, sourceNode, sourceRect, gesture in + self?.chatControllerInteraction.openMessageContextActions(message, sourceNode, sourceRect, gesture) + }, + toggleSelection: { [weak self] id, value in + self?.chatControllerInteraction.toggleMessagesSelection([id], value) } ) - self.itemInteraction.selectedMessageIds = self.interaction.selectedMessageIds + self.itemInteraction.selectedMessageIds = chatControllerInteraction.selectionState.flatMap { $0.selectedIds } self.scrollNode.view.showsVerticalScrollIndicator = false if #available(iOS 11.0, *) { @@ -416,8 +435,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro let wasFirstHistoryView = self.isFirstHistoryView self.isFirstHistoryView = false - if let (size, visibleHeight, isScrollingLockedAtTop, presentationData) = self.currentParams { - self.update(size: size, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate) + if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) = self.currentParams { + self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: wasFirstHistoryView, transition: .immediate) if !self.didSetReady { self.didSetReady = true self.ready.set(.single(true)) @@ -444,6 +463,12 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro return nil } + func updateHiddenMedia() { + for (_, itemNode) in self.visibleMediaItems { + itemNode.updateHiddenMedia() + } + } + func transferVelocity(_ velocity: CGFloat) { if velocity > 0.0 { //print("transferVelocity \(velocity)") @@ -493,15 +518,19 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro return nil } + func addToTransitionSurface(view: UIView) { + self.scrollNode.view.addSubview(view) + } + func updateSelectedMessages(animated: Bool) { - self.itemInteraction.selectedMessageIds = self.interaction.selectedMessageIds + self.itemInteraction.selectedMessageIds = self.chatControllerInteraction.selectionState.flatMap { $0.selectedIds } for (_, itemNode) in self.visibleMediaItems { itemNode.updateSelectionState(animated: animated) } } - func update(size: CGSize, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - self.currentParams = (size, visibleHeight, isScrollingLockedAtTop, presentationData) + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) @@ -510,10 +539,10 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro 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 + let contentHeight = CGFloat(rowCount + 1) * itemSpacing + CGFloat(rowCount) * itemSize + bottomInset self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight) - self.updateVisibleItems(size: size, visibleHeight: visibleHeight, theme: presentationData.theme, synchronousLoad: synchronous) + self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, synchronousLoad: synchronous) if isScrollingLockedAtTop { transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)) @@ -527,8 +556,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } func scrollViewDidScroll(_ scrollView: UIScrollView) { - if let (size, visibleHeight, _, presentationData) = self.currentParams { - self.updateVisibleItems(size: size, visibleHeight: visibleHeight, theme: presentationData.theme, synchronousLoad: false) + if let (size, sideInset, bottomInset, visibleHeight, _, presentationData) = self.currentParams { + self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, 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 { @@ -539,10 +568,12 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro } } - private func updateVisibleItems(size: CGSize, visibleHeight: CGFloat, theme: PresentationTheme, synchronousLoad: Bool) { + private func updateVisibleItems(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, theme: PresentationTheme, synchronousLoad: Bool) { + let availableWidth = size.width - sideInset * 2.0 + let itemSpacing: CGFloat = 1.0 - let itemsInRow: Int = max(3, min(6, Int(size.width / 140.0))) - let itemSize: CGFloat = floor(size.width / CGFloat(itemsInRow)) + let itemsInRow: Int = max(3, min(6, Int(availableWidth / 140.0))) + let itemSize: CGFloat = floor(availableWidth / CGFloat(itemsInRow)) let rowCount: Int = self.mediaItems.count / itemsInRow + (self.mediaItems.count % itemsInRow == 0 ? 0 : 1) @@ -562,8 +593,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro 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 itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing)) + let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (availableWidth - itemOrigin.x) : itemSize, height: itemSize)) let itemNode: VisualMediaItemNode if let current = self.visibleMediaItems[stableId] { itemNode = current diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoData.swift new file mode 100644 index 0000000000..feb2c71926 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoData.swift @@ -0,0 +1,306 @@ +import Foundation +import UIKit +import Postbox +import SyncCore +import TelegramCore +import SwiftSignalKit +import AccountContext +import PeerPresenceStatusManager +import TelegramStringFormatting +import TelegramPresentationData + +final class PeerInfoState { + let isEditing: Bool + let isSearching: Bool + let selectedMessageIds: Set? + + init( + isEditing: Bool, + isSearching: Bool, + selectedMessageIds: Set? + ) { + self.isEditing = isEditing + self.isSearching = isSearching + self.selectedMessageIds = selectedMessageIds + } + + func withIsEditing(_ isEditing: Bool) -> PeerInfoState { + return PeerInfoState( + isEditing: isEditing, + isSearching: self.isSearching, + selectedMessageIds: self.selectedMessageIds + ) + } + + func withSelectedMessageIds(_ selectedMessageIds: Set?) -> PeerInfoState { + return PeerInfoState( + isEditing: self.isEditing, + isSearching: self.isSearching, + selectedMessageIds: selectedMessageIds + ) + } +} + +final class PeerInfoScreenData { + let peer: Peer? + let cachedData: CachedPeerData? + let status: PeerInfoStatusData? + let notificationSettings: TelegramPeerNotificationSettings? + let globalNotificationSettings: GlobalNotificationSettings? + let isContact: Bool + let availablePanes: [PeerInfoPaneKey] + let groupsInCommon: [Peer]? + + init( + peer: Peer?, + cachedData: CachedPeerData?, + status: PeerInfoStatusData?, + notificationSettings: TelegramPeerNotificationSettings?, + globalNotificationSettings: GlobalNotificationSettings?, + isContact: Bool, + availablePanes: [PeerInfoPaneKey], + groupsInCommon: [Peer]? + ) { + self.peer = peer + self.cachedData = cachedData + self.status = status + self.notificationSettings = notificationSettings + self.globalNotificationSettings = globalNotificationSettings + self.isContact = isContact + self.availablePanes = availablePanes + self.groupsInCommon = groupsInCommon + } +} + +enum PeerInfoScreenInputData: Equatable { + case none + case user(userId: PeerId, secretChatId: PeerId?, isBot: Bool) +} + +func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId) -> Signal<[PeerInfoPaneKey], NoError> { + let tags: [(MessageTags, PeerInfoPaneKey)] = [ + (.photoOrVideo, .media), + (.file, .files), + (.music, .music), + (.voiceOrInstantVideo, .voice), + (.webPage, .links) + ] + return combineLatest(tags.map { tagAndKey -> Signal in + let (tag, key) = tagAndKey + return context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId), index: .upperBound, anchorIndex: .upperBound, count: 20, clipHoles: false, fixedCombinedReadStates: nil, tagMask: tag) + |> map { (view, _, _) -> PeerInfoPaneKey? in + if view.entries.isEmpty { + return nil + } else { + return key + } + } + }) + |> map { keys -> [PeerInfoPaneKey] in + return keys.compactMap { $0 } + } + |> distinctUntilChanged + /*return context.account.postbox.combinedView(keys: tags.map { (tag, _) -> PostboxViewKey in + return .historyTagInfo(peerId: peerId, tag: tag) + }) + |> map { view -> [PeerInfoPaneKey] in + return tags.compactMap { (tag, key) -> PeerInfoPaneKey? in + if let info = view.views[.historyTagInfo(peerId: peerId, tag: tag)] as? HistoryTagInfoView, !info.isEmpty { + return key + } else { + return nil + } + } + } + |> distinctUntilChanged*/ +} + +struct PeerInfoStatusData: Equatable { + var text: String + var isActivity: Bool +} + +func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> 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 user = peer as? TelegramUser { + return .user(userId: user.id, secretChatId: nil, isBot: user.botInfo != nil) + } else { + preconditionFailure() + } + } + |> distinctUntilChanged + |> mapToSignal { inputData -> Signal in + switch inputData { + case .none: + return .single(PeerInfoScreenData( + peer: nil, + cachedData: nil, + status: nil, + notificationSettings: nil, + globalNotificationSettings: nil, + isContact: false, + availablePanes: [], + groupsInCommon: nil + )) + case let .user(peerId, secretChatId, isBot): + let groupsInCommonSignal: Signal<[Peer]?, NoError> + if isBot { + groupsInCommonSignal = .single([]) + } else { + groupsInCommonSignal = .single(nil) + |> then( + groupsInCommon(account: context.account, peerId: peerId) + |> map(Optional.init) + ) + } + enum StatusInputData: Equatable { + case none + case presence(TelegramUserPresence) + case bot + } + let status = Signal { subscriber in + class Manager { + var currentValue: TelegramUserPresence? = nil + var updateManager: QueueLocalObject? = nil + } + let manager = Atomic(value: Manager()) + let notify: () -> Void = { + let data = manager.with { manager -> PeerInfoStatusData? in + if let presence = manager.currentValue { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (text, isActivity) = stringAndActivityForUserPresence(strings: strings, dateTimeFormat: dateTimeFormat, presence: presence, relativeTo: Int32(timestamp), expanded: true) + return PeerInfoStatusData(text: text, isActivity: isActivity) + } else { + return nil + } + } + subscriber.putNext(data) + } + let disposable = (context.account.viewTracker.peerView(peerId, updateData: false) + |> map { view -> StatusInputData in + guard let user = view.peers[peerId] as? TelegramUser else { + return .none + } + if user.isDeleted { + return .none + } + if user.botInfo != nil { + return .bot + } + if user.flags.contains(.isSupport) { + return .none + } + guard let presence = view.peerPresences[peerId] as? TelegramUserPresence else { + return .none + } + return .presence(presence) + } + |> distinctUntilChanged).start(next: { inputData in + switch inputData { + case .bot: + subscriber.putNext(PeerInfoStatusData(text: strings.Bot_GenericBotStatus, isActivity: false)) + default: + var presence: TelegramUserPresence? + if case let .presence(value) = inputData { + presence = value + } + let _ = manager.with { manager -> Void in + manager.currentValue = presence + if let presence = presence { + let updateManager: QueueLocalObject + if let current = manager.updateManager { + updateManager = current + } else { + updateManager = QueueLocalObject(queue: .mainQueue(), generate: { + return PeerPresenceStatusManager(update: { + notify() + }) + }) + } + updateManager.with { updateManager in + updateManager.reset(presence: presence) + } + } else if let _ = manager.updateManager { + manager.updateManager = nil + } + } + notify() + } + }) + return disposable + } + |> distinctUntilChanged + let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) + var combinedKeys: [PostboxViewKey] = [] + combinedKeys.append(globalNotificationsKey) + if let secretChatId = secretChatId { + combinedKeys.append(.peerChatState(peerId: peerId)) + } + return combineLatest( + context.account.viewTracker.peerView(peerId, updateData: true), + peerInfoAvailableMediaPanes(context: context, peerId: peerId), + context.account.postbox.combinedView(keys: combinedKeys), + status, + groupsInCommonSignal + ) + |> map { peerView, availablePanes, combinedView, status, groupsInCommon -> PeerInfoScreenData in + var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings + if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { + if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { + globalNotificationSettings = settings + } + } + + var availablePanes = availablePanes + if let groupsInCommon = groupsInCommon, !groupsInCommon.isEmpty { + availablePanes.append(.groupsInCommon) + } + + return PeerInfoScreenData( + peer: peerView.peers[peerId], + cachedData: peerView.cachedData, + status: status, + notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, + globalNotificationSettings: globalNotificationSettings, + isContact: peerView.peerIsContact, + availablePanes: availablePanes, + groupsInCommon: groupsInCommon + ) + } + } + } +} + +func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?) -> [PeerInfoHeaderButtonKey] { + var result: [PeerInfoHeaderButtonKey] = [] + if let user = peer as? TelegramUser { + result.append(.message) + var callsAvailable = false + if !user.isDeleted, user.botInfo == nil, !user.flags.contains(.isSupport), let cachedUserData = cachedData as? CachedUserData { + callsAvailable = cachedUserData.callsAvailable + } + if callsAvailable { + result.append(.call) + } + result.append(.mute) + + if !user.isDeleted, user.botInfo == nil && !user.flags.contains(.isSupport) { + result.append(.more) + } + } + return result +} + +func peerInfoCanEdit(peer: Peer?, cachedData: CachedPeerData?) -> Bool { + if let user = peer as? TelegramUser { + if user.isDeleted { + return false + } + return true + } + return false +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift new file mode 100644 index 0000000000..b601a275e4 --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoHeaderNode.swift @@ -0,0 +1,1497 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import SyncCore +import TelegramCore +import AvatarNode +import AccountContext +import SwiftSignalKit +import TelegramPresentationData +import PhotoResources +import PeerAvatarGalleryUI +import TelegramStringFormatting + +enum PeerInfoHeaderButtonKey: Hashable { + case message + case call + case mute + case more + case addMember +} + +enum PeerInfoHeaderButtonIcon { + case message + case call + case mute + case unmute + case more + case addMember +} + +final class PeerInfoHeaderButtonNode: HighlightableButtonNode { + let key: PeerInfoHeaderButtonKey + private let action: (PeerInfoHeaderButtonNode) -> Void + let containerNode: ASDisplayNode + 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.containerNode = ASDisplayNode() + + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.containerNode) + self.containerNode.addSubnode(self.backgroundNode) + self.containerNode.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(.normal) + context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) + let imageName: String + switch icon { + case .message: + imageName = "Peer Info/ButtonMessage" + case .call: + imageName = "Peer Info/ButtonCall" + case .mute: + imageName = "Peer Info/ButtonMute" + case .unmute: + imageName = "Peer Info/ButtonUnmute" + case .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) + 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.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) + } +} + +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 + } +} + +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) + } + } +} + +final class PeerInfoAvatarListItemNode: ASDisplayNode { + 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 imageSize = CGSize(width: min(size.width, size.height), height: min(size.width, size.height)) + let makeLayout = self.imageNode.asyncLayout() + let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) + let _ = applyLayout() + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) + } +} + +final class PeerInfoAvatarListContainerNode: ASDisplayNode { + private let context: AccountContext + + let controlsContainerNode: ASDisplayNode + let controlsContainerTransformNode: ASDisplayNode + let shadowNode: ASDisplayNode + + let contentNode: ASDisplayNode + private(set) var galleryEntries: [AvatarGalleryEntry] = [] + 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 + + var currentItemNode: PeerInfoAvatarListItemNode? { + if self.currentIndex >= 0 && self.currentIndex < self.items.count { + return self.itemNodes[self.items[self.currentIndex].id] + } else { + return nil + } + } + + init(context: AccountContext) { + self.context = context + + self.contentNode = ASDisplayNode() + + self.controlsContainerNode = ASDisplayNode() + self.controlsContainerNode.isUserInteractionEnabled = false + + self.controlsContainerTransformNode = ASDisplayNode() + self.controlsContainerTransformNode.isUserInteractionEnabled = false + + self.shadowNode = ASDisplayNode() + //self.shadowNode.backgroundColor = .green + + super.init() + + self.backgroundColor = .black + + self.addSubnode(self.contentNode) + + self.controlsContainerNode.addSubnode(self.shadowNode) + self.controlsContainerTransformNode.addSubnode(self.controlsContainerNode) + self.addSubnode(self.controlsContainerTransformNode) + + 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) + } + + func selectFirstItem() { + self.currentIndex = 0 + if let size = self.validLayout { + self.updateItems(size: size, transition: .immediate) + } + } + + @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.galleryEntries = entries + 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()) + } + } + } +} + +final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { + let context: AccountContext + let avatarNode: AvatarNode + + var tapped: (() -> Void)? + + private var isFirstAvatarLoading = true + + init(context: AccountContext) { + self.context = context + let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) + 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 { + var overrideImage: AvatarNodeImageOverride? + if peer.isDeleted { + overrideImage = .deletedIcon + } + self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: 100.0, height: 100.0)) + self.isFirstAvatarLoading = false + } + } +} + +final class PeerInfoEditingAvatarNode: ASDisplayNode { + let context: AccountContext + let avatarNode: AvatarNode + + var tapped: (() -> Void)? + + init(context: AccountContext) { + self.context = context + let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) + 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: false, displayDimensions: CGSize(width: 100.0, height: 100.0)) + } + } +} + +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) + } + + func animateAvatarCollapse(transition: ContainedViewLayoutTransition) { + if let currentItemNode = self.listContainerNode.currentItemNode, let avatarCopyView = self.avatarContainerNode.avatarNode.view.snapshotContentTree(), case let .animated(duration, curve) = transition { + avatarCopyView.center = currentItemNode.imageNode.position + currentItemNode.view.addSubview(avatarCopyView) + let scale = currentItemNode.imageNode.bounds.height / avatarCopyView.bounds.height + avatarCopyView.layer.transform = CATransform3DMakeScale(scale, scale, scale) + avatarCopyView.alpha = 0.0 + transition.updateAlpha(layer: avatarCopyView.layer, alpha: 1.0, completion: { [weak avatarCopyView] _ in + Queue.mainQueue().after(0.1, { + avatarCopyView?.removeFromSuperview() + }) + }) + } + } +} + +final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { + private let regularTextNode: ImmediateTextNode + private let whiteTextNode: ImmediateTextNode + private let iconNode: ASImageNode + + private var key: PeerInfoHeaderNavigationButtonKey? + private var theme: PresentationTheme? + + var isWhite: Bool = false { + didSet { + if self.isWhite != oldValue { + self.regularTextNode.isHidden = self.isWhite + self.whiteTextNode.isHidden = !self.isWhite + } + } + } + + var action: (() -> Void)? + + override init() { + self.regularTextNode = ImmediateTextNode() + self.whiteTextNode = ImmediateTextNode() + self.whiteTextNode.isHidden = true + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + + super.init() + + self.addSubnode(self.regularTextNode) + self.addSubnode(self.whiteTextNode) + self.addSubnode(self.iconNode) + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc private func pressed() { + self.action?() + } + + func update(key: PeerInfoHeaderNavigationButtonKey, presentationData: PresentationData) -> CGSize { + let textSize: CGSize + if self.key != key || self.theme !== presentationData.theme { + self.key = key + self.theme = presentationData.theme + + let text: String + var icon: UIImage? + var isBold = false + switch key { + case .edit: + text = presentationData.strings.Common_Edit + case .done, .cancel, .selectionDone: + text = presentationData.strings.Common_Done + isBold = true + case .select: + text = presentationData.strings.Common_Select + case .search: + text = "" + icon = PresentationResourcesRootController.navigationCompactSearchIcon(presentationData.theme) + } + + let font: UIFont = isBold ? Font.semibold(17.0) : Font.regular(17.0) + + self.regularTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: presentationData.theme.rootController.navigationBar.accentTextColor) + self.whiteTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: .white) + self.iconNode.image = icon + + textSize = self.regularTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + let _ = self.whiteTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) + } else { + textSize = self.regularTextNode.bounds.size + } + + let inset: CGFloat = 0.0 + let height: CGFloat = 44.0 + + let textFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - textSize.height) / 2.0)), size: textSize) + self.regularTextNode.frame = textFrame + self.whiteTextNode.frame = textFrame + + if let image = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size) + + return CGSize(width: image.size.width + inset * 2.0, height: height) + } else { + return CGSize(width: textSize.width + inset * 2.0, height: height) + } + } +} + +enum PeerInfoHeaderNavigationButtonKey { + case edit + case done + case cancel + case select + case selectionDone + case search +} + +struct PeerInfoHeaderNavigationButtonSpec: Equatable { + let key: PeerInfoHeaderNavigationButtonKey + let isForExpandedView: Bool +} + +final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode { + private var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] + + private var currentButtons: [PeerInfoHeaderNavigationButtonSpec] = [] + + var isWhite: Bool = false { + didSet { + if self.isWhite != oldValue { + for (_, buttonNode) in self.buttonNodes { + buttonNode.isWhite = self.isWhite + } + } + } + } + + var performAction: ((PeerInfoHeaderNavigationButtonKey) -> Void)? + + override init() { + super.init() + } + + func update(size: CGSize, presentationData: PresentationData, buttons: [PeerInfoHeaderNavigationButtonSpec], expandFraction: CGFloat, transition: ContainedViewLayoutTransition) { + let maximumExpandOffset: CGFloat = 14.0 + let expandOffset: CGFloat = -expandFraction * maximumExpandOffset + if self.currentButtons != buttons { + self.currentButtons = buttons + + var nextRegularButtonOrigin = size.width - 16.0 + var nextExpandedButtonOrigin = size.width - 16.0 + for spec in buttons.reversed() { + let buttonNode: PeerInfoHeaderNavigationButton + var wasAdded = false + if let current = self.buttonNodes[spec.key] { + buttonNode = current + } else { + wasAdded = true + buttonNode = PeerInfoHeaderNavigationButton() + self.buttonNodes[spec.key] = buttonNode + self.addSubnode(buttonNode) + buttonNode.isWhite = self.isWhite + buttonNode.action = { [weak self] in + self?.performAction?(spec.key) + } + } + let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData) + var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin + let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) + nextButtonOrigin -= buttonSize.width + 4.0 + if spec.isForExpandedView { + nextExpandedButtonOrigin = nextButtonOrigin + } else { + nextRegularButtonOrigin = nextButtonOrigin + } + if wasAdded { + buttonNode.frame = buttonFrame + } else { + transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) + } + let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) + transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + } + var removeKeys: [PeerInfoHeaderNavigationButtonKey] = [] + for (key, _) in self.buttonNodes { + if !buttons.contains(where: { $0.key == key }) { + removeKeys.append(key) + } + } + for key in removeKeys { + if let buttonNode = self.buttonNodes.removeValue(forKey: key) { + buttonNode.removeFromSupernode() + } + } + } else { + var nextRegularButtonOrigin = size.width - 16.0 + var nextExpandedButtonOrigin = size.width - 16.0 + for spec in buttons.reversed() { + if let buttonNode = self.buttonNodes[spec.key] { + let buttonSize = buttonNode.bounds.size + var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin + let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) + nextButtonOrigin -= buttonSize.width + 4.0 + if spec.isForExpandedView { + nextExpandedButtonOrigin = nextButtonOrigin + } else { + nextRegularButtonOrigin = nextButtonOrigin + } + transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) + let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) + transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) + } + } + } + } +} + +final class PeerInfoHeaderRegularContentNode: ASDisplayNode { + +} + +enum PeerInfoHeaderTextFieldNodeKey { + case firstName + case lastName + case title + case description +} + +protocol PeerInfoHeaderTextFieldNode: ASDisplayNode { + var text: String { get } + + func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat +} + +final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode { + private let textNode: TextFieldNode + private let topSeparator: ASDisplayNode + + private var theme: PresentationTheme? + + var text: String { + return self.textNode.textField.text ?? "" + } + + override init() { + self.textNode = TextFieldNode() + self.topSeparator = ASDisplayNode() + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.topSeparator) + } + + func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { + if self.theme !== presentationData.theme { + self.theme = presentationData.theme + self.textNode.textField.textColor = presentationData.theme.list.itemPrimaryTextColor + //self.textNode.textField.keyboardAppearance = presentationData.theme.keyboardAppearance + self.textNode.textField.tintColor = presentationData.theme.list.itemAccentColor + } + + let attributedPlaceholderText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPlaceholderTextColor) + if self.textNode.textField.attributedPlaceholder == nil || !self.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) { + self.textNode.textField.attributedPlaceholder = attributedPlaceholderText + self.textNode.textField.accessibilityHint = attributedPlaceholderText.string + } + + if let updateText = updateText { + self.textNode.textField.text = updateText + } + + self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + self.topSeparator.frame = CGRect(origin: CGPoint(x: safeInset + (hasPrevious ? 16.0 : 0.0), y: 0.0), size: CGSize(width: width, height: UIScreenPixel)) + + let height: CGFloat = 44.0 + + self.textNode.frame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: floor((height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - 16.0 * 2.0), height: 40.0)) + + self.textNode.isUserInteractionEnabled = isEnabled + self.textNode.alpha = isEnabled ? 1.0 : 0.6 + + return height + } +} + +/*final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode { + private let textNode: TextFieldNode + private let topSeparator: ASDisplayNode + + override init() { + self.textNode = TextFieldNode() + self.topSeparator = ASDisplayNode() + + super.init() + } + + func update(width: CGFloat, safeInset: CGFloat) -> CGFloat { + return 44.0 + } +}*/ + +final class PeerInfoHeaderEditingContentNode: ASDisplayNode { + private let context: AccountContext + let avatarNode: PeerInfoEditingAvatarNode + + var itemNodes: [PeerInfoHeaderTextFieldNodeKey: PeerInfoHeaderTextFieldNode] = [:] + + init(context: AccountContext) { + self.context = context + self.avatarNode = PeerInfoEditingAvatarNode(context: context) + + super.init() + + self.addSubnode(self.avatarNode) + } + + func editingTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) -> String? { + return self.itemNodes[key]?.text + } + + func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, peer: Peer?, isContact: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { + 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.updateFrameAdditiveToCenter(node: self.avatarNode, frame: CGRect(origin: avatarFrame.center, size: CGSize())) + + var contentHeight: CGFloat = statusBarHeight + 10.0 + 100.0 + 20.0 + + var fieldKeys: [PeerInfoHeaderTextFieldNodeKey] = [] + if let user = peer as? TelegramUser { + if !user.isDeleted { + fieldKeys.append(.firstName) + if user.botInfo == nil { + fieldKeys.append(.lastName) + } + } + } + var hasPrevious = false + for key in fieldKeys { + let itemNode: PeerInfoHeaderTextFieldNode + var updateText: String? + if let current = self.itemNodes[key] { + itemNode = current + } else { + switch key { + case .firstName: + updateText = (peer as? TelegramUser)?.firstName ?? "" + case .lastName: + updateText = (peer as? TelegramUser)?.lastName ?? "" + case .title: + updateText = (peer as? TelegramUser)?.debugDisplayTitle ?? "" + case .description: + break + } + itemNode = PeerInfoHeaderSingleLineTextFieldNode() + self.itemNodes[key] = itemNode + self.addSubnode(itemNode) + } + let placeholder: String + var isEnabled = true + switch key { + case .firstName: + placeholder = "First Name" + isEnabled = isContact + case .lastName: + placeholder = "Last Name" + isEnabled = isContact + case .title: + placeholder = "Title" + case .description: + placeholder = "Description" + } + let itemHeight = itemNode.update(width: width, safeInset: safeInset, hasPrevious: hasPrevious, placeholder: placeholder, isEnabled: isEnabled, presentationData: presentationData, updateText: updateText) + transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight))) + contentHeight += itemHeight + hasPrevious = true + } + var removeKeys: [PeerInfoHeaderTextFieldNodeKey] = [] + for (key, _) in self.itemNodes { + if !fieldKeys.contains(key) { + removeKeys.append(key) + } + } + for key in removeKeys { + if let itemNode = self.itemNodes.removeValue(forKey: key) { + itemNode.removeFromSupernode() + } + } + + return contentHeight + } +} + +final class PeerInfoHeaderNode: ASDisplayNode { + private var context: AccountContext + private var presentationData: PresentationData? + + private(set) var isAvatarExpanded: Bool + + let avatarListNode: PeerInfoAvatarListNode + + let regularContentNode: PeerInfoHeaderRegularContentNode + let editingContentNode: PeerInfoHeaderEditingContentNode + let titleNodeContainer: ASDisplayNode + let titleNodeRawContainer: ASDisplayNode + let titleNode: ImmediateTextNode + let titleCredibilityIconNode: ASImageNode + let subtitleNodeContainer: ASDisplayNode + let subtitleNodeRawContainer: ASDisplayNode + let subtitleNode: ImmediateTextNode + private var buttonNodes: [PeerInfoHeaderButtonKey: PeerInfoHeaderButtonNode] = [:] + private let backgroundNode: ASDisplayNode + private let expandedBackgroundNode: ASDisplayNode + let separatorNode: ASDisplayNode + let navigationButtonContainer: PeerInfoHeaderNavigationButtonContainerNode + + var performButtonAction: ((PeerInfoHeaderButtonKey) -> Void)? + var requestAvatarExpansion: (([AvatarGalleryEntry], (ASDisplayNode, CGRect, () -> (UIView?, UIView?))) -> Void)? + + var navigationTransition: PeerInfoHeaderNavigationTransition? + + init(context: AccountContext, avatarInitiallyExpanded: Bool) { + self.context = context + self.isAvatarExpanded = avatarInitiallyExpanded + + self.avatarListNode = PeerInfoAvatarListNode(context: context, readyWhenGalleryLoads: avatarInitiallyExpanded) + + self.titleNodeContainer = ASDisplayNode() + self.titleNodeRawContainer = ASDisplayNode() + self.titleNode = ImmediateTextNode() + self.titleNode.displaysAsynchronously = false + + self.titleCredibilityIconNode = ASImageNode() + self.titleCredibilityIconNode.displaysAsynchronously = false + self.titleCredibilityIconNode.displayWithoutProcessing = true + self.titleNode.addSubnode(self.titleCredibilityIconNode) + + self.subtitleNodeContainer = ASDisplayNode() + self.subtitleNodeRawContainer = ASDisplayNode() + self.subtitleNode = ImmediateTextNode() + self.subtitleNode.displaysAsynchronously = false + + self.regularContentNode = PeerInfoHeaderRegularContentNode() + self.editingContentNode = PeerInfoHeaderEditingContentNode(context: context) + self.editingContentNode.alpha = 0.0 + + self.navigationButtonContainer = PeerInfoHeaderNavigationButtonContainerNode() + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.expandedBackgroundNode = ASDisplayNode() + self.expandedBackgroundNode.isLayerBacked = true + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.expandedBackgroundNode) + self.addSubnode(self.separatorNode) + self.titleNodeContainer.addSubnode(self.titleNode) + self.regularContentNode.addSubnode(self.titleNodeContainer) + self.subtitleNodeContainer.addSubnode(self.subtitleNode) + self.regularContentNode.addSubnode(self.subtitleNodeContainer) + self.regularContentNode.addSubnode(self.avatarListNode) + self.addSubnode(self.regularContentNode) + self.addSubnode(self.editingContentNode) + self.addSubnode(self.navigationButtonContainer) + + self.avatarListNode.avatarContainerNode.tapped = { [weak self] in + guard let strongSelf = self else { + return + } + let avatarNode = strongSelf.avatarListNode.avatarContainerNode.avatarNode + strongSelf.requestAvatarExpansion?(strongSelf.avatarListNode.listContainerNode.galleryEntries, (avatarNode, avatarNode.bounds, { [weak avatarNode] in + return (avatarNode?.view.snapshotContentTree(unhide: true), nil) + })) + } + } + + func updateAvatarIsHidden(_ isHidden: Bool) { + self.avatarListNode.avatarContainerNode.avatarNode.isHidden = isHidden + } + + func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, contentOffset: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, isContact: Bool, state: PeerInfoState, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { + let themeUpdated = self.presentationData?.theme !== presentationData.theme + self.presentationData = presentationData + + if themeUpdated { + self.titleCredibilityIconNode.image = PresentationResourcesItemList.verifiedPeerIcon(presentationData.theme) + } + + self.regularContentNode.alpha = state.isEditing ? 0.0 : 1.0 + self.editingContentNode.alpha = state.isEditing ? 1.0 : 0.0 + + let editingContentHeight = self.editingContentNode.update(width: width, safeInset: containerInset, statusBarHeight: statusBarHeight, navigationHeight: navigationHeight, peer: state.isEditing ? peer : nil, isContact: isContact, presentationData: presentationData, transition: transition) + transition.updateFrame(node: self.editingContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -contentOffset), size: CGSize(width: width, height: editingContentHeight))) + + var transitionSourceHeight: CGFloat = 0.0 + var transitionFraction: CGFloat = 0.0 + var transitionSourceAvatarFrame = CGRect() + var transitionSourceTitleFrame = CGRect() + var transitionSourceSubtitleFrame = CGRect() + + self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor + self.expandedBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor + + 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.updateAlpha(node: self.expandedBackgroundNode, alpha: transitionFraction) + } else { + let backgroundTransitionFraction: CGFloat = max(0.0, min(1.0, contentOffset / (212.0))) + transition.updateAlpha(node: self.expandedBackgroundNode, alpha: backgroundTransitionFraction) + } + + self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor + + let defaultButtonSize: CGFloat = 40.0 + let defaultMaxButtonSpacing: CGFloat = 40.0 + let expandedAvatarListHeight = min(width, containerHeight - 64.0) + let expandedAvatarListSize = CGSize(width: width, height: expandedAvatarListHeight) + + let buttonKeys: [PeerInfoHeaderButtonKey] = peerInfoHeaderButtons(peer: peer, cachedData: cachedData) + + if let peer = peer { + self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.medium(24.0), textColor: presentationData.theme.list.itemPrimaryTextColor) + + let subtitleString: NSAttributedString? + if let statusData = statusData { + let subtitleColor: UIColor + if statusData.isActivity { + subtitleColor = presentationData.theme.list.itemAccentColor + } else { + subtitleColor = presentationData.theme.list.itemSecondaryTextColor + } + subtitleString = NSAttributedString(string: statusData.text, font: Font.regular(15.0), textColor: subtitleColor) + } else { + subtitleString = nil + } + self.subtitleNode.attributedText = subtitleString + } + + let textSideInset: CGFloat = 16.0 + let expandedAvatarControlsHeight: CGFloat = 64.0 + let expandedAvatarHeight: CGFloat = expandedAvatarListSize.height + expandedAvatarControlsHeight + + 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)) + let avatarCenter = CGPoint(x: (1.0 - transitionFraction) * avatarFrame.midX + transitionFraction * transitionSourceAvatarFrame.midX, y: (1.0 - transitionFraction) * avatarFrame.midY + transitionFraction * transitionSourceAvatarFrame.midY) + + var isVerified = false + if let peer = peer, peer.isVerified { + isVerified = true + } + + let titleSize = self.titleNode.updateLayout(CGSize(width: width - textSideInset * 2.0 - (isVerified ? 16.0 : 0.0), height: .greatestFiniteMagnitude)) + let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: width - textSideInset * 2.0, height: .greatestFiniteMagnitude)) + + if let image = self.titleCredibilityIconNode.image { + transition.updateFrame(node: self.titleCredibilityIconNode, frame: CGRect(origin: CGPoint(x: titleSize.width + 4.0, y: floor((titleSize.height - image.size.height) / 2.0) + 1.0), size: image.size)) + self.titleCredibilityIconNode.isHidden = !isVerified + } + + let titleFrame: CGRect + let subtitleFrame: CGRect + if self.isAvatarExpanded { + titleFrame = CGRect(origin: CGPoint(x: 16.0, y: expandedAvatarHeight - expandedAvatarControlsHeight + 12.0 + (subtitleSize.height.isZero ? 10.0 : 0.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 + (subtitleSize.height.isZero ? 11.0 : 0.0)), size: titleSize) + subtitleFrame = CGRect(origin: CGPoint(x: floor((width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize) + } + + let titleLockOffset: CGFloat = 7.0 + (subtitleSize.height.isZero ? 8.0 : 0.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: expandedAvatarListSize) + + 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) + self.editingContentNode.avatarNode.update(peer: peer, theme: presentationData.theme) + 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: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 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, false { + 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 = expandedAvatarListSize + 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: -expandedAvatarListSize.width / 2.0, y: -expandedAvatarListSize.height / 2.0), size: expandedAvatarListSize) + } + avatarListContainerScale = 1.0 + max(0.0, -contentOffset / avatarListContainerFrame.height) + } 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.height / expandedAvatarListSize.height + let innerDeltaX = (avatarListContainerFrame.width - expandedAvatarListSize.width) / 2.0 + let innerDeltaY = (avatarListContainerFrame.height - expandedAvatarListSize.height) / 2.0 + transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerNode, scale: innerScale) + transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.contentNode, frame: CGRect(origin: CGPoint(x: innerDeltaX + expandedAvatarListSize.width / 2.0, y: innerDeltaY + expandedAvatarListSize.height / 2.0), size: CGSize())) + + transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.controlsContainerTransformNode, frame: CGRect(origin: CGPoint(x: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 2.0 - innerDeltaY), size: CGSize())) + transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerNode.controlsContainerNode, scale: 1.0 / innerScale) + transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.listContainerNode.controlsContainerTransformNode, scale: 1.0 / avatarListContainerScale) + transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.shadowNode, frame: CGRect(origin: CGPoint(x: -apparentAvatarFrame.minX, y: -apparentAvatarFrame.minY), size: CGSize(width: expandedAvatarListSize.width, height: navigationHeight))) + + if additive { + transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) + } else { + transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) + } + + self.avatarListNode.listContainerNode.update(size: expandedAvatarListSize, 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 = max(0.01, min(10.0, (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 + 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 + 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.regularContentNode.addSubnode(buttonNode) + } + + 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 + + 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 { + 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 + 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) + + let hiddenWhileExpanded: Bool + switch buttonKey { + case .mute: + hiddenWhileExpanded = true + default: + hiddenWhileExpanded = false + } + + if self.isAvatarExpanded, hiddenWhileExpanded { + 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 + } + } + + for key in self.buttonNodes.keys { + if !buttonKeys.contains(key) { + if let buttonNode = self.buttonNodes[key] { + self.buttonNodes.removeValue(forKey: key) + buttonNode.removeFromSupernode() + } + } + } + + let resolvedRegularHeight: CGFloat + if self.isAvatarExpanded { + resolvedRegularHeight = expandedAvatarListSize.height + expandedAvatarControlsHeight + } else { + resolvedRegularHeight = 212.0 + navigationHeight + } + + let backgroundFrame: CGRect + let separatorFrame: CGRect + + let resolvedHeight: CGFloat + if state.isEditing { + resolvedHeight = editingContentHeight + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + resolvedHeight - contentOffset), size: CGSize(width: width, height: 2000.0)) + separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: resolvedHeight - contentOffset), size: CGSize(width: width, height: UIScreenPixel)) + } else { + resolvedHeight = resolvedRegularHeight + backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + apparentHeight), size: CGSize(width: width, height: 2000.0)) + separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: apparentHeight), size: CGSize(width: width, height: UIScreenPixel)) + } + + transition.updateFrame(node: self.regularContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: resolvedHeight))) + + if additive { + transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrameAdditive(node: self.expandedBackgroundNode, frame: backgroundFrame) + transition.updateFrameAdditive(node: self.separatorNode, frame: separatorFrame) + } else { + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.expandedBackgroundNode, frame: backgroundFrame) + transition.updateFrame(node: self.separatorNode, frame: separatorFrame) + } + + return resolvedHeight + } + + private func buttonPressed(_ buttonNode: PeerInfoHeaderButtonNode) { + self.performButtonAction?(buttonNode.key) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + if result.isDescendant(of: self.navigationButtonContainer.view) { + return result + } + if !self.backgroundNode.frame.contains(point) { + return nil + } + if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view { + return nil + } + return result + } + + func updateIsAvatarExpanded(_ isAvatarExpanded: Bool, transition: ContainedViewLayoutTransition) { + if self.isAvatarExpanded != isAvatarExpanded { + self.isAvatarExpanded = isAvatarExpanded + if isAvatarExpanded { + self.avatarListNode.listContainerNode.selectFirstItem() + } + if case .animated = transition, !isAvatarExpanded { + self.avatarListNode.animateAvatarCollapse(transition: transition) + } + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift new file mode 100644 index 0000000000..976036603b --- /dev/null +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoPaneContainerNode.swift @@ -0,0 +1,553 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import TelegramPresentationData +import Postbox +import SyncCore +import TelegramCore +import AccountContext +import ContextUI + +protocol PeerInfoPaneNode: ASDisplayNode { + var isReady: Signal { get } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) + func scrollToTop() -> Bool + func transferVelocity(_ velocity: CGFloat) + func findLoadedMessage(id: MessageId) -> Message? + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? + func addToTransitionSurface(view: UIView) + func updateHiddenMedia() + func updateSelectedMessages(animated: Bool) +} + +final class PeerInfoPaneWrapper { + let key: PeerInfoPaneKey + let node: PeerInfoPaneNode + private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, PresentationData)? + + init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) { + self.key = key + self.node = node + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { + if let (currentSize, currentSideInset, currentBottomInset, visibleHeight, currentIsScrollingLockedAtTop, currentPresentationData) = self.appliedParams { + if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentPresentationData === presentationData { + return + } + } + self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, presentationData) + self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: synchronous, transition: transition) + } +} + +enum PeerInfoPaneKey { + case media + case files + case links + case voice + case music + case groupsInCommon +} + +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)) + } +} + +struct PeerInfoPaneSpecifier: Equatable { + var key: PeerInfoPaneKey + var title: String +} + +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.disablesInteractiveTransitionGestureRecognizer = true + 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)) + + let focusOnSelectedPane = self.currentParams?.1 != selectedPane + + 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 tabSizes.count == 1 { + for i in 0 ..< tabSizes.count { + let (paneNodeSize, paneNode) = tabSizes[i] + let leftOffset: CGFloat = 16.0 + + let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) + paneNode.frame = paneFrame + let areaSideInset: CGFloat = 16.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 if totalRawTabSize + CGFloat(tabSizes.count + 1) * spacing <= size.width { + let availableSpace = size.width + let availableSpacing = availableSpace - totalRawTabSize + let perTabSpacing = floor(availableSpacing / CGFloat(tabSizes.count + 1)) + + var leftOffset = perTabSpacing + 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 + let areaSideInset = floor(perTabSpacing / 2.0) + paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) + paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) + + leftOffset += paneNodeSize.width + perTabSpacing + + 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))) + if focusOnSelectedPane { + if selectedPane == paneList.first?.key { + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)) + } else if selectedPane == paneList.last?.key { + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, y: 0.0), size: self.scrollNode.bounds.size)) + } else { + let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0))) + transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)) + } + } + } else { + self.selectedLineNode.isHidden = true + } + } + + private func paneSelected(_ key: PeerInfoPaneKey) { + self.requestSelectPane?(key) + } +} + +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, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?)? + private(set) var currentPaneKey: PeerInfoPaneKey? + private(set) var currentPane: PeerInfoPaneWrapper? + + private var currentCandidatePaneKey: PeerInfoPaneKey? + private var candidatePane: (PeerInfoPaneWrapper, Disposable, Bool)? + + var selectionPanelNode: PeerInfoSelectionPanelNode? + + var chatControllerInteraction: ChatControllerInteraction? + var openPeerContextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? + + var currentPaneUpdated: (() -> Void)? + + private var currentAvailablePanes: [PeerInfoPaneKey]? + + 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.tabsContainerNode.requestSelectPane = { [weak self] key in + guard let strongSelf = self else { + return + } + if strongSelf.currentPaneKey == key { + strongSelf.currentPane?.node.scrollToTop() + return + } + if strongSelf.currentCandidatePaneKey == key { + return + } + strongSelf.currentCandidatePaneKey = key + + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) + } + } + } + + 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 updateHiddenMedia() { + self.currentPane?.node.updateHiddenMedia() + } + + func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return self.currentPane?.node.transitionNodeForGallery(messageId: messageId, media: media) + } + + func updateSelectedMessageIds(_ selectedMessageIds: Set?, animated: Bool) { + self.currentPane?.node.updateSelectedMessages(animated: animated) + self.candidatePane?.0.node.updateSelectedMessages(animated: animated) + } + + func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, transition: ContainedViewLayoutTransition) { + let previousAvailablePanes = self.currentAvailablePanes ?? [] + let availablePanes = data?.availablePanes ?? [] + self.currentAvailablePanes = availablePanes + + if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) { + var nextCandidatePaneKey: PeerInfoPaneKey? + if let index = previousAvailablePanes.index(of: currentPaneKey), index != 0 { + for i in (0 ... index - 1).reversed() { + if availablePanes.contains(previousAvailablePanes[i]) { + nextCandidatePaneKey = previousAvailablePanes[i] + } + } + } + if nextCandidatePaneKey == nil { + nextCandidatePaneKey = availablePanes.first + } + + if let nextCandidatePaneKey = nextCandidatePaneKey { + if self.currentCandidatePaneKey != nextCandidatePaneKey { + self.currentCandidatePaneKey = nextCandidatePaneKey + } + } else { + self.currentCandidatePaneKey = nil + if let (_, disposable, _) = self.candidatePane { + disposable.dispose() + self.candidatePane = nil + } + if let currentPane = self.currentPane { + self.currentPane = nil + currentPane.node.removeFromSupernode() + } + } + } else if self.currentPaneKey == nil { + self.currentCandidatePaneKey = availablePanes.first + } + + let previousCurrentPaneKey = self.currentPaneKey + + self.currentParams = (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) + + transition.updateAlpha(node: self.coveringBackgroundNode, alpha: expansionFraction) + + 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: -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))) + + let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight)) + + if let currentCandidatePaneKey = self.currentCandidatePaneKey { + if self.candidatePane?.0.key != currentCandidatePaneKey { + self.candidatePane?.1.dispose() + + let paneNode: PeerInfoPaneNode + switch currentCandidatePaneKey { + case .media: + paneNode = PeerInfoVisualMediaPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId) + case .files: + paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .file) + case .links: + paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .webPage) + case .voice: + paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .voiceOrInstantVideo) + case .music: + paneNode = PeerInfoListPaneNode(context: self.context, chatControllerInteraction: self.chatControllerInteraction!, peerId: self.peerId, tagMask: .music) + case .groupsInCommon: + paneNode = PeerInfoGroupsInCommonPaneNode(context: self.context, peerId: peerId, chatControllerInteraction: self.chatControllerInteraction!, openPeerContextAction: self.openPeerContextAction!, peers: data?.groupsInCommon ?? []) + } + + let disposable = MetaDisposable() + self.candidatePane = (PeerInfoPaneWrapper(key: currentCandidatePaneKey, node: paneNode), disposable, false) + + var shouldReLayout = false + disposable.set((paneNode.isReady + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + if let (candidatePane, disposable, _) = strongSelf.candidatePane { + strongSelf.candidatePane = (candidatePane, disposable, true) + + if shouldReLayout { + if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: strongSelf.currentPane != nil ? .animated(duration: 0.35, curve: .spring) : .immediate) + } + } + } + })) + shouldReLayout = true + } + } + + if let (candidatePane, _, isReady) = self.candidatePane, isReady { + let previousPane = self.currentPane + self.candidatePane = nil + self.currentPaneKey = candidatePane.key + self.currentCandidatePaneKey = nil + self.currentPane = candidatePane + + if let selectionPanelNode = self.selectionPanelNode { + self.insertSubnode(candidatePane.node, belowSubnode: selectionPanelNode) + } else { + self.addSubnode(candidatePane.node) + } + candidatePane.node.frame = paneFrame + candidatePane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: max(0.0, visibleHeight - paneFrame.minY), isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: .immediate) + + if let previousPane = previousPane { + let directionToRight: Bool + if let previousIndex = availablePanes.index(of: previousPane.key), let updatedIndex = availablePanes.index(of: candidatePane.key) { + directionToRight = previousIndex < updatedIndex + } else { + directionToRight = false + } + + let offset: CGFloat = directionToRight ? previousPane.node.bounds.width : -previousPane.node.bounds.width + + transition.animatePositionAdditive(node: candidatePane.node, offset: CGPoint(x: offset, y: 0.0)) + let previousNode = previousPane.node + transition.updateFrame(node: previousNode, frame: paneFrame.offsetBy(dx: -offset, dy: 0.0), completion: { [weak previousNode] _ in + previousNode?.removeFromSupernode() + }) + } + } else 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, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) + } + + 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: availablePanes.map { key in + let title: String + switch key { + case .media: + title = "Media" + case .files: + title = "Files" + case .links: + title = "Links" + case .voice: + title = "Voice Messages" + case .music: + title = "Audio" + case .groupsInCommon: + title = "Groups" + } + return PeerInfoPaneSpecifier(key: key, title: title) + }, selectedPane: self.currentPaneKey, transition: transition) + + if let (candidatePane, _, _) = self.candidatePane { + let paneTransition: ContainedViewLayoutTransition = .immediate + paneTransition.updateFrame(node: candidatePane.node, frame: paneFrame) + candidatePane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: paneTransition) + } + if !self.didSetIsReady && data != nil { + if let currentPane = self.currentPane { + self.didSetIsReady = true + self.isReady.set(currentPane.node.isReady) + } else if self.candidatePane == nil { + self.didSetIsReady = true + self.isReady.set(.single(true)) + } + } + if let previousCurrentPaneKey = previousCurrentPaneKey, self.currentPaneKey != previousCurrentPaneKey { + self.currentPaneUpdated?() + } + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift similarity index 51% rename from submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift rename to submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift index 1c4c6b0612..26aa03313d 100644 --- a/submodules/TelegramUI/TelegramUI/PeerInfoScreen.swift +++ b/submodules/TelegramUI/TelegramUI/PeerInfo/PeerInfoScreen.swift @@ -24,1960 +24,10 @@ import TelegramIntents import PeerInfoUI import SearchBarNode import SearchUI - -private enum PeerInfoHeaderButtonKey: Hashable { - case message - case call - case mute - case more - case addMember -} - -private enum PeerInfoHeaderButtonIcon { - case message - case call - 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 - - private var theme: PresentationTheme? - private var icon: PeerInfoHeaderButtonIcon? - - init(key: PeerInfoHeaderButtonKey, action: @escaping (PeerInfoHeaderButtonNode) -> Void) { - self.key = key - self.action = action - - self.containerNode = ASDisplayNode() - - self.backgroundNode = ASImageNode() - self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.displayWithoutProcessing = true - - self.textNode = ImmediateTextNode() - self.textNode.displaysAsynchronously = false - - super.init() - - self.addSubnode(self.containerNode) - self.containerNode.addSubnode(self.backgroundNode) - self.containerNode.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(.normal) - context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) - let imageName: String - switch icon { - case .message: - imageName = "Peer Info/ButtonMessage" - case .call: - imageName = "Peer Info/ButtonCall" - case .mute: - imageName = "Peer Info/ButtonMute" - case .unmute: - imageName = "Peer Info/ButtonUnmute" - case .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) - 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.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 imageSize = CGSize(width: min(size.width, size.height), height: min(size.width, size.height)) - let makeLayout = self.imageNode.asyncLayout() - let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) - let _ = applyLayout() - transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) - } -} - -private final class PeerInfoAvatarListContainerNode: ASDisplayNode { - private let context: AccountContext - - let controlsContainerNode: ASDisplayNode - let controlsContainerTransformNode: ASDisplayNode - let shadowNode: ASDisplayNode - - 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() - - self.controlsContainerNode = ASDisplayNode() - self.controlsContainerNode.isUserInteractionEnabled = false - - self.controlsContainerTransformNode = ASDisplayNode() - self.controlsContainerTransformNode.isUserInteractionEnabled = false - - self.shadowNode = ASDisplayNode() - //self.shadowNode.backgroundColor = .green - - super.init() - - self.backgroundColor = .black - - self.addSubnode(self.contentNode) - - self.controlsContainerNode.addSubnode(self.shadowNode) - self.controlsContainerTransformNode.addSubnode(self.controlsContainerNode) - self.addSubnode(self.controlsContainerTransformNode) - - 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 - let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) - 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 PeerInfoEditingAvatarNode: ASDisplayNode { - let context: AccountContext - let avatarNode: AvatarNode - - var tapped: (() -> Void)? - - init(context: AccountContext) { - self.context = context - let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0)) - 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: false, displayDimensions: CGSize(width: 100.0, height: 100.0)) - } - } -} - -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) - } -} - -private final class PeerInfoHeaderNavigationButton: HighlightableButtonNode { - private let regularTextNode: ImmediateTextNode - private let whiteTextNode: ImmediateTextNode - private let iconNode: ASImageNode - - private var key: PeerInfoHeaderNavigationButtonKey? - private var theme: PresentationTheme? - - var isWhite: Bool = false { - didSet { - if self.isWhite != oldValue { - self.regularTextNode.isHidden = self.isWhite - self.whiteTextNode.isHidden = !self.isWhite - } - } - } - - var action: (() -> Void)? - - override init() { - self.regularTextNode = ImmediateTextNode() - self.whiteTextNode = ImmediateTextNode() - self.whiteTextNode.isHidden = true - - self.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - - super.init() - - self.addSubnode(self.regularTextNode) - self.addSubnode(self.whiteTextNode) - self.addSubnode(self.iconNode) - - self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) - } - - @objc private func pressed() { - self.action?() - } - - func update(key: PeerInfoHeaderNavigationButtonKey, presentationData: PresentationData) -> CGSize { - let textSize: CGSize - if self.key != key || self.theme !== presentationData.theme { - self.key = key - self.theme = presentationData.theme - - let text: String - var icon: UIImage? - var isBold = false - switch key { - case .edit: - text = presentationData.strings.Common_Edit - case .done, .cancel, .selectionDone: - text = presentationData.strings.Common_Done - isBold = true - case .select: - text = presentationData.strings.Common_Select - case .search: - text = "" - icon = PresentationResourcesRootController.navigationCompactSearchIcon(presentationData.theme) - } - - let font: UIFont = isBold ? Font.semibold(17.0) : Font.regular(17.0) - - self.regularTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: presentationData.theme.rootController.navigationBar.accentTextColor) - self.whiteTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: .white) - self.iconNode.image = icon - - textSize = self.regularTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) - let _ = self.whiteTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) - } else { - textSize = self.regularTextNode.bounds.size - } - - let inset: CGFloat = 0.0 - let height: CGFloat = 44.0 - - let textFrame = CGRect(origin: CGPoint(x: inset, y: floor((height - textSize.height) / 2.0)), size: textSize) - self.regularTextNode.frame = textFrame - self.whiteTextNode.frame = textFrame - - if let image = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size) - - return CGSize(width: image.size.width + inset * 2.0, height: height) - } else { - return CGSize(width: textSize.width + inset * 2.0, height: height) - } - } -} - -private enum PeerInfoHeaderNavigationButtonKey { - case edit - case done - case cancel - case select - case selectionDone - case search -} - -private struct PeerInfoHeaderNavigationButtonSpec: Equatable { - let key: PeerInfoHeaderNavigationButtonKey - let isForExpandedView: Bool -} - -private final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode { - private var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:] - - private var currentButtons: [PeerInfoHeaderNavigationButtonSpec] = [] - - var isWhite: Bool = false { - didSet { - if self.isWhite != oldValue { - for (_, buttonNode) in self.buttonNodes { - buttonNode.isWhite = self.isWhite - } - } - } - } - - var performAction: ((PeerInfoHeaderNavigationButtonKey) -> Void)? - - override init() { - super.init() - } - - func update(size: CGSize, presentationData: PresentationData, buttons: [PeerInfoHeaderNavigationButtonSpec], expandFraction: CGFloat, transition: ContainedViewLayoutTransition) { - let maximumExpandOffset: CGFloat = 14.0 - let expandOffset: CGFloat = -expandFraction * maximumExpandOffset - if self.currentButtons != buttons { - self.currentButtons = buttons - - var nextRegularButtonOrigin = size.width - 16.0 - var nextExpandedButtonOrigin = size.width - 16.0 - for spec in buttons.reversed() { - let buttonNode: PeerInfoHeaderNavigationButton - var wasAdded = false - if let current = self.buttonNodes[spec.key] { - buttonNode = current - } else { - wasAdded = true - buttonNode = PeerInfoHeaderNavigationButton() - self.buttonNodes[spec.key] = buttonNode - self.addSubnode(buttonNode) - buttonNode.isWhite = self.isWhite - buttonNode.action = { [weak self] in - self?.performAction?(spec.key) - } - } - let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData) - var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin - let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) - nextButtonOrigin -= buttonSize.width + 4.0 - if spec.isForExpandedView { - nextExpandedButtonOrigin = nextButtonOrigin - } else { - nextRegularButtonOrigin = nextButtonOrigin - } - if wasAdded { - buttonNode.frame = buttonFrame - } else { - transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) - } - let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) - transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) - } - var removeKeys: [PeerInfoHeaderNavigationButtonKey] = [] - for (key, _) in self.buttonNodes { - if !buttons.contains(where: { $0.key == key }) { - removeKeys.append(key) - } - } - for key in removeKeys { - if let buttonNode = self.buttonNodes.removeValue(forKey: key) { - buttonNode.removeFromSupernode() - } - } - } else { - var nextRegularButtonOrigin = size.width - 16.0 - var nextExpandedButtonOrigin = size.width - 16.0 - for spec in buttons.reversed() { - if let buttonNode = self.buttonNodes[spec.key] { - let buttonSize = buttonNode.bounds.size - var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin - let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize) - nextButtonOrigin -= buttonSize.width + 4.0 - if spec.isForExpandedView { - nextExpandedButtonOrigin = nextButtonOrigin - } else { - nextRegularButtonOrigin = nextButtonOrigin - } - transition.updateFrameAdditiveToCenter(node: buttonNode, frame: buttonFrame) - let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction) - transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor) - } - } - } - } -} - -private final class PeerInfoHeaderRegularContentNode: ASDisplayNode { - -} - -private enum PeerInfoHeaderTextFieldNodeKey { - case firstName - case lastName - case title - case description -} - -private protocol PeerInfoHeaderTextFieldNode: ASDisplayNode { - var text: String { get } - - func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat -} - -private final class PeerInfoHeaderSingleLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode { - private let textNode: TextFieldNode - private let topSeparator: ASDisplayNode - - private var theme: PresentationTheme? - - var text: String { - return self.textNode.textField.text ?? "" - } - - override init() { - self.textNode = TextFieldNode() - self.topSeparator = ASDisplayNode() - - super.init() - - self.addSubnode(self.textNode) - self.addSubnode(self.topSeparator) - } - - func update(width: CGFloat, safeInset: CGFloat, hasPrevious: Bool, placeholder: String, isEnabled: Bool, presentationData: PresentationData, updateText: String?) -> CGFloat { - if self.theme !== presentationData.theme { - self.theme = presentationData.theme - self.textNode.textField.textColor = presentationData.theme.list.itemPrimaryTextColor - //self.textNode.textField.keyboardAppearance = presentationData.theme.keyboardAppearance - self.textNode.textField.tintColor = presentationData.theme.list.itemAccentColor - } - - let attributedPlaceholderText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.list.itemPlaceholderTextColor) - if self.textNode.textField.attributedPlaceholder == nil || !self.textNode.textField.attributedPlaceholder!.isEqual(to: attributedPlaceholderText) { - self.textNode.textField.attributedPlaceholder = attributedPlaceholderText - self.textNode.textField.accessibilityHint = attributedPlaceholderText.string - } - - if let updateText = updateText { - self.textNode.textField.text = updateText - } - - self.topSeparator.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - self.topSeparator.frame = CGRect(origin: CGPoint(x: safeInset + (hasPrevious ? 16.0 : 0.0), y: 0.0), size: CGSize(width: width, height: UIScreenPixel)) - - let height: CGFloat = 44.0 - - self.textNode.frame = CGRect(origin: CGPoint(x: safeInset + 16.0, y: floor((height - 40.0) / 2.0)), size: CGSize(width: max(1.0, width - 16.0 * 2.0), height: 40.0)) - - self.textNode.isUserInteractionEnabled = isEnabled - self.textNode.alpha = isEnabled ? 1.0 : 0.6 - - return height - } -} - -/*private final class PeerInfoHeaderMultiLineTextFieldNode: ASDisplayNode, PeerInfoHeaderTextFieldNode { - private let textNode: TextFieldNode - private let topSeparator: ASDisplayNode - - override init() { - self.textNode = TextFieldNode() - self.topSeparator = ASDisplayNode() - - super.init() - } - - func update(width: CGFloat, safeInset: CGFloat) -> CGFloat { - return 44.0 - } -}*/ - -private final class PeerInfoHeaderEditingContentNode: ASDisplayNode { - private let context: AccountContext - let avatarNode: PeerInfoEditingAvatarNode - - var itemNodes: [PeerInfoHeaderTextFieldNodeKey: PeerInfoHeaderTextFieldNode] = [:] - - init(context: AccountContext) { - self.context = context - self.avatarNode = PeerInfoEditingAvatarNode(context: context) - - super.init() - - self.addSubnode(self.avatarNode) - } - - func editingTextForKey(_ key: PeerInfoHeaderTextFieldNodeKey) -> String? { - return self.itemNodes[key]?.text - } - - func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, peer: Peer?, isContact: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { - 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.updateFrameAdditiveToCenter(node: self.avatarNode, frame: CGRect(origin: avatarFrame.center, size: CGSize())) - - var contentHeight: CGFloat = statusBarHeight + 10.0 + 100.0 + 20.0 - - var fieldKeys: [PeerInfoHeaderTextFieldNodeKey] = [] - if let _ = peer as? TelegramUser { - fieldKeys.append(.firstName) - fieldKeys.append(.lastName) - } - var hasPrevious = false - for key in fieldKeys { - let itemNode: PeerInfoHeaderTextFieldNode - var updateText: String? - if let current = self.itemNodes[key] { - itemNode = current - } else { - switch key { - case .firstName: - updateText = (peer as? TelegramUser)?.firstName ?? "" - case .lastName: - updateText = (peer as? TelegramUser)?.lastName ?? "" - case .title: - updateText = (peer as? TelegramUser)?.debugDisplayTitle ?? "" - case .description: - break - } - itemNode = PeerInfoHeaderSingleLineTextFieldNode() - self.itemNodes[key] = itemNode - self.addSubnode(itemNode) - } - let placeholder: String - var isEnabled = true - switch key { - case .firstName: - placeholder = "First Name" - isEnabled = isContact - case .lastName: - placeholder = "Last Name" - isEnabled = isContact - case .title: - placeholder = "Title" - case .description: - placeholder = "Description" - } - let itemHeight = itemNode.update(width: width, safeInset: safeInset, hasPrevious: hasPrevious, placeholder: placeholder, isEnabled: isEnabled, presentationData: presentationData, updateText: updateText) - transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight))) - contentHeight += itemHeight - hasPrevious = true - } - var removeKeys: [PeerInfoHeaderTextFieldNodeKey] = [] - for (key, _) in self.itemNodes { - if !fieldKeys.contains(key) { - removeKeys.append(key) - } - } - for key in removeKeys { - if let itemNode = self.itemNodes.removeValue(forKey: key) { - itemNode.removeFromSupernode() - } - } - - return contentHeight - } -} - -private final class PeerInfoHeaderNode: ASDisplayNode { - private var context: AccountContext - private var presentationData: PresentationData? - - private(set) var isAvatarExpanded: Bool - - let avatarListNode: PeerInfoAvatarListNode - - let regularContentNode: PeerInfoHeaderRegularContentNode - let editingContentNode: PeerInfoHeaderEditingContentNode - 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 expandedBackgroundNode: ASDisplayNode - let separatorNode: ASDisplayNode - let navigationButtonContainer: PeerInfoHeaderNavigationButtonContainerNode - - var performButtonAction: ((PeerInfoHeaderButtonKey) -> Void)? - var requestAvatarExpansion: (() -> Void)? - - var navigationTransition: PeerInfoHeaderNavigationTransition? - - init(context: AccountContext, avatarInitiallyExpanded: Bool) { - self.context = context - self.isAvatarExpanded = avatarInitiallyExpanded - - 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 - - self.regularContentNode = PeerInfoHeaderRegularContentNode() - self.editingContentNode = PeerInfoHeaderEditingContentNode(context: context) - self.editingContentNode.alpha = 0.0 - - self.navigationButtonContainer = PeerInfoHeaderNavigationButtonContainerNode() - - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - self.expandedBackgroundNode = ASDisplayNode() - self.expandedBackgroundNode.isLayerBacked = true - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - - super.init() - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.expandedBackgroundNode) - self.addSubnode(self.separatorNode) - self.titleNodeContainer.addSubnode(self.titleNode) - self.regularContentNode.addSubnode(self.titleNodeContainer) - self.subtitleNodeContainer.addSubnode(self.subtitleNode) - self.regularContentNode.addSubnode(self.subtitleNodeContainer) - self.regularContentNode.addSubnode(self.avatarListNode) - self.addSubnode(self.regularContentNode) - self.addSubnode(self.editingContentNode) - self.addSubnode(self.navigationButtonContainer) - - self.avatarListNode.avatarContainerNode.tapped = { [weak self] in - guard let strongSelf = self else { - return - } - if !strongSelf.isAvatarExpanded { - strongSelf.requestAvatarExpansion?() - } - } - } - - func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, contentOffset: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, presence: TelegramUserPresence?, isContact: Bool, state: PeerInfoState, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { - self.presentationData = presentationData - - self.regularContentNode.alpha = state.isEditing ? 0.0 : 1.0 - self.editingContentNode.alpha = state.isEditing ? 1.0 : 0.0 - - let editingContentHeight = self.editingContentNode.update(width: width, safeInset: containerInset, statusBarHeight: statusBarHeight, navigationHeight: navigationHeight, peer: state.isEditing ? peer : nil, isContact: isContact, presentationData: presentationData, transition: transition) - transition.updateFrame(node: self.editingContentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -contentOffset), size: CGSize(width: width, height: editingContentHeight))) - - var transitionSourceHeight: CGFloat = 0.0 - var transitionFraction: CGFloat = 0.0 - var transitionSourceAvatarFrame = CGRect() - var transitionSourceTitleFrame = CGRect() - var transitionSourceSubtitleFrame = CGRect() - - self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor - self.expandedBackgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor - - 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)!) - transition.updateAlpha(node: self.expandedBackgroundNode, alpha: transitionFraction) - } else { - let backgroundTransitionFraction: CGFloat = max(0.0, min(1.0, contentOffset / (212.0))) - transition.updateAlpha(node: self.expandedBackgroundNode, alpha: backgroundTransitionFraction) - //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 defaultButtonSize: CGFloat = 40.0 - let defaultMaxButtonSpacing: CGFloat = 40.0 - let expandedAvatarListHeight = min(width, containerHeight - 64.0) - let expandedAvatarListSize = CGSize(width: width, height: expandedAvatarListHeight) - - var buttonKeys: [PeerInfoHeaderButtonKey] = [] - - if let peer = peer { - buttonKeys.append(.message) - buttonKeys.append(.call) - buttonKeys.append(.mute) - buttonKeys.append(.more) - - 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 - let expandedAvatarControlsHeight: CGFloat = 64.0 - let expandedAvatarHeight: CGFloat = expandedAvatarListSize.height + expandedAvatarControlsHeight - - 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)) - 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 - 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 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: expandedAvatarListSize) - - 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) - self.editingContentNode.avatarNode.update(peer: peer, theme: presentationData.theme) - 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: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 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, false { - 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 = expandedAvatarListSize - 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: -expandedAvatarListSize.width / 2.0, y: -expandedAvatarListSize.height / 2.0), size: expandedAvatarListSize) - } - avatarListContainerScale = 1.0 + max(0.0, -contentOffset / avatarListContainerFrame.height) - } 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.height / expandedAvatarListSize.height - let innerDeltaX = (avatarListContainerFrame.width - expandedAvatarListSize.width) / 2.0 - let innerDeltaY = (avatarListContainerFrame.height - expandedAvatarListSize.height) / 2.0 - transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerNode, scale: innerScale) - transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.contentNode, frame: CGRect(origin: CGPoint(x: innerDeltaX + expandedAvatarListSize.width / 2.0, y: innerDeltaY + expandedAvatarListSize.height / 2.0), size: CGSize())) - - transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.controlsContainerTransformNode, frame: CGRect(origin: CGPoint(x: expandedAvatarListSize.width / 2.0, y: expandedAvatarListSize.height / 2.0 - innerDeltaY), size: CGSize())) - transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerNode.controlsContainerNode, scale: 1.0 / innerScale) - transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.listContainerNode.controlsContainerTransformNode, scale: 1.0 / avatarListContainerScale) - transition.updateFrameAdditive(node: self.avatarListNode.listContainerNode.shadowNode, frame: CGRect(origin: CGPoint(x: -apparentAvatarFrame.minX, y: -apparentAvatarFrame.minY), size: CGSize(width: expandedAvatarListSize.width, height: navigationHeight))) - - if additive { - transition.updateSublayerTransformScaleAdditive(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) - } else { - transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale) - } - - self.avatarListNode.listContainerNode.update(size: expandedAvatarListSize, 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 = max(0.01, min(10.0, (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 - 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 - 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.regularContentNode.addSubnode(buttonNode) - } - - 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 - - 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 { - 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 - 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) - - let hiddenWhileExpanded: Bool - switch buttonKey { - case .more: - hiddenWhileExpanded = false - default: - hiddenWhileExpanded = true - } - - if self.isAvatarExpanded, hiddenWhileExpanded { - 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 - } - } - - for key in self.buttonNodes.keys { - if !buttonKeys.contains(key) { - if let buttonNode = self.buttonNodes[key] { - self.buttonNodes.removeValue(forKey: key) - buttonNode.removeFromSupernode() - } - } - } - - let resolvedRegularHeight: CGFloat - if self.isAvatarExpanded { - resolvedRegularHeight = expandedAvatarListSize.height + expandedAvatarControlsHeight - } else { - resolvedRegularHeight = 212.0 + navigationHeight - } - - let backgroundFrame: CGRect - let separatorFrame: CGRect - - let resolvedHeight: CGFloat - if state.isEditing { - resolvedHeight = editingContentHeight - backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + resolvedHeight - contentOffset), size: CGSize(width: width, height: 2000.0)) - separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: resolvedHeight - contentOffset), size: CGSize(width: width, height: UIScreenPixel)) - } else { - resolvedHeight = resolvedRegularHeight - backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0 + apparentHeight), size: CGSize(width: width, height: 2000.0)) - separatorFrame = CGRect(origin: CGPoint(x: 0.0, y: apparentHeight), size: CGSize(width: width, height: UIScreenPixel)) - } - - transition.updateFrame(node: self.regularContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: resolvedHeight))) - - if additive { - transition.updateFrameAdditive(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrameAdditive(node: self.expandedBackgroundNode, frame: backgroundFrame) - transition.updateFrameAdditive(node: self.separatorNode, frame: separatorFrame) - } else { - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.expandedBackgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.separatorNode, frame: separatorFrame) - } - - return resolvedHeight - } - - private func buttonPressed(_ buttonNode: PeerInfoHeaderButtonNode) { - self.performButtonAction?(buttonNode.key) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - guard let result = super.hitTest(point, with: event) else { - return nil - } - if result.isDescendant(of: self.navigationButtonContainer.view) { - return result - } - if !self.backgroundNode.frame.contains(point) { - return nil - } - if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view { - return nil - } - return result - } - - func updateIsAvatarExpanded(_ isAvatarExpanded: Bool) { - self.isAvatarExpanded = isAvatarExpanded - } -} - -protocol PeerInfoPaneNode: ASDisplayNode { - var isReady: Signal { get } - - func update(size: CGSize, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) - func scrollToTop() -> Bool - func transferVelocity(_ velocity: CGFloat) - func findLoadedMessage(id: MessageId) -> Message? - func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? - func updateSelectedMessages(animated: Bool) -} - -final class PeerInfoPaneInteraction { - var selectedMessageIds: Set? - - let toggleMessageSelected: (MessageId) -> Void - let openPeer: (Peer) -> Void - - init( - toggleMessageSelected: @escaping (MessageId) -> Void, - openPeer: @escaping (Peer) -> Void - ) { - self.toggleMessageSelected = toggleMessageSelected - self.openPeer = openPeer - } -} - -private final class PeerInfoPaneWrapper { - let key: PeerInfoPaneKey - let node: PeerInfoPaneNode - private var appliedParams: (CGSize, CGFloat, Bool, PresentationData)? - - init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) { - self.key = key - self.node = node - } - - func update(size: CGSize, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { - if let (currentSize, visibleHeight, currentIsScrollingLockedAtTop, currentPresentationData) = self.appliedParams { - if currentSize == size && currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentPresentationData === presentationData { - return - } - } - self.appliedParams = (size, visibleHeight, isScrollingLockedAtTop, presentationData) - self.node.update(size: size, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, presentationData: presentationData, synchronous: synchronous, transition: transition) - } -} - -private enum PeerInfoPaneKey { - case media - case files - case links - case voice - case music - case groupsInCommon -} - -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.disablesInteractiveTransitionGestureRecognizer = true - 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)) - - let focusOnSelectedPane = self.currentParams?.1 != selectedPane - - 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))) - if focusOnSelectedPane { - if selectedPane == paneList.first?.key { - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)) - } else if selectedPane == paneList.last?.key { - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, y: 0.0), size: self.scrollNode.bounds.size)) - } else { - let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0))) - transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)) - } - } - } 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, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?)? - private(set) var currentPaneKey: PeerInfoPaneKey? - private(set) var currentPane: PeerInfoPaneWrapper? - - private var currentCandidatePaneKey: PeerInfoPaneKey? - private var candidatePane: (PeerInfoPaneWrapper, Disposable, Bool)? - - var selectionPanelNode: PeerInfoSelectionPanelNode? - - var _paneInteraction: PeerInfoPaneInteraction? - var paneInteraction: PeerInfoPaneInteraction { - return self._paneInteraction! - } - - var openMessage: ((MessageId) -> Bool)? - var toggleMessageSelected: ((MessageId) -> Void)? - var openPeer: ((Peer) -> Void)? - var currentPaneUpdated: (() -> Void)? - - 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._paneInteraction = PeerInfoPaneInteraction( - toggleMessageSelected: { [weak self] id in - guard let strongSelf = self else { - return - } - strongSelf.toggleMessageSelected?(id) - }, - openPeer: { [weak self] peer in - guard let strongSelf = self else { - return - } - strongSelf.openPeer?(peer) - } - ) - - self.addSubnode(self.separatorNode) - self.addSubnode(self.coveringBackgroundNode) - self.addSubnode(self.tabsContainerNode) - self.addSubnode(self.tapsSeparatorNode) - - self.tabsContainerNode.requestSelectPane = { [weak self] key in - guard let strongSelf = self else { - return - } - if strongSelf.currentPaneKey == key { - return - } - if strongSelf.currentCandidatePaneKey == key { - return - } - strongSelf.currentCandidatePaneKey = key - - if let (size, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { - strongSelf.update(size: size, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) - } - } - } - - 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 updateSelectedMessageIds(_ selectedMessageIds: Set?, animated: Bool) { - if self.paneInteraction.selectedMessageIds != selectedMessageIds { - self.paneInteraction.selectedMessageIds = selectedMessageIds - self.currentPane?.node.updateSelectedMessages(animated: animated) - self.candidatePane?.0.node.updateSelectedMessages(animated: animated) - } - } - - func update(size: CGSize, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, transition: ContainedViewLayoutTransition) { - let availablePanes = data?.availablePanes ?? [] - - let previousCurrentPaneKey = self.currentPaneKey - if availablePanes.isEmpty { - self.currentPaneKey = nil - self.currentCandidatePaneKey = nil - if let (_, disposable, _) = self.candidatePane { - disposable.dispose() - self.candidatePane = nil - } - if let currentPane = self.currentPane { - self.currentPane = nil - currentPane.node.removeFromSupernode() - } - } else if (self.currentParams?.data?.availablePanes ?? []).isEmpty { - self.currentCandidatePaneKey = availablePanes.first - } - - self.currentParams = (size, visibleHeight, expansionFraction, presentationData, data) - - transition.updateAlpha(node: self.coveringBackgroundNode, alpha: expansionFraction) - - 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: -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))) - - let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight)) - - if let currentCandidatePaneKey = self.currentCandidatePaneKey { - if self.candidatePane?.0.key != currentCandidatePaneKey { - self.candidatePane?.1.dispose() - - let paneNode: PeerInfoPaneNode - switch currentCandidatePaneKey { - case .media: - paneNode = PeerInfoVisualMediaPaneNode(context: self.context, openMessage: { [weak self] id in - return self?.openMessage?(id) ?? false - }, peerId: self.peerId, interaction: self.paneInteraction) - case .files: - paneNode = PeerInfoListPaneNode(context: self.context, openMessage: { [weak self] id in - return self?.openMessage?(id) ?? false - }, peerId: self.peerId, tagMask: .file, interaction: self.paneInteraction) - case .links: - paneNode = PeerInfoListPaneNode(context: self.context, openMessage: { [weak self] id in - return self?.openMessage?(id) ?? false - }, peerId: self.peerId, tagMask: .webPage, interaction: self.paneInteraction) - case .voice: - paneNode = PeerInfoListPaneNode(context: self.context, openMessage: { [weak self] id in - return self?.openMessage?(id) ?? false - }, peerId: self.peerId, tagMask: .voiceOrInstantVideo, interaction: self.paneInteraction) - case .music: - paneNode = PeerInfoListPaneNode(context: self.context, openMessage: { [weak self] id in - return self?.openMessage?(id) ?? false - }, peerId: self.peerId, tagMask: .music, interaction: self.paneInteraction) - case .groupsInCommon: - paneNode = PeerInfoGroupsInCommonPaneNode(context: self.context, peerId: peerId, interaction: self.paneInteraction, peers: data?.groupsInCommon ?? []) - } - - let disposable = MetaDisposable() - self.candidatePane = (PeerInfoPaneWrapper(key: currentCandidatePaneKey, node: paneNode), disposable, false) - - var shouldReLayout = false - disposable.set((paneNode.isReady - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let strongSelf = self else { - return - } - if let (candidatePane, disposable, _) = strongSelf.candidatePane { - strongSelf.candidatePane = (candidatePane, disposable, true) - - if shouldReLayout { - if let (size, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { - strongSelf.update(size: size, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: strongSelf.currentPane != nil ? .animated(duration: 0.35, curve: .spring) : .immediate) - } - } - } - })) - shouldReLayout = true - } - } - - if let (candidatePane, _, isReady) = self.candidatePane, isReady { - let previousPane = self.currentPane - self.candidatePane = nil - self.currentPaneKey = candidatePane.key - self.currentCandidatePaneKey = nil - self.currentPane = candidatePane - - if let selectionPanelNode = self.selectionPanelNode { - self.insertSubnode(candidatePane.node, belowSubnode: selectionPanelNode) - } else { - self.addSubnode(candidatePane.node) - } - candidatePane.node.frame = paneFrame - candidatePane.update(size: paneFrame.size, visibleHeight: max(0.0, visibleHeight - paneFrame.minY), isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: .immediate) - - if let previousPane = previousPane { - let directionToRight: Bool - if let previousIndex = availablePanes.index(of: previousPane.key), let updatedIndex = availablePanes.index(of: candidatePane.key) { - directionToRight = previousIndex < updatedIndex - } else { - directionToRight = false - } - - let offset: CGFloat = directionToRight ? previousPane.node.bounds.width : -previousPane.node.bounds.width - - transition.animatePositionAdditive(node: candidatePane.node, offset: CGPoint(x: offset, y: 0.0)) - let previousNode = previousPane.node - transition.updateFrame(node: previousNode, frame: paneFrame.offsetBy(dx: -offset, dy: 0.0), completion: { [weak previousNode] _ in - previousNode?.removeFromSupernode() - }) - } - } else 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, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) - } - - 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: availablePanes.map { key in - let title: String - switch key { - case .media: - title = "Media" - case .files: - title = "Files" - case .links: - title = "Links" - case .voice: - title = "Voice Messages" - case .music: - title = "Audio" - case .groupsInCommon: - title = "Groups" - } - return PeerInfoPaneSpecifier(key: key, title: title) - }, selectedPane: self.currentPaneKey, transition: transition) - - if let (candidatePane, _, _) = self.candidatePane { - let paneTransition: ContainedViewLayoutTransition = .immediate - paneTransition.updateFrame(node: candidatePane.node, frame: paneFrame) - candidatePane.update(size: paneFrame.size, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, presentationData: presentationData, synchronous: true, transition: paneTransition) - } - if !self.didSetIsReady && data != nil { - if let currentPane = self.currentPane { - self.didSetIsReady = true - self.isReady.set(currentPane.node.isReady) - } else if self.candidatePane == nil { - self.didSetIsReady = true - self.isReady.set(.single(true)) - } - } - if let previousCurrentPaneKey = previousCurrentPaneKey, self.currentPaneKey != previousCurrentPaneKey { - self.currentPaneUpdated?() - } - } -} +import ContextUI +import OpenInExternalAppUI +import SafariServices +import GalleryUI protocol PeerInfoScreenItem: class { var id: AnyHashable { get } @@ -2090,7 +140,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { } } -private final class PeerInfoSelectionPanelNode: ASDisplayNode { +final class PeerInfoSelectionPanelNode: ASDisplayNode { private let context: AccountContext private let peerId: PeerId @@ -2197,7 +247,7 @@ private final class PeerInfoSelectionPanelNode: ASDisplayNode { }, displaySearchResultsTooltip: { _, _ in }, statuses: nil) - selectionPanel.interfaceInteraction = interfaceInteraction + self.selectionPanel.interfaceInteraction = interfaceInteraction super.init() @@ -2206,192 +256,28 @@ private final class PeerInfoSelectionPanelNode: ASDisplayNode { self.addSubnode(self.selectionPanel) } - func update(width: CGFloat, safeInset: CGFloat, metrics: LayoutMetrics, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { + func update(layout: ContainerViewLayout, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { self.backgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor let interfaceState = ChatPresentationInterfaceState(chatWallpaper: .color(0), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners(mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true), accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: .peer(self.peerId), isScheduledMessages: false) - let panelHeight = self.selectionPanel.updateLayout(width: width, leftInset: safeInset, rightInset: safeInset, maxHeight: 0.0, isSecondary: false, transition: transition, interfaceState: interfaceState, metrics: metrics) + let panelHeight = self.selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: 0.0, isSecondary: false, transition: transition, interfaceState: interfaceState, metrics: layout.metrics) - transition.updateFrame(node: self.selectionPanel, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))) - transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))) - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel))) + transition.updateFrame(node: self.selectionPanel, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeight))) - return panelHeight + let panelHeightWithInset = panelHeight + layout.intrinsicInsets.bottom + + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeightWithInset))) + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + return panelHeightWithInset } } -private final class PeerInfoState { - let isEditing: Bool - let isSearching: Bool - let selectedMessageIds: Set? - - init( - isEditing: Bool, - isSearching: Bool, - selectedMessageIds: Set? - ) { - self.isEditing = isEditing - self.isSearching = isSearching - self.selectedMessageIds = selectedMessageIds - } - - func withIsEditing(_ isEditing: Bool) -> PeerInfoState { - return PeerInfoState( - isEditing: isEditing, - isSearching: self.isSearching, - selectedMessageIds: self.selectedMessageIds - ) - } - - func withSelectedMessageIds(_ selectedMessageIds: Set?) -> PeerInfoState { - return PeerInfoState( - isEditing: self.isEditing, - isSearching: self.isSearching, - selectedMessageIds: selectedMessageIds - ) - } -} - -private final class PeerInfoScreenData { - let peer: Peer? - let cachedData: CachedPeerData? - let presence: TelegramUserPresence? - let notificationSettings: TelegramPeerNotificationSettings? - let globalNotificationSettings: GlobalNotificationSettings? - let isContact: Bool - let availablePanes: [PeerInfoPaneKey] - let groupsInCommon: [Peer]? - - init( - peer: Peer?, - cachedData: CachedPeerData?, - presence: TelegramUserPresence?, - notificationSettings: TelegramPeerNotificationSettings?, - globalNotificationSettings: GlobalNotificationSettings?, - isContact: Bool, - availablePanes: [PeerInfoPaneKey], - groupsInCommon: [Peer]? - ) { - self.peer = peer - self.cachedData = cachedData - self.presence = presence - self.notificationSettings = notificationSettings - self.globalNotificationSettings = globalNotificationSettings - self.isContact = isContact - self.availablePanes = availablePanes - self.groupsInCommon = groupsInCommon - } -} - -private enum PeerInfoScreenInputData: Equatable { - case none - case user -} - -private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId) -> Signal<[PeerInfoPaneKey], NoError> { - let tags: [(MessageTags, PeerInfoPaneKey)] = [ - (.photoOrVideo, .media), - (.file, .files), - (.music, .music), - (.voiceOrInstantVideo, .voice), - (.webPage, .links) - ] - return combineLatest(tags.map { tagAndKey -> Signal in - let (tag, key) = tagAndKey - return context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId), index: .upperBound, anchorIndex: .upperBound, count: 2, clipHoles: false, fixedCombinedReadStates: nil, tagMask: tag) - |> map { (view, _, _) -> PeerInfoPaneKey? in - if view.entries.isEmpty { - return nil - } else { - return key - } - } - }) - |> map { keys -> [PeerInfoPaneKey] in - return keys.compactMap { $0 } - } - |> distinctUntilChanged - /*return context.account.postbox.combinedView(keys: tags.map { (tag, _) -> PostboxViewKey in - return .historyTagInfo(peerId: peerId, tag: tag) - }) - |> map { view -> [PeerInfoPaneKey] in - return tags.compactMap { (tag, key) -> PeerInfoPaneKey? in - if let info = view.views[.historyTagInfo(peerId: peerId, tag: tag)] as? HistoryTagInfoView, !info.isEmpty { - return key - } else { - return nil - } - } - } - |> distinctUntilChanged*/ -} - -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, - globalNotificationSettings: nil, - isContact: false, - availablePanes: [], - groupsInCommon: nil - )) - case .user: - let groupsInCommonSignal: Signal<[Peer]?, NoError> = .single(nil) - |> then( - groupsInCommon(account: context.account, peerId: peerId) - |> map(Optional.init) - ) - let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.globalNotifications])) - return combineLatest( - context.account.viewTracker.peerView(peerId, updateData: true), - peerInfoAvailableMediaPanes(context: context, peerId: peerId), - context.account.postbox.combinedView(keys: [.peerChatState(peerId: peerId), globalNotificationsKey]), - groupsInCommonSignal - ) - |> map { peerView, availablePanes, combinedView, groupsInCommon -> PeerInfoScreenData in - var globalNotificationSettings: GlobalNotificationSettings = .defaultSettings - if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView { - if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { - globalNotificationSettings = settings - } - } - - var availablePanes = availablePanes - if let groupsInCommon = groupsInCommon, !groupsInCommon.isEmpty { - availablePanes.append(.groupsInCommon) - } - - return PeerInfoScreenData( - peer: peerView.peers[peerId], - cachedData: peerView.cachedData, - presence: peerView.peerPresences[peerId] as? TelegramUserPresence, - notificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, - globalNotificationSettings: globalNotificationSettings, - isContact: peerView.peerIsContact, - availablePanes: availablePanes, - groupsInCommon: groupsInCommon - ) - } - } - } +private enum PeerInfoBotCommand { + case settings + case help + case privacy } private final class PeerInfoInteraction { @@ -2403,6 +289,10 @@ private final class PeerInfoInteraction { let requestDeleteContact: () -> Void let openAddContact: () -> Void let updateBlocked: (Bool) -> Void + let openReport: () -> Void + let openShareBot: () -> Void + let openAddBotToGroup: () -> Void + let performBotCommand: (PeerInfoBotCommand) -> Void init( openUsername: @escaping (String) -> Void, @@ -2412,7 +302,11 @@ private final class PeerInfoInteraction { editingToggleShowMessageText: @escaping (Bool) -> Void, requestDeleteContact: @escaping () -> Void, openAddContact: @escaping () -> Void, - updateBlocked: @escaping (Bool) -> Void + updateBlocked: @escaping (Bool) -> Void, + openReport: @escaping () -> Void, + openShareBot: @escaping () -> Void, + openAddBotToGroup: @escaping () -> Void, + performBotCommand: @escaping (PeerInfoBotCommand) -> Void ) { self.openUsername = openUsername self.openPhone = openPhone @@ -2422,6 +316,10 @@ private final class PeerInfoInteraction { self.requestDeleteContact = requestDeleteContact self.openAddContact = openAddContact self.updateBlocked = updateBlocked + self.openReport = openReport + self.openShareBot = openShareBot + self.openAddBotToGroup = openAddBotToGroup + self.performBotCommand = performBotCommand } } @@ -2447,23 +345,57 @@ private func peerInfoSectionItems(data: PeerInfoScreenData?, presentationData: P } } if !data.isContact { - items.append(PeerInfoScreenActionItem(id: 3, text: "Add Contact", action: { - interaction.openAddContact() - })) + if let botInfo = user.botInfo { + if botInfo.flags.contains(.worksWithGroups) { + items.append(PeerInfoScreenActionItem(id: 6, text: presentationData.strings.UserInfo_InviteBotToGroup, action: { + interaction.openAddBotToGroup() + })) + } + items.append(PeerInfoScreenActionItem(id: 7, text: presentationData.strings.UserInfo_ShareBot, action: { + interaction.openShareBot() + })) + + if let cachedData = data.cachedData as? CachedUserData, let botInfo = cachedData.botInfo { + for command in botInfo.commands { + if command.text == "settings" { + items.append(PeerInfoScreenActionItem(id: 8, text: presentationData.strings.UserInfo_BotSettings, action: { + interaction.performBotCommand(.settings) + })) + } else if command.text == "help" { + items.append(PeerInfoScreenActionItem(id: 9, text: presentationData.strings.UserInfo_BotHelp, action: { + interaction.performBotCommand(.help) + })) + } else if command.text == "privacy" { + items.append(PeerInfoScreenActionItem(id: 10, text: presentationData.strings.UserInfo_BotPrivacy, action: { + interaction.performBotCommand(.privacy) + })) + } + } + } + } else { + items.append(PeerInfoScreenActionItem(id: 3, text: "Add Contact", action: { + interaction.openAddContact() + })) + } if let cachedData = data.cachedData as? CachedUserData { if cachedData.isBlocked { - items.append(PeerInfoScreenActionItem(id: 4, text: "Unblock", action: { + items.append(PeerInfoScreenActionItem(id: 4, text: user.botInfo != nil ? "Restart Bot" : "Unblock", action: { interaction.updateBlocked(false) })) } else { if user.flags.contains(.isSupport) { } else { - items.append(PeerInfoScreenActionItem(id: 4, text: "Block User", color: .destructive, action: { + items.append(PeerInfoScreenActionItem(id: 4, text: user.botInfo != nil ? "Stop Bot" : "Block User", color: .destructive, action: { interaction.updateBlocked(true) })) } } } + if user.botInfo != nil, !user.isVerified { + items.append(PeerInfoScreenActionItem(id: 5, text: presentationData.strings.ReportPeer_Report, action: { + interaction.openReport() + })) + } } } return items @@ -2547,6 +479,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private var chatInterfaceInteraction: ChatControllerInteraction { return self._chatInterfaceInteraction! } + private var hiddenMediaDisposable: Disposable? + private let hiddenAvatarRepresentationDisposable = MetaDisposable() private(set) var validLayout: (ContainerViewLayout, CGFloat)? private(set) var data: PeerInfoScreenData? @@ -2558,6 +492,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD private var dataDisposable: Disposable? private let activeActionDisposable = MetaDisposable() + private let resolveUrlDisposable = MetaDisposable() private let _ready = Promise() var ready: Promise { @@ -2605,56 +540,33 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, updateBlocked: { [weak self] block in self?.updateBlocked(block: block) + }, + openReport: { [weak self] in + self?.openReport() + }, + openShareBot: { [weak self] in + self?.openShareBot() + }, + openAddBotToGroup: { [weak self] in + self?.openAddBotToGroup() + }, + performBotCommand: { [weak self] command in + self?.performBotCommand(command: command) } ) - self._chatInterfaceInteraction = ChatControllerInteraction(openMessage: { message, mode in - /*if let strongSelf = self, strongSelf.isNodeLoaded, let galleryMessage = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id) { - guard let navigationController = strongSelf.navigationController as? NavigationController else { - return false - } - strongSelf.mediaCollectionDisplayNode.view.endEditing(true) - return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, message: galleryMessage.message, standalone: false, reverseMessageGalleryOrder: true, navigationController: navigationController, dismissInput: { - self?.mediaCollectionDisplayNode.view.endEditing(true) - }, present: { c, a in - self?.present(c, in: .window(.root), with: a, blockInteraction: true) - }, transitionNode: { messageId, media in - if let strongSelf = self { - return strongSelf.mediaCollectionDisplayNode.transitionNodeForGallery(messageId: messageId, media: media) - } - return nil - }, addToTransitionSurface: { view in - if let strongSelf = self { - var belowSubview: UIView? - if let historyNode = strongSelf.mediaCollectionDisplayNode.historyNode as? ChatHistoryGridNode { - if let lowestSectionNode = historyNode.lowestSectionNode() { - belowSubview = lowestSectionNode.view - } - } - strongSelf.mediaCollectionDisplayNode.historyNode - if let belowSubview = belowSubview { - strongSelf.mediaCollectionDisplayNode.historyNode.view.insertSubview(view, belowSubview: belowSubview) - } else { - strongSelf.mediaCollectionDisplayNode.historyNode.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 })) - }*/ - return false - }, openPeer: { id, navigation, _ in - /*if let strongSelf = self, let id = id, let navigationController = strongSelf.navigationController as? NavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id))) - }*/ + self._chatInterfaceInteraction = ChatControllerInteraction(openMessage: { [weak self] message, mode in + guard let strongSelf = self else { + return false + } + return strongSelf.openMessage(id: message.id) + }, openPeer: { [weak self] id, navigation, _ in + if let id = id { + self?.openPeer(peerId: id, navigation: navigation) + } }, openPeerMention: { _ in - }, openMessageContextMenu: { message, _, _, _, _ in - /*guard let strongSelf = self else { + }, openMessageContextMenu: { [weak self] message, _, _, _, _ in + guard let strongSelf = self else { return } let items = (chatAvailableMessageActionsImpl(postbox: strongSelf.context.account.postbox, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id]) @@ -2662,28 +574,28 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD var messageIds = Set() messageIds.insert(message.id) - if let strongSelf = self, strongSelf.isNodeLoaded { - if let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id)?.message { + if let strongSelf = self { + if let message = strongSelf.paneContainerNode.findLoadedMessage(id: message.id) { let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetButtonItem] = [] items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.SharedMedia_ViewInChat, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { + if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(message.id))) } })) items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuForward, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.forwardMessages(messageIds) + strongSelf.forwardMessages(messageIds: messageIds) } })) if actions.options.contains(.deleteLocally) || actions.options.contains(.deleteGlobally) { items.append( ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { - strongSelf.deleteMessages(messageIds) + strongSelf.deleteMessages(messageIds: Set(messageIds)) } })) } @@ -2692,18 +604,18 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD actionSheet?.dismissAnimated() }) ])]) - strongSelf.mediaCollectionDisplayNode.view.endEditing(true) - strongSelf.present(actionSheet, in: .window(.root)) + strongSelf.view.endEditing(true) + strongSelf.controller?.present(actionSheet, in: .window(.root)) } } - })*/ - }, openMessageContextActions: { message, node, rect, gesture in - /*guard let strongSelf = self else { + }) + }, openMessageContextActions: { [weak self] message, node, rect, gesture in + guard let strongSelf = self else { gesture?.cancel() return } - let _ = (chatMediaListPreviewControllerData(context: strongSelf.context, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongSelf.navigationController as? NavigationController) + let _ = (chatMediaListPreviewControllerData(context: strongSelf.context, message: message, standalone: false, reverseMessageGalleryOrder: false, navigationController: strongSelf.controller?.navigationController as? NavigationController) |> deliverOnMainQueue).start(next: { previewData in guard let strongSelf = self else { gesture?.cancel() @@ -2718,7 +630,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { - if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { + if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId), subject: .message(message.id))) } }) @@ -2727,7 +639,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuForward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, f in c.dismiss(completion: { if let strongSelf = self { - strongSelf.forwardMessages([message.id]) + strongSelf.forwardMessages(messageIds: [message.id]) } }) }))) @@ -2759,7 +671,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD items.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { c, f in c.dismiss(completion: { if let strongSelf = self { - strongSelf.updateInterfaceState(animated: true, { $0.withoutSelectionState() }) + strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forEveryone).start() } }) @@ -2778,7 +690,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD items.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { c, f in c.dismiss(completion: { if let strongSelf = self { - strongSelf.updateInterfaceState(animated: true, { $0.withoutSelectionState() }) + strongSelf.headerNode.navigationButtonContainer.performAction?(.selectionDone) let _ = deleteMessagesInteractively(account: strongSelf.context.account, messageIds: Array(messageIds), type: .forLocalPeer).start() } }) @@ -2798,30 +710,35 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD case let .gallery(gallery): gallery.setHintWillBePresentedInPreviewingContext(true) let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: node)), items: items, reactionItems: [], gesture: gesture) - strongSelf.presentInGlobalOverlay(contextController) + strongSelf.controller?.presentInGlobalOverlay(contextController) case .instantPage: break } } - })*/ + }) }, navigateToMessage: { fromId, id in - /*if let strongSelf = self, strongSelf.isNodeLoaded { - if id.peerId == strongSelf.peerId { - var fromIndex: MessageIndex? - - if let message = strongSelf.mediaCollectionDisplayNode.historyNode.messageInCurrentHistoryView(fromId) { - fromIndex = message.index - } - } else { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id.peerId), subject: .message(id))) - } - }*/ }, tapMessage: nil, clickThroughMessage: { - //self?.view.endEditing(true) - }, toggleMessagesSelection: { ids, value in - /*if let strongSelf = self, strongSelf.isNodeLoaded { - strongSelf.updateInterfaceState(animated: true, { $0.withToggledSelectedMessages(ids, value: value) }) - }*/ + }, toggleMessagesSelection: { [weak self] ids, value in + guard let strongSelf = self else { + return + } + if var selectedMessageIds = strongSelf.state.selectedMessageIds { + for id in ids { + if value { + selectedMessageIds.insert(id) + } else { + selectedMessageIds.remove(id) + } + } + strongSelf.state = strongSelf.state.withSelectedMessageIds(selectedMessageIds) + } else { + strongSelf.state = strongSelf.state.withSelectedMessageIds(value ? Set(ids) : Set()) + } + strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } + if let (layout, navigationHeight) = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) + } + strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _ in @@ -2831,21 +748,37 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, requestMessageActionCallback: { _, _, _ in }, requestMessageActionUrlAuth: { _, _, _ in }, activateSwitchInline: { _, _ in - }, openUrl: { url, _, external, _ in - //self?.openUrl(url, external: external ?? false) + }, openUrl: { [weak self] url, _, external, _ in + guard let strongSelf = self else { + return + } + strongSelf.openUrl(url: url, external: external ?? false) }, shareCurrentLocation: { }, shareAccountContact: { }, sendBotCommand: { _, _ in - }, openInstantPage: { message, associatedData in - /*if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.navigationController as? NavigationController, let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id)?.message { - openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) - }*/ - }, openWallpaper: { message in - /*if let strongSelf = self, strongSelf.isNodeLoaded, let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id)?.message { - openChatWallpaper(context: strongSelf.context, message: message, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a, blockInteraction: true) - }) - }*/ + }, openInstantPage: { [weak self] message, associatedData in + guard let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController else { + return + } + var foundGalleryMessage: Message? + if let searchContentNode = strongSelf.searchDisplayController?.contentNode as? ChatHistorySearchContainerNode { + if let galleryMessage = searchContentNode.messageForGallery(message.id) { + let _ = (strongSelf.context.account.postbox.transaction { transaction -> Void in + if transaction.getMessage(galleryMessage.id) == nil { + storeMessageFromSearch(transaction: transaction, message: galleryMessage) + } + }).start() + foundGalleryMessage = galleryMessage + } + } + if foundGalleryMessage == nil, let galleryMessage = strongSelf.paneContainerNode.findLoadedMessage(id: message.id) { + foundGalleryMessage = galleryMessage + } + + if let foundGalleryMessage = foundGalleryMessage { + openChatInstantPage(context: strongSelf.context, message: foundGalleryMessage, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController) + } + }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in @@ -2859,55 +792,55 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, reactionContainerNode: { return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in - }, longTap: { content, _ in - /*if let strongSelf = self { - strongSelf.view.endEditing(true) - switch content { - case let .url(url): - let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1 - let openText = canOpenIn ? strongSelf.presentationData.strings.Conversation_FileOpenIn : strongSelf.presentationData.strings.Conversation_LinkDialogOpen - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: url), - ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - if canOpenIn { - let actionSheet = OpenInActionSheetController(context: strongSelf.context, item: .url(url: url), openUrl: { [weak self] url in - if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { - strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { - }) - } + }, longTap: { [weak self] content, _ in + guard let strongSelf = self else { + return + } + strongSelf.view.endEditing(true) + switch content { + case let .url(url): + let canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1 + let openText = canOpenIn ? strongSelf.presentationData.strings.Conversation_FileOpenIn : strongSelf.presentationData.strings.Conversation_LinkDialogOpen + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: url), + ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + if canOpenIn { + let actionSheet = OpenInActionSheetController(context: strongSelf.context, item: .url(url: url), openUrl: { [weak self] url in + if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { + strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.presentationData, navigationController: navigationController, dismissInput: { }) - strongSelf.present(actionSheet, in: .window(.root)) - } else { - strongSelf.context.sharedContext.applicationBindings.openUrl(url) } - } - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = url - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let link = URL(string: url) { - let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) - } - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.present(actionSheet, in: .window(.root)) - default: - break - } - }*/ + }) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + } else { + strongSelf.context.sharedContext.applicationBindings.openUrl(url) + } + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = url + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.controller?.present(actionSheet, in: .window(.root)) + default: + break + } }, openCheckoutOrReceipt: { _ in }, openSearch: { - //self?.activateSearch() }, setupReply: { _ in }, canSetupReply: { _ in return false @@ -2934,6 +867,19 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }, cancelInteractiveKeyboardGestures: { }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false)) + 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.chatInterfaceInteraction.hiddenMedia = hiddenMedia + strongSelf.paneContainerNode.updateHiddenMedia() + }) self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor @@ -2951,35 +897,22 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.scrollNode.addSubnode(self.paneContainerNode) self.addSubnode(self.headerNode) - self.paneContainerNode.openMessage = { [weak self] id in - return self?.openMessage(id: id) ?? false - } - - self.paneContainerNode.toggleMessageSelected = { [weak self] id in - guard let strongSelf = self else { + self.paneContainerNode.chatControllerInteraction = self.chatInterfaceInteraction + self.paneContainerNode.openPeerContextAction = { [weak self] peer, node, gesture in + guard let strongSelf = self, let controller = strongSelf.controller else { return } - if var selectedMessageIds = strongSelf.state.selectedMessageIds { - if selectedMessageIds.contains(id) { - selectedMessageIds.remove(id) - } else { - selectedMessageIds.insert(id) - } - strongSelf.state = strongSelf.state.withSelectedMessageIds(selectedMessageIds) - strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) - } - } - } - - self.paneContainerNode.openPeer = { [weak self] peer 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(peer.id), keepStack: .always)) - } + let presentationData = strongSelf.presentationData + let chatController = strongSelf.context.sharedContext.makeChatController(context: context, chatLocation: .peer(peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true)) + chatController.canReadHistory.set(false) + let items: [ContextMenuItem] = [ + .action(ContextMenuActionItem(text: presentationData.strings.Conversation_LinkDialogOpen, icon: { _ in nil }, action: { _, f in + f(.dismissWithoutContent) + self?.chatInterfaceInteraction.openPeer(peer.id, .default, nil) + })) + ] + let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) + controller.presentInGlobalOverlay(contextController) } self.paneContainerNode.currentPaneUpdated = { [weak self] in @@ -2996,19 +929,25 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self?.performButtonAction(key: key) } - self.headerNode.requestAvatarExpansion = { [weak self] in + self.headerNode.requestAvatarExpansion = { [weak self] entries, transitionNode in guard let strongSelf = self, let peer = strongSelf.data?.peer, peer.smallProfileImage != nil 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) - } + let entriesPromise = Promise<[AvatarGalleryEntry]>(entries) + let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, remoteEntries: entriesPromise, replaceRootController: { controller, ready in + }) + strongSelf.hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in + if entry == entries.first { + self?.headerNode.updateAvatarIsHidden(true) + } else { + self?.headerNode.updateAvatarIsHidden(false) + } + })) + strongSelf.controller?.present(galleryController, in: .window(.root), with: AvatarGalleryControllerPresentationArguments(transitionArguments: { _ in + return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { _ in + }) + })) } self.headerNode.navigationButtonContainer.performAction = { [weak self] key in @@ -3019,7 +958,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD case .edit: strongSelf.state = strongSelf.state.withIsEditing(true) if strongSelf.headerNode.isAvatarExpanded { - strongSelf.headerNode.updateIsAvatarExpanded(false) + strongSelf.headerNode.updateIsAvatarExpanded(false, transition: .immediate) strongSelf.updateNavigationExpansionPresentation(isExpanded: false, animated: true) } if let (layout, navigationHeight) = strongSelf.validLayout { @@ -3092,19 +1031,21 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if let (layout, navigationHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) } + strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) case .selectionDone: strongSelf.state = strongSelf.state.withSelectedMessageIds(nil) if let (layout, navigationHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false) } + strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) } strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true) case .search: strongSelf.activateSearch() } } - self.dataDisposable = (peerInfoScreenData(context: context, peerId: peerId) + self.dataDisposable = (peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat) |> deliverOnMainQueue).start(next: { [weak self] data in guard let strongSelf = self else { return @@ -3115,7 +1056,10 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD deinit { self.dataDisposable?.dispose() + self.hiddenMediaDisposable?.dispose() self.activeActionDisposable.dispose() + self.resolveUrlDisposable.dispose() + self.hiddenAvatarRepresentationDisposable.dispose() } override func didLoad() { @@ -3130,7 +1074,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } func scrollToTop() { - self.scrollNode.view.setContentOffset(CGPoint(), animated: true) + if !self.paneContainerNode.scrollToTop() { + self.scrollNode.view.setContentOffset(CGPoint(), animated: true) + } } @objc private func editingCancelPressed() { @@ -3138,7 +1084,25 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } 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 { + guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return false + } + var foundGalleryMessage: Message? + if let searchContentNode = self.searchDisplayController?.contentNode as? ChatHistorySearchContainerNode { + if let galleryMessage = searchContentNode.messageForGallery(id) { + let _ = (self.context.account.postbox.transaction { transaction -> Void in + if transaction.getMessage(galleryMessage.id) == nil { + storeMessageFromSearch(transaction: transaction, message: galleryMessage) + } + }).start() + foundGalleryMessage = galleryMessage + } + } + if foundGalleryMessage == nil, let galleryMessage = self.paneContainerNode.findLoadedMessage(id: id) { + foundGalleryMessage = galleryMessage + } + + guard let galleryMessage = foundGalleryMessage else { return false } self.view.endEditing(true) @@ -3156,17 +1120,73 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD 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) + strongSelf.paneContainerNode.currentPane?.node.addToTransitionSurface(view: view) + }, openUrl: { [weak self] url in + self?.openUrl(url: url, external: false) + }, openPeer: { [weak self] peer, navigation in + self?.openPeer(peerId: peer.id, navigation: navigation) }, callPeer: { peerId in //self?.controllerInteraction?.callPeer(peerId) }, enqueueMessage: { _ in }, sendSticker: nil, setupTemporaryHiddenMedia: { _, _, _ in }, chatAvatarHiddenMedia: { _, _ in })) } + private func openUrl(url: String, external: Bool) { + let disposable = self.resolveUrlDisposable + + let resolvedUrl: Signal + if external { + resolvedUrl = .single(.externalUrl(url)) + } else { + resolvedUrl = self.context.sharedContext.resolveUrl(account: self.context.account, url: url) + } + + disposable.set((resolvedUrl + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, openPeer: { peerId, navigation in + self?.openPeer(peerId: peerId, navigation: navigation) + }, sendFile: nil, + sendSticker: nil, + present: { c, a in + self?.controller?.present(c, in: .window(.root), with: a) + }, dismissInput: { + self?.view.endEditing(true) + }, contentContext: nil) + })) + } + + private func openPeer(peerId: PeerId, navigation: ChatControllerInteractionNavigateToPeer) { + switch navigation { + case .default: + if let navigationController = self.controller?.navigationController as? NavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peerId), keepStack: .always)) + } + case let .chat(_, subject): + if let navigationController = self.controller?.navigationController as? NavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peerId), subject: subject, keepStack: .always)) + } + case .info: + self.resolveUrlDisposable.set((self.context.account.postbox.loadedPeerWithId(peerId) + |> 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, avatarInitiallyExpanded: false) { + (strongSelf.controller?.navigationController as? NavigationController)?.pushViewController(infoController) + } + } + })) + case let .withBotStartPayload(startPayload): + if let navigationController = self.controller?.navigationController as? NavigationController { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peerId), botStart: startPayload)) + } + default: + break + } + } + private func performButtonAction(key: PeerInfoHeaderButtonKey) { guard let controller = self.controller else { return @@ -3215,6 +1235,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD strongSelf.controller?.present(muteSettingsController, in: .window(.root)) }) case .more: + guard let data = self.data, let peer = data.peer else { + return + } let actionSheet = ActionSheetController(presentationData: self.presentationData) let dismissAction: () -> Void = { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -3222,24 +1245,14 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD var reportSpam = false var deleteChat = false var items: [ActionSheetItem] = [] - if self.headerNode.isAvatarExpanded { - items.append(ActionSheetButtonItem(title: "Message", color: .accent, action: { [weak self] in - dismissAction() - self?.performButtonAction(key: .message) - })) - items.append(ActionSheetButtonItem(title: "Call", color: .accent, action: { [weak self] in - dismissAction() - self?.performButtonAction(key: .call) - })) - items.append(ActionSheetButtonItem(title: "Mute", color: .accent, action: { [weak self] in - dismissAction() - self?.performButtonAction(key: .mute) - })) + if let user = peer as? TelegramUser { + if user.botInfo == nil && !user.flags.contains(.isSupport) { + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_StartSecretChat, color: .accent, action: { [weak self] in + dismissAction() + self?.openStartSecretChat() + })) + } } - items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_StartSecretChat, color: .accent, action: { [weak self] in - dismissAction() - self?.openStartSecretChat() - })) actionSheet.setItemGroups([ ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, action: { dismissAction() })]) @@ -3297,9 +1310,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD cancelImpl?() })) strongSelf.controller?.present(statusController, in: .window(.root)) - return ActionDisposable { [weak controller] in + return ActionDisposable { [weak statusController] in Queue.mainQueue().async() { - controller?.dismiss() + statusController?.dismiss() } } } else { @@ -3327,7 +1340,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD return } if let navigationController = (strongSelf.controller?.navigationController as? NavigationController) { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId))) } }, error: { _ in guard let strongSelf = self else { @@ -3636,8 +1649,69 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD }) } - func deleteMessages() { - if let messageIds = self.state.selectedMessageIds, !messageIds.isEmpty { + private func openReport() { + guard let controller = self.controller else { + return + } + controller.present(peerReportOptionsController(context: self.context, subject: .peer(self.peerId), present: { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + }, push: { [weak controller] c in + controller?.push(c) + }, completion: { _ in }), in: .window(.root)) + } + + private func openShareBot() { + let _ = (getUserPeer(postbox: self.context.account.postbox, peerId: self.peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer, _ in + guard let strongSelf = self else { + return + } + if let peer = peer as? TelegramUser, let username = peer.username { + let shareController = ShareController(context: strongSelf.context, subject: .url("https://t.me/\(username)")) + strongSelf.controller?.present(shareController, in: .window(.root)) + } + }) + } + + private func openAddBotToGroup() { + guard let controller = self.controller else { + return + } + context.sharedContext.openResolvedUrl(.groupBotStart(peerId: peerId, payload: ""), context: self.context, urlContext: .generic, navigationController: controller.navigationController as? NavigationController, openPeer: { id, navigation in + }, sendFile: nil, + sendSticker: nil, + present: { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + }, dismissInput: { [weak controller] in + controller?.view.endEditing(true) + }, contentContext: nil) + } + + private func performBotCommand(command: PeerInfoBotCommand) { + let _ = (self.context.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self else { + return + } + let text: String + switch command { + case .settings: + text = "/settings" + case .privacy: + text = "/privacy" + case .help: + text = "/help" + } + let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]).start() + + if let navigationController = strongSelf.controller?.navigationController as? NavigationController { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(strongSelf.peerId))) + } + }) + } + + func deleteMessages(messageIds: Set?) { + if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty { self.activeActionDisposable.set((self.context.sharedContext.chatAvailableMessageActions(postbox: self.context.account.postbox, accountPeerId: self.context.account.peerId, messageIds: messageIds) |> deliverOnMainQueue).start(next: { [weak self] actions in if let strongSelf = self, let peer = strongSelf.data?.peer, !actions.options.isEmpty { @@ -3696,8 +1770,8 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } - func forwardMessages() { - if let messageIds = self.state.selectedMessageIds, !messageIds.isEmpty { + func forwardMessages(messageIds: Set?) { + if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty { let peerSelectionController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.onlyWriteable, .excludeDisabled])) peerSelectionController.peerSelected = { [weak self, weak peerSelectionController] peerId in if let strongSelf = self, let _ = peerSelectionController { @@ -3770,15 +1844,6 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD return } - var maybePlaceholderNode: SearchBarPlaceholderNode? - /*if let listNode = historyNode as? ListView { - listNode.forEachItemNode { node in - if let node = node as? ChatListSearchItemNode { - maybePlaceholderNode = node.searchBarNode - } - } - }*/ - if let _ = self.searchDisplayController { return } @@ -3795,7 +1860,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, contentNode: ChatHistorySearchContainerNode(context: self.context, peerId: self.peerId, tagMask: tagMask, interfaceInteraction: self.chatInterfaceInteraction), cancel: { [weak self] in + self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, contentNode: ChatHistorySearchContainerNode(context: self.context, peerId: self.peerId, tagMask: tagMask, interfaceInteraction: self.chatInterfaceInteraction), cancel: { [weak self] in self?.deactivateSearch() }) let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) @@ -3828,6 +1893,18 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD } } + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor + + self.updateNavigationExpansionPresentation(isExpanded: self.headerNode.isAvatarExpanded, animated: false) + + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + } + } + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition, additive: Bool = false) { self.validLayout = (layout, navigationHeight) @@ -3846,7 +1923,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD var contentHeight: CGFloat = 0.0 - let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, 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, isContact: self.data?.isContact ?? false, state: self.state, transition: transition, additive: additive) + let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, 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, statusData: self.data?.status, isContact: self.data?.isContact ?? false, state: self.state, 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) @@ -3930,7 +2007,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD guard let strongSelf = self else { return } - strongSelf.deleteMessages() + strongSelf.deleteMessages(messageIds: nil) }, shareMessages: { [weak self] in guard let strongSelf = self, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty else { return @@ -3958,7 +2035,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD guard let strongSelf = self else { return } - strongSelf.forwardMessages() + strongSelf.forwardMessages(messageIds: nil) }, reportMessages: { [weak self] in guard let strongSelf = self, let messageIds = strongSelf.state.selectedMessageIds, !messageIds.isEmpty else { return @@ -3973,7 +2050,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.paneContainerNode.addSubnode(selectionPanelNode) } selectionPanelNode.selectionPanel.selectedMessages = selectedMessageIds - let panelHeight = selectionPanelNode.update(width: layout.size.width, safeInset: layout.safeInsets.left, metrics: layout.metrics, presentationData: self.presentationData, transition: wasAdded ? .immediate : transition) + let panelHeight = selectionPanelNode.update(layout: layout, presentationData: self.presentationData, transition: wasAdded ? .immediate : transition) let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: paneContainerSize.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight)) if wasAdded { selectionPanelNode.frame = panelFrame @@ -4022,16 +2099,20 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let offsetY = self.scrollNode.view.contentOffset.y if self.state.isEditing || offsetY <= 50.0 { - self.scrollNode.view.bounces = true - self.scrollNode.view.alwaysBounceVertical = true + if !self.scrollNode.view.bounces { + self.scrollNode.view.bounces = true + self.scrollNode.view.alwaysBounceVertical = true + } } else { - self.scrollNode.view.bounces = false - self.scrollNode.view.alwaysBounceVertical = false + if self.scrollNode.view.bounces { + self.scrollNode.view.bounces = false + self.scrollNode.view.alwaysBounceVertical = false + } } if let (layout, navigationHeight) = self.validLayout { if !additive { - self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, 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, isContact: self.data?.isContact ?? false, state: self.state, transition: transition, additive: additive) + self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, contentOffset: offsetY, presentationData: self.presentationData, peer: self.data?.peer, cachedData: self.data?.cachedData, notificationSettings: self.data?.notificationSettings, statusData: self.data?.status, isContact: self.data?.isContact ?? false, state: self.state, transition: transition, additive: additive) } let paneAreaExpansionDistance: CGFloat = 32.0 @@ -4051,7 +2132,12 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD let visibleHeight = self.scrollNode.view.contentOffset.y + self.scrollNode.view.bounds.height - self.paneContainerNode.frame.minY - self.paneContainerNode.update(size: self.paneContainerNode.bounds.size, visibleHeight: visibleHeight, expansionFraction: paneAreaExpansionFraction, presentationData: self.presentationData, data: self.data, transition: transition) + var bottomInset = layout.intrinsicInsets.bottom + if let selectionPanelNode = self.paneContainerNode.selectionPanelNode { + bottomInset = max(bottomInset, selectionPanelNode.bounds.height) + } + + self.paneContainerNode.update(size: self.paneContainerNode.bounds.size, sideInset: layout.safeInsets.left, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: paneAreaExpansionFraction, presentationData: self.presentationData, data: self.data, transition: transition) self.headerNode.navigationButtonContainer.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: layout.statusBarHeight ?? 0.0), size: CGSize(width: layout.size.width - layout.safeInsets.left * 2.0, height: 44.0)) self.headerNode.navigationButtonContainer.isWhite = self.headerNode.isAvatarExpanded @@ -4059,7 +2145,9 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD if self.state.isEditing { navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .done, isForExpandedView: false)) } else { - navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) + if peerInfoCanEdit(peer: self.data?.peer, cachedData: self.data?.cachedData) { + navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) + } if self.state.selectedMessageIds == nil { if let currentPaneKey = self.paneContainerNode.currentPaneKey { switch currentPaneKey { @@ -4068,8 +2156,13 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD default: break } + switch currentPaneKey { + case .media, .files, .music, .links, .voice: + navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .select, isForExpandedView: true)) + default: + break + } } - navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .select, isForExpandedView: true)) } else { navigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .selectionDone, isForExpandedView: true)) } @@ -4123,7 +2216,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD self.hapticFeedback?.tap() } - self.headerNode.updateIsAvatarExpanded(shouldBeExpanded) + self.headerNode.updateIsAvatarExpanded(shouldBeExpanded, transition: transition) self.updateNavigationExpansionPresentation(isExpanded: shouldBeExpanded, animated: true) if let (layout, navigationHeight) = self.validLayout { @@ -4230,6 +2323,7 @@ public final class PeerInfoScreen: ViewController { private let avatarInitiallyExpanded: Bool private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? fileprivate var controllerNode: PeerInfoScreenNode { return self.displayNode as! PeerInfoScreenNode @@ -4269,6 +2363,9 @@ public final class PeerInfoScreen: ViewController { if strongSelf.controllerNode.scrollNode.view.contentOffset.y > .ulpOfOne { return nil } + if strongSelf.controllerNode.headerNode.isAvatarExpanded { + return nil + } if let tag = other.userInfo as? PeerInfoNavigationSourceTag, tag.peerId == peerId { return PeerInfoNavigationTransitionNode(screenNode: strongSelf.controllerNode, presentationData: strongSelf.presentationData, headerNode: strongSelf.controllerNode.headerNode) } @@ -4280,12 +2377,30 @@ public final class PeerInfoScreen: ViewController { self.scrollToTop = { [weak self] in self?.controllerNode.scrollToTop() } + + self.presentationDataDisposable = (context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.controllerNode.updatePresentationData(strongSelf.presentationData) + } + } + }) } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + self.presentationDataDisposable?.dispose() + } + override public func loadDisplayNode() { self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded) @@ -4457,7 +2572,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig 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, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, 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, isContact: self.screenNode.data?.isContact ?? false, state: self.screenNode.state, transition: transition, additive: false) + self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: layout.safeInsets.left, 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, statusData: self.screenNode.data?.status, isContact: self.screenNode.data?.isContact ?? false, state: self.screenNode.state, transition: transition, additive: false) } let titleScale = (fraction * previousTitleNode.bounds.height + (1.0 - fraction) * self.headerNode.titleNode.bounds.height) / previousTitleNode.bounds.height @@ -4499,3 +2614,32 @@ private func encodeText(_ string: String, _ key: Int) -> String { } return result } + +private final class ContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceNode: ASDisplayNode? + + let navigationController: NavigationController? = nil + + let passthroughTouches: Bool = false + + init(controller: ViewController, sourceNode: ASDisplayNode?) { + self.controller = controller + self.sourceNode = sourceNode + } + + func transitionInfo() -> ContextControllerTakeControllerInfo? { + let sourceNode = self.sourceNode + return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in + if let sourceNode = sourceNode { + return (sourceNode, sourceNode.bounds) + } else { + return nil + } + }) + } + + func animatedIn() { + self.controller.didAppearInContextPreview() + } +} diff --git a/submodules/TelegramUI/TelegramUI/PeerInfoFilesPane.swift b/submodules/TelegramUI/TelegramUI/PeerInfoFilesPane.swift deleted file mode 100644 index 1a2b77ee30..0000000000 --- a/submodules/TelegramUI/TelegramUI/PeerInfoFilesPane.swift +++ /dev/null @@ -1,217 +0,0 @@ -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 paneInteraction: PeerInfoPaneInteraction - private let controllerInteraction: ChatControllerInteraction - - 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 let selectedMessagesPromise = Promise?>(nil) - private var selectedMessages: Set? { - didSet { - if self.selectedMessages != oldValue { - self.selectedMessagesPromise.set(.single(self.selectedMessages)) - } - } - } - - private var hiddenMediaDisposable: Disposable? - - init(context: AccountContext, openMessage: @escaping (MessageId) -> Bool, peerId: PeerId, tagMask: MessageTags, interaction: PeerInfoPaneInteraction) { - self.context = context - self.peerId = peerId - self.paneInteraction = interaction - - var openMessageImpl: ((MessageId) -> Bool)? - var toggleMessageSelectionImpl: (([MessageId]) -> Void)? - self.controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in - return openMessageImpl?(message.id) ?? false - }, openPeer: { _, _, _ in - }, openPeerMention: { _ in - }, openMessageContextMenu: { _, _, _, _, _ in - }, openMessageContextActions: { _, _, _, _ in - }, navigateToMessage: { _, _ in - }, tapMessage: nil, clickThroughMessage: { - }, toggleMessagesSelection: { ids, _ in - toggleMessageSelectionImpl?(ids) - }, 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.controllerInteraction.selectionState = self.paneInteraction.selectedMessageIds.flatMap { ids in - return ChatInterfaceSelectionState(selectedIds: ids) - } - self.selectedMessages = self.paneInteraction.selectedMessageIds - self.selectedMessagesPromise.set(.single(self.selectedMessages)) - - self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), tagMask: tagMask, subject: nil, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false)) - - super.init() - - openMessageImpl = { id in - return openMessage(id) - } - - toggleMessageSelectionImpl = { [weak self] ids in - for id in ids { - self?.paneInteraction.toggleMessageSelected(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] - } - } - strongSelf.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, visibleHeight: CGFloat, 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 transferVelocity(_ velocity: CGFloat) { - if velocity > 0.0 { - self.listNode.transferVelocity(velocity) - } - } - - 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 - } - - func updateSelectedMessages(animated: Bool) { - self.controllerInteraction.selectionState = self.paneInteraction.selectedMessageIds.flatMap { ids in - return ChatInterfaceSelectionState(selectedIds: ids) - } - self.listNode.forEachItemNode { itemNode in - if let itemNode = itemNode as? ChatMessageItemView { - itemNode.updateSelectionState(animated: animated) - } - } - self.selectedMessages = self.paneInteraction.selectedMessageIds - } -}