mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
600 lines
27 KiB
Swift
600 lines
27 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import ComponentFlow
|
|
import TelegramCore
|
|
import PeerInfoVisualMediaPaneNode
|
|
import ViewControllerComponent
|
|
import ChatListHeaderComponent
|
|
import ContextUI
|
|
import ChatTitleView
|
|
import BottomButtonPanelComponent
|
|
import UndoUI
|
|
import MoreHeaderButton
|
|
import MediaEditorScreen
|
|
import SaveToCameraRoll
|
|
|
|
final class PeerInfoStoryGridScreenComponent: Component {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let peerId: EnginePeer.Id
|
|
let scope: PeerInfoStoryGridScreen.Scope
|
|
|
|
init(
|
|
context: AccountContext,
|
|
peerId: EnginePeer.Id,
|
|
scope: PeerInfoStoryGridScreen.Scope
|
|
) {
|
|
self.context = context
|
|
self.peerId = peerId
|
|
self.scope = scope
|
|
}
|
|
|
|
static func ==(lhs: PeerInfoStoryGridScreenComponent, rhs: PeerInfoStoryGridScreenComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.peerId != rhs.peerId {
|
|
return false
|
|
}
|
|
if lhs.scope != rhs.scope {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private var component: PeerInfoStoryGridScreenComponent?
|
|
private weak var state: EmptyComponentState?
|
|
private var environment: EnvironmentType?
|
|
|
|
private(set) var paneNode: PeerInfoStoryPaneNode?
|
|
private var paneStatusDisposable: Disposable?
|
|
private(set) var paneStatusText: String?
|
|
|
|
private(set) var selectedCount: Int = 0
|
|
private var selectionStateDisposable: Disposable?
|
|
|
|
private var selectionPanel: ComponentView<Empty>?
|
|
|
|
private weak var mediaGalleryContextMenu: ContextController?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.paneStatusDisposable?.dispose()
|
|
self.selectionStateDisposable?.dispose()
|
|
}
|
|
|
|
func morePressed(source: ContextReferenceContentNode) {
|
|
guard let component = self.component, let controller = self.environment?.controller(), let pane = self.paneNode else {
|
|
return
|
|
}
|
|
|
|
var items: [ContextMenuItem] = []
|
|
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
let strings = presentationData.strings
|
|
|
|
if self.selectedCount != 0 {
|
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.StoryList_ContextSaveToGallery, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor)
|
|
}, action: { [weak self] _, a in
|
|
a(.default)
|
|
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
self.saveSelected()
|
|
})))
|
|
items.append(.action(ContextMenuActionItem(text: strings.Common_Delete, textColor: .destructive, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
|
}, action: { [weak self] _, a in
|
|
a(.default)
|
|
|
|
guard let self, let component = self.component, let environment = self.environment else {
|
|
return
|
|
}
|
|
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
|
|
return
|
|
}
|
|
let _ = component.context.engine.messages.deleteStories(ids: Array(paneNode.selectedIds)).start()
|
|
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
|
let text: String = presentationData.strings.StoryList_TooltipStoriesDeleted(Int32(paneNode.selectedIds.count))
|
|
|
|
environment.controller()?.present(UndoOverlayController(
|
|
presentationData: presentationData,
|
|
content: .info(title: nil, text: text, timeout: nil),
|
|
elevatedLayout: false,
|
|
animateInAsReplacement: false,
|
|
action: { _ in return false }
|
|
), in: .current)
|
|
|
|
paneNode.clearSelection()
|
|
})))
|
|
} else if let paneNode = self.paneNode {
|
|
if !paneNode.isEmpty {
|
|
var recurseGenerateAction: ((Bool) -> ContextMenuActionItem)?
|
|
let generateAction: (Bool) -> ContextMenuActionItem = { [weak pane] isZoomIn in
|
|
let nextZoomLevel = isZoomIn ? pane?.availableZoomLevels().increment : pane?.availableZoomLevels().decrement
|
|
let canZoom: Bool = nextZoomLevel != nil
|
|
|
|
return ContextMenuActionItem(id: isZoomIn ? 0 : 1, text: isZoomIn ? strings.SharedMedia_ZoomIn : strings.SharedMedia_ZoomOut, textColor: canZoom ? .primary : .disabled, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: isZoomIn ? "Chat/Context Menu/ZoomIn" : "Chat/Context Menu/ZoomOut"), color: canZoom ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withMultipliedAlpha(0.4))
|
|
}, action: canZoom ? { action in
|
|
guard let pane = pane, let zoomLevel = isZoomIn ? pane.availableZoomLevels().increment : pane.availableZoomLevels().decrement else {
|
|
return
|
|
}
|
|
pane.updateZoomLevel(level: zoomLevel)
|
|
if let recurseGenerateAction = recurseGenerateAction {
|
|
action.updateAction(0, recurseGenerateAction(true))
|
|
action.updateAction(1, recurseGenerateAction(false))
|
|
}
|
|
} : nil)
|
|
}
|
|
recurseGenerateAction = { isZoomIn in
|
|
return generateAction(isZoomIn)
|
|
}
|
|
|
|
items.append(.action(generateAction(true)))
|
|
items.append(.action(generateAction(false)))
|
|
}
|
|
|
|
if component.peerId == component.context.account.peerId, case .saved = component.scope {
|
|
var ignoreNextActions = false
|
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.StoryList_ContextShowArchive, icon: { theme in
|
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/StoryArchive"), color: theme.contextMenu.primaryColor)
|
|
}, action: { [weak self] _, a in
|
|
if ignoreNextActions {
|
|
return
|
|
}
|
|
ignoreNextActions = true
|
|
a(.default)
|
|
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
|
|
self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive))
|
|
})))
|
|
}
|
|
}
|
|
|
|
let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
|
|
contextController.passthroughTouchEvent = { [weak self] sourceView, point in
|
|
guard let self else {
|
|
return .ignore
|
|
}
|
|
|
|
let localPoint = self.convert(sourceView.convert(point, to: nil), from: nil)
|
|
guard let localResult = self.hitTest(localPoint, with: nil) else {
|
|
return .dismiss(consume: true, result: nil)
|
|
}
|
|
|
|
var testView: UIView? = localResult
|
|
while true {
|
|
if let testViewValue = testView {
|
|
if let node = testViewValue.asyncdisplaykit_node as? PeerInfoStoryPaneNode {
|
|
node.brieflyDisableTouchActions()
|
|
return .dismiss(consume: false, result: nil)
|
|
} else {
|
|
testView = testViewValue.superview
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return .dismiss(consume: true, result: nil)
|
|
}
|
|
self.mediaGalleryContextMenu = contextController
|
|
controller.presentInGlobalOverlay(contextController)
|
|
}
|
|
|
|
private func saveSelected() {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
|
|
let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId))
|
|
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
|
guard let self, let component = self.component, let peer else {
|
|
return
|
|
}
|
|
guard let peerReference = PeerReference(peer._asPeer()) else {
|
|
return
|
|
}
|
|
|
|
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
|
|
return
|
|
}
|
|
|
|
var signals: [Signal<Float, NoError>] = []
|
|
let sortedItems = paneNode.selectedItems.sorted(by: { lhs, rhs in return lhs.key < rhs.key })
|
|
if sortedItems.isEmpty {
|
|
return
|
|
}
|
|
|
|
let strings = (component.context.sharedContext.currentPresentationData.with { $0 }).strings
|
|
let saveScreen = SaveProgressScreen(context: component.context, content: .progress(strings.Story_TooltipSaving, 0.0))
|
|
self.environment?.controller()?.present(saveScreen, in: .current)
|
|
|
|
let valueNorm: Float = 1.0 / Float(sortedItems.count)
|
|
var progressStart: Float = 0.0
|
|
for (_, item) in sortedItems {
|
|
let itemOffset = progressStart
|
|
progressStart += valueNorm
|
|
signals.append(saveToCameraRoll(context: component.context, postbox: component.context.account.postbox, userLocation: .other, mediaReference: .story(peer: peerReference, id: item.id, media: item.media._asMedia()))
|
|
|> map { progress -> Float in
|
|
return itemOffset + progress * valueNorm
|
|
})
|
|
}
|
|
|
|
var allSignal: Signal<Float, NoError> = .single(0.0)
|
|
for signal in signals {
|
|
allSignal = allSignal |> then(signal)
|
|
}
|
|
|
|
let disposable = (allSignal
|
|
|> deliverOnMainQueue).start(next: { [weak saveScreen] progress in
|
|
guard let saveScreen else {
|
|
return
|
|
}
|
|
saveScreen.content = .progress(strings.Story_TooltipSaving, progress)
|
|
}, completed: { [weak saveScreen] in
|
|
guard let saveScreen else {
|
|
return
|
|
}
|
|
saveScreen.content = .completion(strings.Story_TooltipSaved)
|
|
Queue.mainQueue().after(3.0, { [weak saveScreen] in
|
|
saveScreen?.dismiss()
|
|
})
|
|
})
|
|
|
|
saveScreen.cancelled = {
|
|
disposable.dispose()
|
|
}
|
|
})
|
|
}
|
|
|
|
func scrollToTop() {
|
|
guard let paneNode = self.paneNode else {
|
|
return
|
|
}
|
|
let _ = paneNode.scrollToTop()
|
|
}
|
|
|
|
func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let sideInset: CGFloat = 14.0
|
|
|
|
let environment = environment[EnvironmentType.self].value
|
|
|
|
let themeUpdated = self.environment?.theme !== environment.theme
|
|
|
|
self.environment = environment
|
|
|
|
if themeUpdated {
|
|
self.backgroundColor = environment.theme.list.plainBackgroundColor
|
|
}
|
|
|
|
var bottomInset: CGFloat = environment.safeInsets.bottom
|
|
|
|
if self.selectedCount != 0 {
|
|
let selectionPanel: ComponentView<Empty>
|
|
var selectionPanelTransition = transition
|
|
if let current = self.selectionPanel {
|
|
selectionPanel = current
|
|
} else {
|
|
selectionPanelTransition = .immediate
|
|
selectionPanel = ComponentView()
|
|
self.selectionPanel = selectionPanel
|
|
}
|
|
|
|
let selectionPanelSize = selectionPanel.update(
|
|
transition: selectionPanelTransition,
|
|
component: AnyComponent(BottomButtonPanelComponent(
|
|
theme: environment.theme,
|
|
title: environment.strings.StoryList_SaveToProfile,
|
|
label: nil,
|
|
isEnabled: true,
|
|
insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: environment.safeInsets.bottom, right: sideInset),
|
|
action: { [weak self] in
|
|
guard let self, let component = self.component, let environment = self.environment else {
|
|
return
|
|
}
|
|
guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else {
|
|
return
|
|
}
|
|
|
|
let _ = component.context.engine.messages.updateStoriesArePinned(ids: paneNode.selectedItems, isPinned: true).start()
|
|
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
|
|
|
let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(paneNode.selectedIds.count))
|
|
environment.controller()?.present(UndoOverlayController(
|
|
presentationData: presentationData,
|
|
content: .info(title: title, text: presentationData.strings.StoryList_TooltipStoriesSavedToProfileText, timeout: nil),
|
|
elevatedLayout: false,
|
|
animateInAsReplacement: false,
|
|
action: { _ in return false }
|
|
), in: .current)
|
|
|
|
paneNode.clearSelection()
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let selectionPanelView = selectionPanel.view {
|
|
var animateIn = false
|
|
if selectionPanelView.superview == nil {
|
|
self.addSubview(selectionPanelView)
|
|
animateIn = true
|
|
}
|
|
selectionPanelTransition.setFrame(view: selectionPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - selectionPanelSize.height), size: selectionPanelSize))
|
|
if animateIn {
|
|
transition.animatePosition(view: selectionPanelView, from: CGPoint(x: 0.0, y: selectionPanelSize.height), to: CGPoint(), additive: true)
|
|
}
|
|
}
|
|
bottomInset = selectionPanelSize.height
|
|
} else if let selectionPanel = self.selectionPanel {
|
|
self.selectionPanel = nil
|
|
if let selectionPanelView = selectionPanel.view {
|
|
transition.setPosition(view: selectionPanelView, position: CGPoint(x: selectionPanelView.center.x, y: availableSize.height + selectionPanelView.bounds.height * 0.5), completion: { [weak selectionPanelView] _ in
|
|
selectionPanelView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
let paneNode: PeerInfoStoryPaneNode
|
|
if let current = self.paneNode {
|
|
paneNode = current
|
|
} else {
|
|
paneNode = PeerInfoStoryPaneNode(
|
|
context: component.context,
|
|
peerId: component.peerId,
|
|
chatLocation: .peer(id: component.peerId),
|
|
contentType: .photoOrVideo,
|
|
captureProtected: false,
|
|
isSaved: true,
|
|
isArchive: component.scope == .archive,
|
|
navigationController: { [weak self] in
|
|
guard let self else {
|
|
return nil
|
|
}
|
|
return self.environment?.controller()?.navigationController as? NavigationController
|
|
},
|
|
listContext: nil
|
|
)
|
|
self.paneNode = paneNode
|
|
self.addSubview(paneNode.view)
|
|
|
|
paneNode.emptyAction = { [weak self] in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
self.environment?.controller()?.push(PeerInfoStoryGridScreen(context: component.context, peerId: component.peerId, scope: .archive))
|
|
}
|
|
|
|
self.paneStatusDisposable = (paneNode.status
|
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.paneStatusText != status?.text {
|
|
self.paneStatusText = status?.text
|
|
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
|
|
}
|
|
})
|
|
|
|
var applyState = false
|
|
self.selectionStateDisposable = (paneNode.updatedSelectedIds
|
|
|> distinctUntilChanged
|
|
|> deliverOnMainQueue).start(next: { [weak self] selectedIds in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
self.selectedCount = selectedIds.count
|
|
|
|
if applyState {
|
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
|
}
|
|
(self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle()
|
|
})
|
|
applyState = true
|
|
}
|
|
|
|
paneNode.update(
|
|
size: availableSize,
|
|
topInset: environment.navigationHeight,
|
|
sideInset: environment.safeInsets.left,
|
|
bottomInset: bottomInset,
|
|
visibleHeight: availableSize.height,
|
|
isScrollingLockedAtTop: false,
|
|
expandProgress: 1.0,
|
|
presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }),
|
|
synchronous: false,
|
|
transition: transition.containedViewLayoutTransition
|
|
)
|
|
transition.setFrame(view: paneNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
|
public enum Scope {
|
|
case saved
|
|
case archive
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let scope: Scope
|
|
private var isDismissed: Bool = false
|
|
|
|
private var titleView: ChatTitleView?
|
|
|
|
private var moreBarButton: MoreHeaderButton?
|
|
private var moreBarButtonItem: UIBarButtonItem?
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
peerId: EnginePeer.Id,
|
|
scope: Scope
|
|
) {
|
|
self.context = context
|
|
self.scope = scope
|
|
|
|
super.init(context: context, component: PeerInfoStoryGridScreenComponent(
|
|
context: context,
|
|
peerId: peerId,
|
|
scope: scope
|
|
), navigationBarAppearance: .default, theme: .default)
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
|
let moreBarButton = MoreHeaderButton(color: presentationData.theme.rootController.navigationBar.buttonColor)
|
|
moreBarButton.isUserInteractionEnabled = true
|
|
self.moreBarButton = moreBarButton
|
|
|
|
moreBarButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: presentationData.theme.rootController.navigationBar.buttonColor)))
|
|
let moreBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)!
|
|
self.moreBarButtonItem = moreBarButtonItem
|
|
moreBarButton.contextAction = { [weak self] sourceNode, gesture in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let _ = self
|
|
}
|
|
moreBarButton.addTarget(self, action: #selector(self.morePressed), forControlEvents: .touchUpInside)
|
|
|
|
self.titleView = ChatTitleView(
|
|
context: context, theme:
|
|
presentationData.theme,
|
|
strings: presentationData.strings,
|
|
dateTimeFormat: presentationData.dateTimeFormat,
|
|
nameDisplayOrder: presentationData.nameDisplayOrder,
|
|
animationCache: context.animationCache,
|
|
animationRenderer: context.animationRenderer
|
|
)
|
|
self.titleView?.disableAnimations = true
|
|
|
|
self.navigationItem.titleView = self.titleView
|
|
|
|
self.updateTitle()
|
|
|
|
self.scrollToTop = { [weak self] in
|
|
guard let self, let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
|
|
return
|
|
}
|
|
componentView.scrollToTop()
|
|
}
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
func updateTitle() {
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
switch self.scope {
|
|
case .saved:
|
|
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
|
|
return
|
|
}
|
|
let title: String?
|
|
if let paneStatusText = componentView.paneStatusText, !paneStatusText.isEmpty {
|
|
title = paneStatusText
|
|
} else {
|
|
title = nil
|
|
}
|
|
self.titleView?.titleContent = .custom(presentationData.strings.StoryList_TitleSaved, title, false)
|
|
|
|
self.navigationItem.setRightBarButton(self.moreBarButtonItem, animated: false)
|
|
case .archive:
|
|
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
|
|
return
|
|
}
|
|
let title: String
|
|
if componentView.selectedCount != 0 {
|
|
title = presentationData.strings.StoryList_SubtitleSelected(Int32(componentView.selectedCount))
|
|
} else {
|
|
title = presentationData.strings.StoryList_TitleArchive
|
|
}
|
|
self.titleView?.titleContent = .custom(title, nil, false)
|
|
|
|
var hasMenu = false
|
|
if componentView.selectedCount != 0 {
|
|
hasMenu = true
|
|
} else if let paneNode = componentView.paneNode, !paneNode.isEmpty {
|
|
hasMenu = true
|
|
}
|
|
|
|
if hasMenu {
|
|
self.navigationItem.setRightBarButton(self.moreBarButtonItem, animated: false)
|
|
} else {
|
|
self.navigationItem.setRightBarButton(nil, animated: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func morePressed() {
|
|
guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else {
|
|
return
|
|
}
|
|
guard let moreBarButton = self.moreBarButton else {
|
|
return
|
|
}
|
|
componentView.morePressed(source: moreBarButton.referenceNode)
|
|
}
|
|
|
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
super.containerLayoutUpdated(layout, transition: transition)
|
|
|
|
self.titleView?.layout = layout
|
|
}
|
|
}
|
|
|
|
private final class PeerInfoContextReferenceContentSource: ContextReferenceContentSource {
|
|
private let controller: ViewController
|
|
private let sourceNode: ContextReferenceContentNode
|
|
|
|
init(controller: ViewController, sourceNode: ContextReferenceContentNode) {
|
|
self.controller = controller
|
|
self.sourceNode = sourceNode
|
|
}
|
|
|
|
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
|
return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
|
|
}
|
|
}
|