import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import TelegramPresentationData import Postbox import TelegramCore import AccountContext import ContextUI import ChatControllerInteraction import PeerInfoVisualMediaPaneNode import PeerInfoPaneNode import PeerInfoChatListPaneNode import PeerInfoChatPaneNode import TextFormat import EmojiTextAttachmentView import ComponentFlow import ComponentDisplayAdapters import TabSelectorComponent import MultilineTextComponent import BottomButtonPanelComponent import UndoUI import HorizontalTabsComponent import GlassBackgroundComponent import EdgeEffect final class PeerInfoPaneWrapper { let key: PeerInfoPaneKey let node: PeerInfoPaneNode var isAnimatingOut: Bool = false private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, DeviceMetrics, CGFloat, Bool, CGFloat, CGFloat, PresentationData)? init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) { self.key = key self.node = node } func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { if let (currentSize, currentTopInset, currentSideInset, currentBottomInset, _, currentVisibleHeight, currentIsScrollingLockedAtTop, currentExpandProgress, currentNavigationHeight, currentPresentationData) = self.appliedParams { if currentSize == size && currentTopInset == topInset, currentSideInset == sideInset && currentBottomInset == bottomInset && currentVisibleHeight == visibleHeight && currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentNavigationHeight == navigationHeight && currentPresentationData === presentationData { return } } self.appliedParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) self.node.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: transition) } } private final class GiftsTabItemComponent: Component { let context: AccountContext let icons: [ProfileGiftsContext.State.StarGift] let title: String let theme: PresentationTheme init( context: AccountContext, icons: [ProfileGiftsContext.State.StarGift], title: String, theme: PresentationTheme ) { self.context = context self.icons = icons self.title = title self.theme = theme } static func ==(lhs: GiftsTabItemComponent, rhs: GiftsTabItemComponent) -> Bool { if lhs.icons != rhs.icons { return false } if lhs.title != rhs.title { return false } if lhs.theme !== rhs.theme { return false } return true } final class View: UIView { private let title = ComponentView() private let icon = ComponentView() private var iconLayers: [AnyHashable: InlineStickerItemLayer] = [:] private var component: GiftsTabItemComponent? func update(component: GiftsTabItemComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let textSpacing: CGFloat = 2.0 let iconSpacing: CGFloat = 1.0 let normalColor = component.theme.chat.inputPanel.panelControlColor let effectiveColor = normalColor let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.title, font: Font.medium(15.0), textColor: effectiveColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) ) var iconOffset: CGFloat = titleSize.width + textSpacing var iconsWidth: CGFloat = 0.0 if !component.icons.isEmpty { iconsWidth += iconSpacing var validIds = Set() var index = 0 for icon in component.icons { let id: AnyHashable if let reference = icon.reference { id = reference } else { id = index } validIds.insert(id) let iconSize = CGSize(width: 18.0, height: 18.0) let animationLayer: InlineStickerItemLayer if let current = self.iconLayers[id] { animationLayer = current } else { var file: TelegramMediaFile? switch icon.gift { case let .generic(gift): file = gift.file case let .unique(gift): for attribute in gift.attributes { if case let .model(_, fileValue, _, _) = attribute { file = fileValue } } } guard let file else { continue } let emoji = ChatTextInputTextCustomEmojiAttribute( interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file ) animationLayer = InlineStickerItemLayer( context: .account(component.context), userLocation: .other, attemptSynchronousLoad: false, emoji: emoji, file: file, cache: component.context.animationCache, renderer: component.context.animationRenderer, unique: true, placeholderColor: component.theme.list.mediaPlaceholderColor, pointSize: iconSize, loopCount: 1 ) animationLayer.isVisibleForAnimations = true self.iconLayers[id] = animationLayer self.layer.addSublayer(animationLayer) animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) } transition.setFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: iconOffset, y: 0.0), size: iconSize)) iconOffset += iconSize.width + iconSpacing iconsWidth += iconSize.width + iconSpacing index += 1 } var removeIds: [AnyHashable] = [] for (id, layer) in self.iconLayers { if !validIds.contains(id) { removeIds.append(id) layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in layer.removeFromSuperlayer() }) } } for id in removeIds { self.iconLayers.removeValue(forKey: id) } } else { for (_, layer) in self.iconLayers { layer.removeFromSuperlayer() } self.iconLayers.removeAll() } let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false self.addSubview(titleView) } titleView.frame = titleFrame } return CGSize(width: titleSize.width + iconsWidth, height: titleSize.height) } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { private let pressed: () -> Void private let titleNode: ImmediateTextNode private let buttonNode: HighlightTrackingButtonNode private var iconLayers: [AnyHashable: InlineStickerItemLayer] = [:] private var isSelected: Bool = false private var icons: [ProfileGiftsContext.State.StarGift] = [] private var titleWidth: CGFloat? 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) } @objc private func buttonPressed() { self.pressed() } func updateText(context: AccountContext, title: String, icons: [ProfileGiftsContext.State.StarGift] = [], isSelected: Bool, presentationData: PresentationData) { self.isSelected = isSelected self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) self.icons = icons if !icons.isEmpty { var validIds = Set() var index = 0 for icon in icons { let id: AnyHashable if let reference = icon.reference { id = reference } else { id = index } validIds.insert(id) let iconSize = CGSize(width: 18.0, height: 18.0) if let _ = self.iconLayers[id] { } else { var file: TelegramMediaFile? switch icon.gift { case let .generic(gift): file = gift.file case let .unique(gift): for attribute in gift.attributes { if case let .model(_, fileValue, _, _) = attribute { file = fileValue } } } guard let file else { continue } let emoji = ChatTextInputTextCustomEmojiAttribute( interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file ) let animationLayer = InlineStickerItemLayer( context: .account(context), userLocation: .other, attemptSynchronousLoad: false, emoji: emoji, file: file, cache: context.animationCache, renderer: context.animationRenderer, unique: true, placeholderColor: presentationData.theme.list.mediaPlaceholderColor, pointSize: iconSize, loopCount: 1 ) animationLayer.isVisibleForAnimations = true self.iconLayers[id] = animationLayer self.layer.addSublayer(animationLayer) animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) } index += 1 } var removeIds: [AnyHashable] = [] for (id, layer) in self.iconLayers { if !validIds.contains(id) { removeIds.append(id) layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in layer.removeFromSuperlayer() }) } } for id in removeIds { self.iconLayers.removeValue(forKey: id) } } else { for (_, layer) in self.iconLayers { layer.removeFromSuperlayer() } self.iconLayers.removeAll() } self.buttonNode.accessibilityLabel = title self.buttonNode.accessibilityTraits = [.button] if isSelected { self.buttonNode.accessibilityTraits.insert(.selected) } } func updateLayout(height: CGFloat) -> CGFloat { let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) let iconSize = CGSize(width: 18.0, height: 18.0) let spacing: CGFloat = 1.0 self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize) self.titleWidth = titleSize.width var totalWidth = titleSize.width if !self.iconLayers.isEmpty { totalWidth += 2.0 totalWidth += (iconSize.width + spacing) * CGFloat(self.iconLayers.count) totalWidth -= spacing } self.layoutIcons(transition: .animated(duration: 0.3, curve: .spring)) return totalWidth } func layoutIcons(transition: ContainedViewLayoutTransition) { guard let titleWidth = self.titleWidth else { return } let iconSize = CGSize(width: 18.0, height: 18.0) let spacing: CGFloat = 1.0 var origin = CGPoint(x: titleWidth + 2.0, y: 15.0) var index = 0 for icon in self.icons { let id: AnyHashable if let reference = icon.reference { id = reference } else { id = index } if let layer = self.iconLayers[id] { var iconTransition = transition if layer.frame.width.isZero { iconTransition = .immediate } iconTransition.updateFrame(layer: layer, frame: CGRect(origin: origin, size: iconSize)) } origin.x += iconSize.width + spacing index += 1 } } 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 var icons: [ProfileGiftsContext.State.StarGift] } private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t))) } private final class PeerInfoPendingPane { let pane: PeerInfoPaneWrapper private var disposable: Disposable? var isReady: Bool = false init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, chatControllerInteraction: ChatControllerInteraction, data: PeerInfoScreenData, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void, openAddMemberAction: @escaping () -> Void, requestPerformPeerMemberAction: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, initialStoryFolderId: Int64?, initialGiftCollectionId: Int64?, switchToMediaTarget: PeerInfoSwitchToMediaTarget?, key: PeerInfoPaneKey, hasBecomeReady: @escaping (PeerInfoPaneKey) -> Void, parentController: ViewController?, openMediaCalendar: @escaping () -> Void, openAddStory: @escaping () -> Void, paneDidScroll: @escaping () -> Void, expandIfNeeded: @escaping () -> Void, ensureRectVisible: @escaping (UIView, CGRect) -> Void, externalDataUpdated: @escaping (ContainedViewLayoutTransition) -> Void, openShareLink: @escaping (String) -> Void ) { var chatLocationPeerId = peerId var chatLocation = chatLocation var chatLocationContextHolder = chatLocationContextHolder if let sharedMediaFromForumTopic { chatLocationPeerId = sharedMediaFromForumTopic.0 chatLocation = .replyThread(message: ChatReplyThreadMessage( peerId: sharedMediaFromForumTopic.0, threadId: sharedMediaFromForumTopic.1, channelMessageId: nil, isChannelPost: false, isForumPost: true, isMonoforumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false )) chatLocationContextHolder = Atomic(value: nil) } var captureProtected = peerInfoIsCopyProtected(data: data) let paneNode: PeerInfoPaneNode switch key { case .gifts: var canManage = false var canGift = true if let peer = data.peer { if let cachedUserData = data.cachedData as? CachedUserData, cachedUserData.disallowedGifts == .All { canGift = false } if let channel = peer as? TelegramChannel, case .broadcast = channel.info { if channel.hasPermission(.sendSomething) { canManage = true } } } let giftPaneNode = PeerInfoGiftsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, profileGiftsCollections: data.profileGiftsCollectionsContext!, profileGifts: data.profileGiftsContext!, canManage: canManage, canGift: canGift, initialGiftCollectionId: initialGiftCollectionId) giftPaneNode.openShareLink = openShareLink paneNode = giftPaneNode case .stories, .storyArchive, .botPreview: var canManage = false if let peer = data.peer { if peer.id == context.account.peerId { canManage = true } else if let channel = peer as? TelegramChannel { if channel.hasPermission(.editStories) { canManage = true } } } var listContext: StoryListContext? var scope: PeerInfoStoryPaneNode.Scope = .peer(id: peerId, isSaved: false, isArchived: key == .storyArchive) switch key { case .storyArchive: listContext = data.storyArchiveListContext case .botPreview: listContext = data.botPreviewStoryListContext scope = .botPreview(id: peerId) if let peer = data.peer { if let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { canManage = true } } captureProtected = false default: listContext = data.storyListContext } let visualPaneNode = PeerInfoStoryPaneNode(context: context, scope: scope, captureProtected: captureProtected, isProfileEmbedded: true, canManageStories: canManage, navigationController: chatControllerInteraction.navigationController, listContext: listContext, initialStoryFolderId: initialStoryFolderId) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() } visualPaneNode.paneDidScroll = { paneDidScroll() } visualPaneNode.expandIfNeeded = { expandIfNeeded() } visualPaneNode.ensureRectVisible = { sourceView, rect in ensureRectVisible(sourceView, rect) } visualPaneNode.emptyAction = { openAddStory() } case .media: let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: chatLocationPeerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .photoOrVideo, captureProtected: captureProtected, initialFocusMessageIndex: switchToMediaTarget?.kind == .photoVideo ? switchToMediaTarget?.messageIndex : nil) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() } visualPaneNode.paneDidScroll = { paneDidScroll() } case .files: let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: chatLocationPeerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .files, captureProtected: captureProtected, initialFocusMessageIndex: switchToMediaTarget?.kind == .photoVideo ? switchToMediaTarget?.messageIndex : nil) paneNode = visualPaneNode case .links: paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: chatLocationPeerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: .webPage) case .voice: let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: chatLocationPeerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .voiceAndVideoMessages, captureProtected: captureProtected, initialFocusMessageIndex: nil) paneNode = visualPaneNode case .music: let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: chatLocationPeerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .music, captureProtected: captureProtected, initialFocusMessageIndex: nil) paneNode = visualPaneNode case .gifs: let visualPaneNode = PeerInfoGifPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: chatLocationPeerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .gifs) paneNode = visualPaneNode case .groupsInCommon: paneNode = PeerInfoGroupsInCommonPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, groupsInCommonContext: data.groupsInCommon!) case .members: if case let .longList(membersContext) = data.members { paneNode = PeerInfoMembersPaneNode(context: context, peerId: peerId, membersContext: membersContext, addMemberAction: { openAddMemberAction() }, action: { member, action in requestPerformPeerMemberAction(member, action) }) } else { preconditionFailure() } case .similarChannels, .similarBots: paneNode = PeerInfoRecommendedPeersPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction) case .savedMessagesChats: paneNode = PeerInfoChatListPaneNode(context: context, navigationController: chatControllerInteraction.navigationController) case .savedMessages: paneNode = PeerInfoChatPaneNode(context: context, peerId: peerId, navigationController: chatControllerInteraction.navigationController) } paneNode.externalDataUpdated = externalDataUpdated paneNode.parentController = parentController self.pane = PeerInfoPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] _ in self?.isReady = true hasBecomeReady(key) }) } deinit { self.disposable?.dispose() } } final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegate { private let context: AccountContext private let peerId: PeerId private let chatLocation: ChatLocation private let chatLocationContextHolder: Atomic private let isMediaOnly: Bool private let sharedMediaFromForumTopic: (EnginePeer.Id, Int64)? weak var parentController: ViewController? let headerContainer: UIView private let tabsBackgroundContainer: GlassBackgroundContainerView private let tabsBackgroundView: GlassBackgroundView private let tabsContainer = ComponentView() private var didJustReorderTabs = false private var actionPanel: ComponentView? let isReady = Promise() var didSetIsReady = false private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, areTabsHidden: Bool, disableTabSwitching: Bool, navigationHeight: CGFloat)? private(set) var currentPaneKey: PeerInfoPaneKey? var pendingSwitchToPaneKey: PeerInfoPaneKey? var expandOnSwitch = false var currentPane: PeerInfoPaneWrapper? { if let currentPaneKey = self.currentPaneKey { return self.currentPanes[currentPaneKey] } else { return nil } } private let currentPaneStatusPromise = Promise(nil) private let nextPaneStatusPromise = Promise(nil) private let paneTransitionPromise = ValuePromise(nil) var currentPaneStatus: Signal<(PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), NoError> { return combineLatest(queue: Queue.mainQueue(), self.currentPaneStatusPromise.get(), self.nextPaneStatusPromise.get(), self.paneTransitionPromise.get()) } private var currentPanes: [PeerInfoPaneKey: PeerInfoPaneWrapper] = [:] private var pendingPanes: [PeerInfoPaneKey: PeerInfoPendingPane] = [:] private var shouldFadeIn = false private var initialStoryFolderId: Int64? private var initialGiftCollectionId: Int64? private var switchToMediaTarget: PeerInfoSwitchToMediaTarget? private var isDraggingTabs: Bool = false private var transitionFraction: CGFloat = 0.0 var selectionPanelNode: PeerInfoSelectionPanelNode? var chatControllerInteraction: ChatControllerInteraction? var openPeerContextAction: ((Bool, Peer, ASDisplayNode, ContextGesture?) -> Void)? var openAddMemberAction: (() -> Void)? var requestPerformPeerMemberAction: ((PeerInfoMember, PeerMembersListAction) -> Void)? var currentPaneUpdated: ((Bool) -> Void)? var requestExpandTabs: (() -> Bool)? var requestUpdate: ((ContainedViewLayoutTransition) -> Void)? var openMediaCalendar: (() -> Void)? var openAddStory: (() -> Void)? var openShareLink: ((String) -> Void)? var paneDidScroll: (() -> Void)? var ensurePaneRectVisible: ((UIView, CGRect) -> Void)? private var currentAvailablePanes: [PeerInfoPaneKey]? private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let initialPaneKey: PeerInfoPaneKey? init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, chatLocation: ChatLocation, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, chatLocationContextHolder: Atomic, isMediaOnly: Bool, initialPaneKey: PeerInfoPaneKey?, initialStoryFolderId: Int64?, initialGiftCollectionId: Int64?, switchToMediaTarget: PeerInfoSwitchToMediaTarget?) { self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId self.chatLocation = chatLocation self.chatLocationContextHolder = chatLocationContextHolder self.sharedMediaFromForumTopic = sharedMediaFromForumTopic self.isMediaOnly = isMediaOnly self.initialPaneKey = initialPaneKey self.initialStoryFolderId = initialStoryFolderId self.initialGiftCollectionId = initialGiftCollectionId self.switchToMediaTarget = switchToMediaTarget self.tabsBackgroundContainer = GlassBackgroundContainerView() self.tabsBackgroundView = GlassBackgroundView() self.headerContainer = SparseContainerView() super.init() self.tabsBackgroundContainer.contentView.addSubview(self.tabsBackgroundView) self.headerContainer.addSubview(self.tabsBackgroundContainer) } override func didLoad() { super.didLoad() let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in guard let strongSelf = self else { return [] } guard let currentParams = strongSelf.currentParams else { return [] } if currentParams.disableTabSwitching { return [] } guard let currentPaneKey = strongSelf.currentPaneKey, let availablePanes = currentParams.data?.availablePanes, let index = availablePanes.firstIndex(of: currentPaneKey) else { return [] } if let tabsContainerView = strongSelf.tabsContainer.view, tabsContainerView.bounds.contains(strongSelf.view.convert(point, to: tabsContainerView)) { return [] } if case .savedMessagesChats = currentPaneKey { if index == 0 { return .leftCenter } return [.leftCenter, .rightCenter] } if case .members = currentPaneKey { if index == 0 { return .leftCenter } return [.leftCenter, .rightCenter] } if strongSelf.currentPane?.node.navigationContentNode != nil { return [] } if index == 0 { return .left } return [.left, .right] }) panRecognizer.delegate = self.wrappedGestureRecognizerDelegate panRecognizer.delaysTouchesBegan = false panRecognizer.cancelsTouchesInView = true self.view.addGestureRecognizer(panRecognizer) } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return false } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { return false } if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { return true } return false } @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: self.isDraggingTabs = true func cancelContextGestures(view: UIView) { if let gestureRecognizers = view.gestureRecognizers { for gesture in gestureRecognizers { if let gesture = gesture as? ContextGesture { gesture.cancel() } } } for subview in view.subviews { cancelContextGestures(view: subview) } } cancelContextGestures(view: self.view) case .changed: if let (size, sideInset, topInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { let translation = recognizer.translation(in: self.view) var transitionFraction = translation.x / size.width if currentIndex <= 0 { transitionFraction = min(0.0, transitionFraction) } if currentIndex >= availablePanes.count - 1 { transitionFraction = max(0.0, transitionFraction) } self.transitionFraction = transitionFraction // let nextKey = availablePanes[updatedIndex] // print(transitionFraction) self.paneTransitionPromise.set(transitionFraction) self.update(size: size, sideInset: sideInset, topInset: topInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, areTabsHidden: areTabsHidden, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: .immediate) self.currentPaneUpdated?(false) } case .cancelled, .ended: self.isDraggingTabs = false if let (size, sideInset, topInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { let translation = recognizer.translation(in: self.view) let velocity = recognizer.velocity(in: self.view) var directionIsToRight: Bool? if abs(velocity.x) > 10.0 { directionIsToRight = velocity.x < 0.0 } else { if abs(translation.x) > size.width / 2.0 { directionIsToRight = translation.x > size.width / 2.0 } } if let directionIsToRight = directionIsToRight { var updatedIndex = currentIndex if directionIsToRight { updatedIndex = min(updatedIndex + 1, availablePanes.count - 1) } else { updatedIndex = max(updatedIndex - 1, 0) } let switchToKey = availablePanes[updatedIndex] if switchToKey != self.currentPaneKey && self.currentPanes[switchToKey] != nil{ self.currentPaneKey = switchToKey } } self.transitionFraction = 0.0 self.update(size: size, sideInset: sideInset, topInset: topInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, areTabsHidden: areTabsHidden, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: .animated(duration: 0.35, curve: .spring)) self.currentPaneUpdated?(false) self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil)) } default: break } } 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) { for (_, pane) in self.currentPanes { pane.node.updateSelectedMessages(animated: animated) } for (_, pane) in self.pendingPanes { pane.pane.node.updateSelectedMessages(animated: animated) } } func updateSelectedStoryIds(_ selectedStoryIds: Set?, animated: Bool) { for (_, pane) in self.currentPanes { if let paneNode = pane.node as? PeerInfoStoryPaneNode { paneNode.updateSelectedStories(selectedStoryIds: selectedStoryIds, animated: animated) } } for (_, pane) in self.pendingPanes { if let paneNode = pane.pane.node as? PeerInfoStoryPaneNode { paneNode.updateSelectedStories(selectedStoryIds: selectedStoryIds, animated: false) } } } func updatePaneIsReordering(isReordering: Bool, animated: Bool) { for (_, pane) in self.currentPanes { if let paneNode = pane.node as? PeerInfoStoryPaneNode { paneNode.updateIsReordering(isReordering: isReordering, animated: animated) } else if let paneNode = pane.node as? PeerInfoGiftsPaneNode { paneNode.updateIsReordering(isReordering: isReordering, animated: animated) } } for (_, pane) in self.pendingPanes { if let paneNode = pane.pane.node as? PeerInfoStoryPaneNode { paneNode.updateIsReordering(isReordering: isReordering, animated: false) } else if let paneNode = pane.pane.node as? PeerInfoGiftsPaneNode { paneNode.updateIsReordering(isReordering: isReordering, animated: animated) } } } func openTabContextMenu(key: PeerInfoPaneKey, sourceView: UIView, gesture: ContextGesture?) { guard let params = self.currentParams, let sourceView = sourceView as? ContextExtractedContentContainingView else { return } var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Tabs_SetMainTab, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in guard let self else { return } f(.default) guard let tab = key.tab else { return } Queue.mainQueue().after(0.15) { self.didJustReorderTabs = true let _ = (self.context.engine.peers.setMainProfileTab(peerId: self.peerId, tab: tab) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self else { return } let controller = UndoOverlayController(presentationData: params.presentationData, content: .actionSucceeded(title: nil, text: params.presentationData.strings.PeerInfo_Tabs_SetMainTab_Succeed, cancel: nil, destructive: false), action: { _ in return true }) self.parentController?.present(controller, in: .current) }) } }))) let contextController = makeContextController( presentationData: params.presentationData, source: .reference(TabsReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture ) self.parentController?.presentInGlobalOverlay(contextController) } func update(size: CGSize, sideInset: CGFloat, topInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, areTabsHidden: Bool, disableTabSwitching: Bool, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) { let previousAvailablePanes = self.currentAvailablePanes let availablePanes = data?.availablePanes ?? [] self.currentAvailablePanes = data?.availablePanes let previousPaneKeys = Set(self.currentPanes.keys) let previousCurrentPaneKey = self.currentPaneKey var updateCurrentPaneStatus = false if let previousAvailablePanes, !previousAvailablePanes.contains(.stories), availablePanes.contains(.stories) { self.pendingSwitchToPaneKey = .stories } if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) { var nextCandidatePaneKey: PeerInfoPaneKey? if let previousAvailablePanes = previousAvailablePanes, let index = previousAvailablePanes.firstIndex(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 { self.pendingSwitchToPaneKey = nextCandidatePaneKey } else { self.currentPaneKey = nil self.pendingSwitchToPaneKey = nil } } else if self.currentPaneKey == nil { self.pendingSwitchToPaneKey = self.initialPaneKey ?? availablePanes.first } let currentIndex: Int? if let currentPaneKey = self.currentPaneKey { currentIndex = availablePanes.firstIndex(of: currentPaneKey) } else { currentIndex = nil } self.currentParams = (size, sideInset, topInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) let backgroundColor: UIColor if self.currentPaneKey == .gifts { backgroundColor = presentationData.theme.list.blocksBackgroundColor } else { backgroundColor = presentationData.theme.list.blocksBackgroundColor.mixedWith(presentationData.theme.list.plainBackgroundColor, alpha: expansionFraction) } self.backgroundColor = backgroundColor let isScrollingLockedAtTop = expansionFraction < 1.0 - CGFloat.ulpOfOne let tabsHeight: CGFloat = 40.0 let effectiveTabsHeight: CGFloat = areTabsHidden ? 0.0 : (10.0 + tabsHeight + 10.0 + 6.0) let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: -topInset), size: CGSize(width: size.width, height: topInset + size.height)) var visiblePaneIndices: [Int] = [] var requiredPendingKeys: [PeerInfoPaneKey] = [] if let currentIndex = currentIndex { if currentIndex != 0 { visiblePaneIndices.append(currentIndex - 1) } visiblePaneIndices.append(currentIndex) if currentIndex != availablePanes.count - 1 { visiblePaneIndices.append(currentIndex + 1) } for index in visiblePaneIndices { let key = availablePanes[index] if self.currentPanes[key] == nil && self.pendingPanes[key] == nil { requiredPendingKeys.append(key) } } } if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, availablePanes.contains(pendingSwitchToPaneKey) { if self.currentPanes[pendingSwitchToPaneKey] == nil && self.pendingPanes[pendingSwitchToPaneKey] == nil { if !requiredPendingKeys.contains(pendingSwitchToPaneKey) { requiredPendingKeys.append(pendingSwitchToPaneKey) } } } for key in requiredPendingKeys { if self.pendingPanes[key] == nil, let data { var leftScope = false var initialStoryFolderId: Int64? var initialGiftCollectionId: Int64? var switchToMediaTarget: PeerInfoSwitchToMediaTarget? if case .stories = key { if let initialStoryFolderIdValue = self.initialStoryFolderId { self.initialStoryFolderId = nil initialStoryFolderId = initialStoryFolderIdValue } } if case .gifts = key { if let initialGiftCollectionIdValue = self.initialGiftCollectionId { self.initialGiftCollectionId = nil initialGiftCollectionId = initialGiftCollectionIdValue } } if case .media = key { if let switchToMediaTargetValue = self.switchToMediaTarget, case .photoVideo = switchToMediaTargetValue.kind { self.switchToMediaTarget = nil switchToMediaTarget = switchToMediaTargetValue } } if case .files = key { if let switchToMediaTargetValue = self.switchToMediaTarget, case .file = switchToMediaTargetValue.kind { self.switchToMediaTarget = nil switchToMediaTarget = switchToMediaTargetValue } } let pane = PeerInfoPendingPane( context: self.context, updatedPresentationData: self.updatedPresentationData, chatControllerInteraction: self.chatControllerInteraction!, data: data, openPeerContextAction: { [weak self] recommended, peer, node, gesture in self?.openPeerContextAction?(recommended, peer, node, gesture) }, openAddMemberAction: { [weak self] in self?.openAddMemberAction?() }, requestPerformPeerMemberAction: { [weak self] member, action in self?.requestPerformPeerMemberAction?(member, action) }, peerId: self.peerId, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, sharedMediaFromForumTopic: self.sharedMediaFromForumTopic, initialStoryFolderId: initialStoryFolderId, initialGiftCollectionId: initialGiftCollectionId, switchToMediaTarget: switchToMediaTarget, key: key, hasBecomeReady: { [weak self] key in let apply: () -> Void = { guard let strongSelf = self else { return } if let (size, sideInset, topInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) = strongSelf.currentParams { var transition: ContainedViewLayoutTransition = .immediate if strongSelf.pendingSwitchToPaneKey == key && strongSelf.currentPaneKey != nil { transition = .animated(duration: 0.4, curve: .spring) } strongSelf.update(size: size, sideInset: sideInset, topInset: topInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, areTabsHidden: areTabsHidden, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: transition) } } if leftScope { apply() } }, parentController: self.parentController, openMediaCalendar: { [weak self] in self?.openMediaCalendar?() }, openAddStory: { [weak self] in self?.openAddStory?() }, paneDidScroll: { [weak self] in self?.paneDidScroll?() }, expandIfNeeded: { [weak self] in let _ = self?.requestExpandTabs?() }, ensureRectVisible: { [weak self] sourceView, rect in guard let self else { return } self.ensurePaneRectVisible?(self.view, sourceView.convert(rect, to: self.view)) }, externalDataUpdated: { [weak self] transition in guard let self else { return } self.requestUpdate?(transition) }, openShareLink: { [weak self] url in guard let self else { return } self.openShareLink?(url) } ) self.pendingPanes[key] = pane pane.pane.node.frame = paneFrame pane.pane.update(size: paneFrame.size, topInset: topInset + effectiveTabsHeight, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: true, transition: .immediate) let paneNode = pane.pane.node pane.pane.node.tabBarOffsetUpdated = { [weak self, weak paneNode] transition in guard let strongSelf = self, let paneNode = paneNode, let currentPane = strongSelf.currentPane, paneNode === currentPane.node else { return } if let (size, sideInset, topInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) = strongSelf.currentParams { strongSelf.update(size: size, sideInset: sideInset, topInset: topInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, areTabsHidden: areTabsHidden, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: transition) } } leftScope = true } } for (key, pane) in self.pendingPanes { pane.pane.node.frame = paneFrame pane.pane.update(size: paneFrame.size, topInset: effectiveTabsHeight + topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate) if pane.isReady { self.pendingPanes.removeValue(forKey: key) self.currentPanes[key] = pane.pane } } var paneDefaultTransition = transition var previousPaneKey: PeerInfoPaneKey? var paneSwitchAnimationOffset: CGFloat = 0.0 var updatedCurrentIndex = currentIndex if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, let _ = self.currentPanes[pendingSwitchToPaneKey] { self.pendingSwitchToPaneKey = nil previousPaneKey = self.currentPaneKey self.currentPaneKey = pendingSwitchToPaneKey updateCurrentPaneStatus = true updatedCurrentIndex = availablePanes.firstIndex(of: pendingSwitchToPaneKey) if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.firstIndex(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex { if updatedCurrentIndex < previousIndex { paneSwitchAnimationOffset = -size.width } else { paneSwitchAnimationOffset = size.width } } paneDefaultTransition = .immediate } if self.didJustReorderTabs && previousAvailablePanes != availablePanes { self.didJustReorderTabs = false paneDefaultTransition = .immediate } if let _ = data { if let previousAvailablePanes = previousAvailablePanes, previousAvailablePanes.isEmpty, !availablePanes.isEmpty { self.shouldFadeIn = true self.alpha = 0.0 } let currentPaneKeys = Set(self.currentPanes.keys) if previousPaneKeys.isEmpty && !currentPaneKeys.isEmpty && self.shouldFadeIn { self.shouldFadeIn = false self.alpha = 1.0 self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } } for (key, pane) in self.currentPanes { if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex { var paneWasAdded = false if pane.node.supernode == nil { self.insertSubnode(pane.node, at: 0) paneWasAdded = true } let indexOffset = CGFloat(index - updatedCurrentIndex) let paneTransition: ContainedViewLayoutTransition = paneWasAdded ? .immediate : paneDefaultTransition let adjustedFrame = paneFrame.offsetBy(dx: size.width * self.transitionFraction + indexOffset * size.width, dy: 0.0) let paneCompletion: () -> Void = { [weak self, weak pane] in guard let strongSelf = self, let pane = pane else { return } pane.isAnimatingOut = false if let (_, _, _, _, _, _, _, _, data, _, _, _) = strongSelf.currentParams { if let availablePanes = data?.availablePanes, let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey), let paneIndex = availablePanes.firstIndex(of: key), abs(paneIndex - currentIndex) <= 1 { } else { if let pane = strongSelf.currentPanes.removeValue(forKey: key) { pane.node.removeFromSupernode() } } } } if let previousPaneKey = previousPaneKey, key == previousPaneKey { pane.node.frame = adjustedFrame let isAnimatingOut = pane.isAnimatingOut pane.isAnimatingOut = true transition.animateFrame(node: pane.node, from: paneFrame, to: paneFrame.offsetBy(dx: -paneSwitchAnimationOffset, dy: 0.0), completion: isAnimatingOut ? nil : { _ in paneCompletion() }) } else if let _ = previousPaneKey, key == self.currentPaneKey { pane.node.frame = adjustedFrame let isAnimatingOut = pane.isAnimatingOut pane.isAnimatingOut = true transition.animatePositionAdditive(node: pane.node, offset: CGPoint(x: paneSwitchAnimationOffset, y: 0.0), completion: isAnimatingOut ? nil : { paneCompletion() }) } else { let isAnimatingOut = pane.isAnimatingOut pane.isAnimatingOut = true paneTransition.updateFrame(node: pane.node, frame: adjustedFrame, completion: isAnimatingOut ? nil : { _ in paneCompletion() }) } pane.update(size: paneFrame.size, topInset: effectiveTabsHeight + topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) } } var tabsOffset: CGFloat = 0.0 if !"".isEmpty { if let currentPane = self.currentPane { tabsOffset = currentPane.node.tabBarOffset } tabsOffset = max(0.0, min(tabsHeight, tabsOffset)) if isScrollingLockedAtTop || self.isMediaOnly { tabsOffset = 0.0 } } var tabsAlpha: CGFloat if areTabsHidden { tabsAlpha = 0.0 tabsOffset = tabsHeight } else { tabsAlpha = 1.0 - tabsOffset / tabsHeight } tabsAlpha *= tabsAlpha var canManageTabs = false if let peer = data?.peer { if peer.id == self.context.account.peerId { canManageTabs = true } else if let channel = data?.peer as? TelegramChannel, case .broadcast = channel.info { if channel.hasPermission(.changeInfo) { canManageTabs = true } } } let tabsSideInset: CGFloat = sideInset + 16.0 let tabsContainerSize = CGSize(width: size.width - tabsSideInset * 2.0, height: tabsHeight) let tabsContainerEffectiveSize = self.tabsContainer.update( transition: ComponentTransition(transition), component: AnyComponent(HorizontalTabsComponent( context: self.context, theme: presentationData.theme, tabs: availablePanes.map { paneKey -> HorizontalTabsComponent.Tab in var canReorder = false let content: HorizontalTabsComponent.Tab.Content switch paneKey { case .stories: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneStories, entities: [], enableAnimations: false)) canReorder = true case .storyArchive: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneArchivedStories, entities: [], enableAnimations: false)) case .botPreview: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneBotPreviews, entities: [], enableAnimations: false)) case .media: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneMedia, entities: [], enableAnimations: false)) canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .files: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneFiles, entities: [], enableAnimations: false)) canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .links: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneLinks, entities: [], enableAnimations: false)) canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .voice: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneVoiceAndVideo, entities: [], enableAnimations: false)) canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .gifs: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneGifs, entities: [], enableAnimations: false)) canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .music: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneAudio, entities: [], enableAnimations: false)) canReorder = self.peerId.namespace == Namespaces.Peer.CloudChannel case .groupsInCommon: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneGroups, entities: [], enableAnimations: false)) case .members: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneMembers, entities: [], enableAnimations: false)) case .similarChannels: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneRecommended, entities: [], enableAnimations: false)) case .similarBots: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_PaneRecommendedBots, entities: [], enableAnimations: false)) case .savedMessagesChats: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.DialogList_TabTitle, entities: [], enableAnimations: false)) case .savedMessages: content = .title(HorizontalTabsComponent.Tab.Title(text: presentationData.strings.PeerInfo_SavedMessagesTabTitle, entities: [], enableAnimations: false)) case .gifts: var icons: [ProfileGiftsContext.State.StarGift] = [] if let gifts = data?.profileGiftsContext?.currentState?.gifts.prefix(3) { icons = Array(gifts) } content = .custom(AnyComponent( GiftsTabItemComponent(context: self.context, icons: icons, title: presentationData.strings.PeerInfo_PaneGifts, theme: presentationData.theme) )) canReorder = true } return HorizontalTabsComponent.Tab( id: AnyHashable(paneKey), content: content, badge: nil, action: { [weak self] in guard let self else { return } if self.currentPaneKey == paneKey { if let requestExpandTabs = self.requestExpandTabs, requestExpandTabs() { } else { let _ = self.currentPane?.node.scrollToTop() } return } if self.currentPanes[paneKey] != nil { self.currentPaneKey = paneKey if let (size, sideInset, topInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) = self.currentParams { self.update(size: size, sideInset: sideInset, topInset: topInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, areTabsHidden: areTabsHidden, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) self.currentPaneUpdated?(true) self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil)) self.nextPaneStatusPromise.set(.single(nil)) self.paneTransitionPromise.set(nil) } } else if self.pendingSwitchToPaneKey != paneKey { self.pendingSwitchToPaneKey = paneKey self.expandOnSwitch = true if let (size, sideInset, topInset, bottomInset, deviceMetrics, visibleHeight, expansionFraction, presentationData, data, areTabsHidden, disableTabSwitching, navigationHeight) = self.currentParams { self.update(size: size, sideInset: sideInset, topInset: topInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, areTabsHidden: areTabsHidden, disableTabSwitching: disableTabSwitching, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) } } }, contextAction: paneKey != availablePanes.first && canManageTabs && canReorder ? { [weak self] sourceView, gesture in guard let self else { return } self.openTabContextMenu(key: paneKey, sourceView: sourceView, gesture: gesture) } : nil, deleteAction: nil ) }, selectedTab: self.currentPaneKey.flatMap { HorizontalTabsComponent.Tab.Id($0) }, isEditing: false, layout: .fit, liftWhileSwitching: deviceMetrics.type != .tablet )), environment: {}, containerSize: tabsContainerSize ) let tabContainerFrameOriginX = floorToScreenPixels((size.width - tabsContainerEffectiveSize.width) / 2.0) let tabContainerFrame = CGRect(origin: CGPoint(x: tabContainerFrameOriginX, y: 10.0), size: tabsContainerEffectiveSize) transition.updateFrame(view: self.tabsBackgroundContainer, frame: tabContainerFrame) self.tabsBackgroundContainer.update(size: tabContainerFrame.size, isDark: presentationData.theme.overallDarkAppearance, transition: ComponentTransition(transition)) transition.updateFrame(view: self.tabsBackgroundView, frame: CGRect(origin: CGPoint(), size: tabContainerFrame.size)) self.tabsBackgroundView.update(size: tabContainerFrame.size, cornerRadius: tabContainerFrame.height * 0.5, isDark: presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: ComponentTransition(transition)) ComponentTransition(transition).setAlpha(view: self.tabsBackgroundContainer, alpha: tabsAlpha) if let tabsContainerView = self.tabsContainer.view as? HorizontalTabsComponent.View { if tabsContainerView.superview == nil { self.tabsBackgroundView.contentView.addSubview(tabsContainerView) tabsContainerView.setOverlayContainerView(overlayContainerView: self.headerContainer) } transition.updateFrame(view: tabsContainerView, frame: CGRect(origin: CGPoint(), size: tabContainerFrame.size)) tabsContainerView.updateTabSwitchFraction(fraction: self.transitionFraction, isDragging: self.isDraggingTabs, transition: ComponentTransition(transition)) } for (_, pane) in self.pendingPanes { let paneTransition: ContainedViewLayoutTransition = .immediate paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) pane.pane.update(size: paneFrame.size, topInset: effectiveTabsHeight + topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: true, transition: paneTransition) } var removeKeys: [PeerInfoPaneKey] = [] for (key, paneNode) in self.pendingPanes { if !availablePanes.contains(key) && self.pendingSwitchToPaneKey != key { removeKeys.append(key) paneNode.pane.node.removeFromSupernode() } } for key in removeKeys { self.pendingPanes.removeValue(forKey: key) } removeKeys.removeAll() for (key, paneNode) in self.currentPanes { if !availablePanes.contains(key) && self.pendingSwitchToPaneKey != key { removeKeys.append(key) paneNode.node.removeFromSupernode() } } for key in removeKeys { self.currentPanes.removeValue(forKey: key) } if !self.didSetIsReady && data != nil { if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] { self.didSetIsReady = true self.isReady.set(currentPane.node.isReady) } else if self.pendingSwitchToPaneKey == nil { self.didSetIsReady = true self.isReady.set(.single(true)) } } if let previousCurrentPaneKey, self.currentPaneKey != previousCurrentPaneKey { if self.currentPaneKey == nil && previousCurrentPaneKey == .gifts { } else { self.currentPaneUpdated?(self.expandOnSwitch) self.expandOnSwitch = false } } if updateCurrentPaneStatus { self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil)) } } } private final class TabsExtractedContentSource: ContextExtractedContentSource { let keepInPlace: Bool = false let ignoreContentTouches: Bool = false let blurBackground: Bool = true private let sourceNode: ContextExtractedContentContainingNode init(sourceNode: ContextExtractedContentContainingNode) { self.sourceNode = sourceNode } func takeView() -> ContextControllerTakeViewInfo? { return ContextControllerTakeViewInfo(containingItem: .node(self.sourceNode), contentAreaInScreenSpace: UIScreen.main.bounds) } func putBack() -> ContextControllerPutBackViewInfo? { return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) } } private final class TabsReferenceContentSource: ContextReferenceContentSource { let keepInPlace: Bool = true let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center private let sourceView: ContextExtractedContentContainingView init(sourceView: ContextExtractedContentContainingView) { self.sourceView = sourceView } func transitionInfo() -> ContextControllerReferenceViewInfo? { return ContextControllerReferenceViewInfo( referenceView: self.sourceView.contentView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .bottom ) } }