mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-02-14 23:09:38 +00:00
1459 lines
73 KiB
Swift
1459 lines
73 KiB
Swift
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<Empty>()
|
|
private let icon = ComponentView<Empty>()
|
|
private var iconLayers: [AnyHashable: InlineStickerItemLayer] = [:]
|
|
|
|
private var component: GiftsTabItemComponent?
|
|
|
|
func update(component: GiftsTabItemComponent, availableSize: CGSize, state: State, environment: Environment<Empty>, 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<AnyHashable>()
|
|
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<EnvironmentType>, 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<AnyHashable>()
|
|
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<PresentationData, NoError>)?,
|
|
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<ChatLocationContextHolder?>,
|
|
sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?,
|
|
initialStoryFolderId: Int64?,
|
|
initialGiftCollectionId: Int64?,
|
|
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 = data.peer?.isCopyProtectionEnabled ?? false
|
|
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)
|
|
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)
|
|
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)
|
|
paneNode = visualPaneNode
|
|
case .music:
|
|
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: chatLocationPeerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .music, captureProtected: captureProtected)
|
|
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<ChatLocationContextHolder?>
|
|
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<Empty>()
|
|
private var didJustReorderTabs = false
|
|
|
|
private var actionPanel: ComponentView<Empty>?
|
|
|
|
let isReady = Promise<Bool>()
|
|
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<PeerInfoStatusData?>(nil)
|
|
private let nextPaneStatusPromise = Promise<PeerInfoStatusData?>(nil)
|
|
private let paneTransitionPromise = ValuePromise<CGFloat?>(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 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<PresentationData, NoError>)?
|
|
|
|
private let initialPaneKey: PeerInfoPaneKey?
|
|
|
|
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: PeerId, chatLocation: ChatLocation, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, isMediaOnly: Bool, initialPaneKey: PeerInfoPaneKey?, initialStoryFolderId: Int64?, initialGiftCollectionId: Int64?) {
|
|
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.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<MessageId>?, 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<Int32>?, 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<PeerInfoPaneKey>(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?
|
|
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
|
|
}
|
|
}
|
|
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,
|
|
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<PeerInfoPaneKey>(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, color: UIColor(white: presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), 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
|
|
)
|
|
}
|
|
}
|