mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-02-08 04:14:43 +00:00
Storage management improvements
This commit is contained in:
@@ -38,6 +38,7 @@ swift_library(
|
||||
"//submodules/AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode",
|
||||
"//submodules/LegacyComponents",
|
||||
"//submodules/GalleryData",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -151,6 +151,7 @@ private final class FileListItemComponent: Component {
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: (EngineMessage.Id) -> Void
|
||||
let contextAction: (EngineMessage.Id, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@@ -163,7 +164,8 @@ private final class FileListItemComponent: Component {
|
||||
sideInset: CGFloat,
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EngineMessage.Id) -> Void
|
||||
action: @escaping (EngineMessage.Id) -> Void,
|
||||
contextAction: @escaping (EngineMessage.Id, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@@ -176,6 +178,7 @@ private final class FileListItemComponent: Component {
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: FileListItemComponent, rhs: FileListItemComponent) -> Bool {
|
||||
@@ -212,7 +215,10 @@ private final class FileListItemComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: HighlightTrackingButton {
|
||||
final class View: ContextControllerSourceView {
|
||||
private let extractedContainerView: ContextExtractedContentContainingView
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
@@ -227,19 +233,53 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
private var checkLayer: CheckLayer?
|
||||
|
||||
private var isExtractedToContextMenu: Bool = false
|
||||
|
||||
private var highlightBackgroundFrame: CGRect?
|
||||
private var highlightBackgroundLayer: SimpleLayer?
|
||||
|
||||
private var component: FileListItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.extractedContainerView = ContextExtractedContentContainingView()
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
|
||||
self.highligthedChanged = { [weak self] isHighlighted in
|
||||
self.addSubview(self.extractedContainerView)
|
||||
self.targetViewForActivationProgress = self.extractedContainerView.contentView
|
||||
|
||||
self.extractedContainerView.contentView.addSubview(self.containerButton)
|
||||
|
||||
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.containerButton.clipsToBounds = value
|
||||
self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil
|
||||
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
|
||||
}
|
||||
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isExtractedToContextMenu = value
|
||||
|
||||
let mappedTransition: Transition
|
||||
if value {
|
||||
mappedTransition = Transition(transition)
|
||||
} else {
|
||||
mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
self.state?.updated(transition: mappedTransition)
|
||||
}
|
||||
|
||||
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
|
||||
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
|
||||
return
|
||||
}
|
||||
@@ -267,7 +307,15 @@ private final class FileListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
self.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component else {
|
||||
gesture.cancel()
|
||||
return
|
||||
}
|
||||
component.contextAction(component.messageId, self.extractedContainerView, gesture)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -301,6 +349,11 @@ private final class FileListItemComponent: Component {
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
self.isGestureEnabled = component.selectionState == .none
|
||||
|
||||
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
|
||||
|
||||
let spacing: CGFloat = 1.0
|
||||
let height: CGFloat = 52.0
|
||||
@@ -323,7 +376,7 @@ private final class FileListItemComponent: Component {
|
||||
} else {
|
||||
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
|
||||
self.checkLayer = checkLayer
|
||||
self.layer.addSublayer(checkLayer)
|
||||
self.containerButton.layer.addSublayer(checkLayer)
|
||||
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||
checkLayer.setSelected(isSelected, animated: false)
|
||||
checkLayer.setNeedsDisplay()
|
||||
@@ -338,7 +391,8 @@ private final class FileListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let rightInset: CGFloat = 16.0 + component.sideInset
|
||||
let rightInset: CGFloat = contextInset * 2.0 + 16.0 + component.sideInset
|
||||
let verticalInset: CGFloat = 1.0
|
||||
|
||||
if case let .fileExtension(text) = component.icon {
|
||||
let iconView: UIImageView
|
||||
@@ -347,7 +401,7 @@ private final class FileListItemComponent: Component {
|
||||
} else {
|
||||
iconView = UIImageView()
|
||||
self.iconView = iconView
|
||||
self.addSubview(iconView)
|
||||
self.containerButton.addSubview(iconView)
|
||||
}
|
||||
|
||||
let iconText: ComponentView<Empty>
|
||||
@@ -362,7 +416,7 @@ private final class FileListItemComponent: Component {
|
||||
iconView.image = extensionImage(fileExtension: "mp3")
|
||||
}
|
||||
if let image = iconView.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor(( leftInset - iconLeftInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - image.size.width) / 2.0), y: floor((height - verticalInset * 2.0 - image.size.height) / 2.0)), size: image.size)
|
||||
transition.setFrame(view: iconView, frame: iconFrame)
|
||||
|
||||
let iconTextSize = iconText.update(
|
||||
@@ -377,7 +431,7 @@ private final class FileListItemComponent: Component {
|
||||
)
|
||||
if let iconTextView = iconText.view {
|
||||
if iconTextView.superview == nil {
|
||||
self.addSubview(iconTextView)
|
||||
self.containerButton.addSubview(iconTextView)
|
||||
}
|
||||
transition.setFrame(view: iconTextView, frame: CGRect(origin: CGPoint(x: iconFrame.minX + floor((iconFrame.width - iconTextSize.width) / 2.0), y: iconFrame.maxY - iconTextSize.height - 4.0), size: iconTextSize))
|
||||
}
|
||||
@@ -404,7 +458,7 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
iconImageNode = TransformImageNode()
|
||||
self.iconImageNode = iconImageNode
|
||||
self.addSubview(iconImageNode.view)
|
||||
self.containerButton.addSubview(iconImageNode.view)
|
||||
}
|
||||
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
@@ -429,7 +483,7 @@ private final class FileListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - iconSize.height) / 2.0)), size: iconSize)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - verticalInset * 2.0 - iconSize.height) / 2.0)), size: iconSize)
|
||||
transition.setFrame(view: iconImageNode.view, frame: iconFrame)
|
||||
|
||||
let iconImageLayout = iconImageNode.asyncLayout()
|
||||
@@ -454,11 +508,11 @@ private final class FileListItemComponent: Component {
|
||||
} else {
|
||||
semanticStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
|
||||
self.semanticStatusNode = semanticStatusNode
|
||||
self.addSubview(semanticStatusNode.view)
|
||||
self.containerButton.addSubview(semanticStatusNode.view)
|
||||
}
|
||||
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - iconSize.height) / 2.0)), size: iconSize)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - verticalInset * 2.0 - iconSize.height) / 2.0)), size: iconSize)
|
||||
transition.setFrame(view: semanticStatusNode.view, frame: iconFrame)
|
||||
|
||||
semanticStatusNode.backgroundNodeColor = component.theme.list.itemCheckColors.fillColor
|
||||
@@ -507,13 +561,13 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
let contentHeight = titleSize.height + spacing + subtitleSize.height
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - contentHeight) / 2.0)), size: titleSize)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - contentHeight) / 2.0)), size: titleSize)
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + spacing), size: subtitleSize)
|
||||
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
self.containerButton.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||
@@ -522,7 +576,7 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
|
||||
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
|
||||
self.addSubview(previousTitleContents)
|
||||
self.containerButton.addSubview(previousTitleContents)
|
||||
|
||||
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
|
||||
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
|
||||
@@ -534,14 +588,14 @@ private final class FileListItemComponent: Component {
|
||||
if let subtitleView = self.subtitle.view {
|
||||
if subtitleView.superview == nil {
|
||||
subtitleView.isUserInteractionEnabled = false
|
||||
self.addSubview(subtitleView)
|
||||
self.containerButton.addSubview(subtitleView)
|
||||
}
|
||||
transition.setFrame(view: subtitleView, frame: subtitleFrame)
|
||||
}
|
||||
if let labelView = self.label.view {
|
||||
if labelView.superview == nil {
|
||||
labelView.isUserInteractionEnabled = false
|
||||
self.addSubview(labelView)
|
||||
self.containerButton.addSubview(labelView)
|
||||
}
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize))
|
||||
}
|
||||
@@ -554,6 +608,14 @@ private final class FileListItemComponent: Component {
|
||||
|
||||
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
|
||||
|
||||
let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
|
||||
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
|
||||
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
|
||||
self.extractedContainerView.contentRect = resultBounds
|
||||
|
||||
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
@@ -611,18 +673,21 @@ final class StorageFileListPanelComponent: Component {
|
||||
let context: AccountContext
|
||||
let items: Items?
|
||||
let selectionState: StorageUsageScreenComponent.SelectionState?
|
||||
let peerAction: (EngineMessage.Id) -> Void
|
||||
let action: (EngineMessage.Id) -> Void
|
||||
let contextAction: (EngineMessage.Id, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
items: Items?,
|
||||
selectionState: StorageUsageScreenComponent.SelectionState?,
|
||||
peerAction: @escaping (EngineMessage.Id) -> Void
|
||||
action: @escaping (EngineMessage.Id) -> Void,
|
||||
contextAction: @escaping (EngineMessage.Id, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.selectionState = selectionState
|
||||
self.peerAction = peerAction
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageFileListPanelComponent, rhs: StorageFileListPanelComponent) -> Bool {
|
||||
@@ -681,8 +746,14 @@ final class StorageFileListPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollViewImpl: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: UIScrollView
|
||||
private let scrollView: ScrollViewImpl
|
||||
|
||||
private let measureItem = ComponentView<Empty>()
|
||||
private var visibleItems: [EngineMessage.Id: ComponentView<Empty>] = [:]
|
||||
@@ -694,7 +765,7 @@ final class StorageFileListPanelComponent: Component {
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
self.scrollView = ScrollViewImpl()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@@ -726,6 +797,10 @@ final class StorageFileListPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
cancelContextGestures(view: scrollView)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
@@ -898,7 +973,8 @@ final class StorageFileListPanelComponent: Component {
|
||||
sideInset: environment.containerInsets.left,
|
||||
selectionState: itemSelectionState,
|
||||
hasNext: index != items.items.count - 1,
|
||||
action: component.peerAction
|
||||
action: component.action,
|
||||
contextAction: component.contextAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
|
||||
@@ -949,6 +1025,8 @@ final class StorageFileListPanelComponent: Component {
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { _ in
|
||||
},
|
||||
contextAction: { _, _, _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
||||
@@ -261,18 +261,21 @@ final class StorageMediaGridPanelComponent: Component {
|
||||
let context: AccountContext
|
||||
let items: Items?
|
||||
let selectionState: StorageUsageScreenComponent.SelectionState?
|
||||
let peerAction: (EngineMessage.Id) -> Void
|
||||
let action: (EngineMessage.Id) -> Void
|
||||
let contextAction: (EngineMessage.Id, UIView, CGRect, ContextGesture) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
items: Items?,
|
||||
selectionState: StorageUsageScreenComponent.SelectionState?,
|
||||
peerAction: @escaping (EngineMessage.Id) -> Void
|
||||
action: @escaping (EngineMessage.Id) -> Void,
|
||||
contextAction: @escaping (EngineMessage.Id, UIView, CGRect, ContextGesture) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.selectionState = selectionState
|
||||
self.peerAction = peerAction
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageMediaGridPanelComponent, rhs: StorageMediaGridPanelComponent) -> Bool {
|
||||
@@ -360,7 +363,7 @@ final class StorageMediaGridPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
class View: UIView, UIScrollViewDelegate {
|
||||
class View: ContextControllerSourceView, UIScrollViewDelegate {
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
private var visibleLayers: [EngineMessage.Id: MediaGridLayer] = [:]
|
||||
@@ -372,6 +375,8 @@ final class StorageMediaGridPanelComponent: Component {
|
||||
private var environment: StorageUsagePanelEnvironment?
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private weak var currentGestureItemLayer: MediaGridLayer?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
|
||||
@@ -395,12 +400,121 @@ final class StorageMediaGridPanelComponent: Component {
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
|
||||
self.shouldBegin = { [weak self] point in
|
||||
guard let self else {
|
||||
return false
|
||||
}
|
||||
|
||||
var itemLayer: MediaGridLayer?
|
||||
let scrollPoint = self.convert(point, to: self.scrollView)
|
||||
for (_, itemLayerValue) in self.visibleLayers {
|
||||
if itemLayerValue.frame.contains(scrollPoint) {
|
||||
itemLayer = itemLayerValue
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
guard let itemLayer else {
|
||||
return false
|
||||
}
|
||||
|
||||
self.currentGestureItemLayer = itemLayer
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
self.customActivationProgress = { [weak self] progress, update in
|
||||
guard let self, let itemLayer = self.currentGestureItemLayer else {
|
||||
return
|
||||
}
|
||||
|
||||
let targetContentRect = CGRect(origin: CGPoint(), size: itemLayer.bounds.size)
|
||||
|
||||
let scaleSide = itemLayer.bounds.width
|
||||
let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide)
|
||||
let currentScale = 1.0 * (1.0 - progress) + minScale * progress
|
||||
|
||||
let originalCenterOffsetX: CGFloat = itemLayer.bounds.width / 2.0 - targetContentRect.midX
|
||||
let scaledCenterOffsetX: CGFloat = originalCenterOffsetX * currentScale
|
||||
|
||||
let originalCenterOffsetY: CGFloat = itemLayer.bounds.height / 2.0 - targetContentRect.midY
|
||||
let scaledCenterOffsetY: CGFloat = originalCenterOffsetY * currentScale
|
||||
|
||||
let scaleMidX: CGFloat = scaledCenterOffsetX - originalCenterOffsetX
|
||||
let scaleMidY: CGFloat = scaledCenterOffsetY - originalCenterOffsetY
|
||||
|
||||
switch update {
|
||||
case .update:
|
||||
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
|
||||
itemLayer.transform = sublayerTransform
|
||||
case .begin:
|
||||
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
|
||||
itemLayer.transform = sublayerTransform
|
||||
case .ended:
|
||||
let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
|
||||
let previousTransform = itemLayer.transform
|
||||
itemLayer.transform = sublayerTransform
|
||||
|
||||
itemLayer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "transform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
self.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component, let itemLayer = self.currentGestureItemLayer else {
|
||||
return
|
||||
}
|
||||
self.currentGestureItemLayer = nil
|
||||
guard let message = itemLayer.message else {
|
||||
return
|
||||
}
|
||||
let rect = self.convert(itemLayer.frame, from: self.scrollView)
|
||||
|
||||
component.contextAction(message.id, self, rect, gesture)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
var foundItemLayer: MediaGridLayer?
|
||||
for (_, itemLayer) in self.visibleLayers {
|
||||
if let message = itemLayer.message, message.id == messageId {
|
||||
foundItemLayer = itemLayer
|
||||
}
|
||||
}
|
||||
guard let itemLayer = foundItemLayer else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let itemFrame = self.convert(itemLayer.frame, from: self.scrollView)
|
||||
let proxyNode = ASDisplayNode()
|
||||
proxyNode.frame = itemFrame
|
||||
if let contents = itemLayer.contents {
|
||||
if let image = contents as? UIImage {
|
||||
proxyNode.contents = image.cgImage
|
||||
} else {
|
||||
proxyNode.contents = contents
|
||||
}
|
||||
}
|
||||
proxyNode.isHidden = true
|
||||
self.addSubnode(proxyNode)
|
||||
|
||||
let escapeNotification = EscapeNotification {
|
||||
proxyNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
return (proxyNode, proxyNode.bounds, {
|
||||
let view = UIView()
|
||||
view.frame = proxyNode.frame
|
||||
view.layer.contents = proxyNode.layer.contents
|
||||
escapeNotification.keep()
|
||||
return (view, nil)
|
||||
})
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
guard let component = self.component else {
|
||||
@@ -409,7 +523,7 @@ final class StorageMediaGridPanelComponent: Component {
|
||||
let point = recognizer.location(in: self.scrollView)
|
||||
for (id, itemLayer) in self.visibleLayers {
|
||||
if itemLayer.frame.contains(point) {
|
||||
component.peerAction(id)
|
||||
component.action(id)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,19 @@ import AvatarNode
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private final class PeerListItemComponent: Component {
|
||||
enum SelectionState: Equatable {
|
||||
case none
|
||||
@@ -33,6 +46,7 @@ private final class PeerListItemComponent: Component {
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: (EnginePeer) -> Void
|
||||
let contextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@@ -43,7 +57,8 @@ private final class PeerListItemComponent: Component {
|
||||
label: String,
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
action: @escaping (EnginePeer) -> Void,
|
||||
contextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@@ -54,6 +69,7 @@ private final class PeerListItemComponent: Component {
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
|
||||
@@ -84,7 +100,10 @@ private final class PeerListItemComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: HighlightTrackingButton {
|
||||
final class View: ContextControllerSourceView {
|
||||
private let extractedContainerView: ContextExtractedContentContainingView
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
private let separatorLayer: SimpleLayer
|
||||
@@ -92,22 +111,58 @@ private final class PeerListItemComponent: Component {
|
||||
|
||||
private var checkLayer: CheckLayer?
|
||||
|
||||
private var isExtractedToContextMenu: Bool = false
|
||||
|
||||
private var highlightBackgroundFrame: CGRect?
|
||||
private var highlightBackgroundLayer: SimpleLayer?
|
||||
|
||||
private var component: PeerListItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
self.extractedContainerView = ContextExtractedContentContainingView()
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isLayerBacked = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
self.layer.addSublayer(self.avatarNode.layer)
|
||||
|
||||
self.highligthedChanged = { [weak self] isHighlighted in
|
||||
self.addSubview(self.extractedContainerView)
|
||||
self.targetViewForActivationProgress = self.extractedContainerView.contentView
|
||||
|
||||
self.extractedContainerView.contentView.addSubview(self.containerButton)
|
||||
|
||||
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||
|
||||
self.extractedContainerView.isExtractedToContextPreviewUpdated = { [weak self] value in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.containerButton.clipsToBounds = value
|
||||
self.containerButton.backgroundColor = value ? component.theme.list.plainBackgroundColor : nil
|
||||
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
|
||||
}
|
||||
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isExtractedToContextMenu = value
|
||||
|
||||
let mappedTransition: Transition
|
||||
if value {
|
||||
mappedTransition = Transition(transition)
|
||||
} else {
|
||||
mappedTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
self.state?.updated(transition: mappedTransition)
|
||||
}
|
||||
|
||||
self.containerButton.highligthedChanged = { [weak self] isHighlighted in
|
||||
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
|
||||
return
|
||||
}
|
||||
@@ -135,7 +190,15 @@ private final class PeerListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
self.activated = { [weak self] gesture, _ in
|
||||
guard let self, let component = self.component, let peer = component.peer else {
|
||||
gesture.cancel()
|
||||
return
|
||||
}
|
||||
component.contextAction(peer, self.extractedContainerView, gesture)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -169,6 +232,11 @@ private final class PeerListItemComponent: Component {
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
self.isGestureEnabled = component.selectionState == .none
|
||||
|
||||
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
|
||||
|
||||
let height: CGFloat = 52.0
|
||||
var leftInset: CGFloat = 62.0 + component.sideInset
|
||||
@@ -190,7 +258,7 @@ private final class PeerListItemComponent: Component {
|
||||
} else {
|
||||
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
|
||||
self.checkLayer = checkLayer
|
||||
self.layer.addSublayer(checkLayer)
|
||||
self.containerButton.layer.addSublayer(checkLayer)
|
||||
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||
checkLayer.setSelected(isSelected, animated: false)
|
||||
checkLayer.setNeedsDisplay()
|
||||
@@ -205,7 +273,8 @@ private final class PeerListItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let rightInset: CGFloat = 16.0 + component.sideInset
|
||||
let rightInset: CGFloat = contextInset * 2.0 + 16.0 + component.sideInset
|
||||
let verticalInset: CGFloat = 1.0
|
||||
|
||||
let avatarSize: CGFloat = 40.0
|
||||
|
||||
@@ -248,11 +317,11 @@ private final class PeerListItemComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0)
|
||||
)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - titleSize.height) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
self.containerButton.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||
@@ -273,9 +342,9 @@ private final class PeerListItemComponent: Component {
|
||||
if let labelView = self.label.view {
|
||||
if labelView.superview == nil {
|
||||
labelView.isUserInteractionEnabled = false
|
||||
self.addSubview(labelView)
|
||||
self.containerButton.addSubview(labelView)
|
||||
}
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize))
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - verticalInset * 2.0 - labelSize.height) / 2.0)), size: labelSize))
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
@@ -286,6 +355,14 @@ private final class PeerListItemComponent: Component {
|
||||
|
||||
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
|
||||
|
||||
let resultBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height))
|
||||
transition.setFrame(view: self.extractedContainerView, frame: resultBounds)
|
||||
transition.setFrame(view: self.extractedContainerView.contentView, frame: resultBounds)
|
||||
self.extractedContainerView.contentRect = resultBounds
|
||||
|
||||
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
@@ -344,17 +421,20 @@ final class StoragePeerListPanelComponent: Component {
|
||||
let items: Items?
|
||||
let selectionState: StorageUsageScreenComponent.SelectionState?
|
||||
let peerAction: (EnginePeer) -> Void
|
||||
let contextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
items: Items?,
|
||||
selectionState: StorageUsageScreenComponent.SelectionState?,
|
||||
peerAction: @escaping (EnginePeer) -> Void
|
||||
peerAction: @escaping (EnginePeer) -> Void,
|
||||
contextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.selectionState = selectionState
|
||||
self.peerAction = peerAction
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
static func ==(lhs: StoragePeerListPanelComponent, rhs: StoragePeerListPanelComponent) -> Bool {
|
||||
@@ -413,8 +493,14 @@ final class StoragePeerListPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollViewImpl: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class View: UIView, UIScrollViewDelegate {
|
||||
private let scrollView: UIScrollView
|
||||
private let scrollView: ScrollViewImpl
|
||||
|
||||
private let measureItem = ComponentView<Empty>()
|
||||
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
|
||||
@@ -426,7 +512,7 @@ final class StoragePeerListPanelComponent: Component {
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
self.scrollView = ScrollViewImpl()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
@@ -458,6 +544,10 @@ final class StoragePeerListPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
cancelContextGestures(view: scrollView)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
@@ -505,7 +595,8 @@ final class StoragePeerListPanelComponent: Component {
|
||||
label: dataSizeString(item.size, formatting: dataSizeFormatting),
|
||||
selectionState: itemSelectionState,
|
||||
hasNext: index != items.items.count - 1,
|
||||
action: component.peerAction
|
||||
action: component.peerAction,
|
||||
contextAction: component.contextAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
|
||||
@@ -554,6 +645,8 @@ final class StoragePeerListPanelComponent: Component {
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { _ in
|
||||
},
|
||||
contextAction: { _, _, _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
||||
@@ -439,6 +439,13 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var currentPanelView: UIView? {
|
||||
guard let currentId = self.currentId, let panel = self.visiblePanels[currentId] else {
|
||||
return nil
|
||||
}
|
||||
return panel.view
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import UndoUI
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import TelegramStringFormatting
|
||||
import GalleryData
|
||||
|
||||
#if DEBUG
|
||||
import os.signpost
|
||||
@@ -1561,6 +1562,88 @@ final class StorageUsageScreenComponent: Component {
|
||||
} else {
|
||||
self.openPeer(peer: peer)
|
||||
}
|
||||
},
|
||||
contextAction: { [weak self] peer, sourceView, gesture in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
var itemList: [ContextMenuItem] = []
|
||||
//TODO:localize
|
||||
itemList.append(.action(ContextMenuActionItem(
|
||||
text: "Show Details",
|
||||
icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor) },
|
||||
action: { [weak self] c, _ in
|
||||
c.dismiss(completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openPeer(peer: peer)
|
||||
})
|
||||
})
|
||||
))
|
||||
itemList.append(.action(ContextMenuActionItem(
|
||||
text: "Open Profile",
|
||||
icon: { theme in
|
||||
if case .user = peer {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor)
|
||||
} else {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Groups"), color: theme.contextMenu.primaryColor)
|
||||
}
|
||||
},
|
||||
action: { [weak self] c, _ in
|
||||
c.dismiss(completion: { [weak self] in
|
||||
guard let self, let component = self.component, let controller = self.controller?() else {
|
||||
return
|
||||
}
|
||||
let peerInfoController = component.context.sharedContext.makePeerInfoController(
|
||||
context: component.context,
|
||||
updatedPresentationData: nil,
|
||||
peer: peer._asPeer(),
|
||||
mode: .generic,
|
||||
avatarInitiallyExpanded: false,
|
||||
fromChat: false,
|
||||
requestsContext: nil
|
||||
)
|
||||
if let peerInfoController {
|
||||
controller.push(peerInfoController)
|
||||
}
|
||||
})
|
||||
})
|
||||
))
|
||||
itemList.append(.action(ContextMenuActionItem(
|
||||
text: "Select",
|
||||
icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) },
|
||||
action: { [weak self] c, _ in
|
||||
c.dismiss(completion: {
|
||||
})
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.selectionState == nil {
|
||||
self.selectionState = SelectionState()
|
||||
}
|
||||
self.selectionState = self.selectionState?.togglePeer(id: peer.id)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
})
|
||||
))
|
||||
let items = ContextController.Items(content: .list(itemList))
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let controller = ContextController(
|
||||
account: component.context.account,
|
||||
presentationData: presentationData,
|
||||
source: .extracted(StorageUsageListContextExtractedContentSource(contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture)
|
||||
|
||||
self.controller?()?.forEachController({ controller in
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
controller.dismiss()
|
||||
}
|
||||
return true
|
||||
})
|
||||
self.controller?()?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
))
|
||||
))
|
||||
@@ -1573,15 +1656,25 @@ final class StorageUsageScreenComponent: Component {
|
||||
context: component.context,
|
||||
items: self.imageItems,
|
||||
selectionState: self.selectionState,
|
||||
peerAction: { [weak self] messageId in
|
||||
action: { [weak self] messageId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.selectionState == nil {
|
||||
self.selectionState = SelectionState()
|
||||
guard let message = self.currentMessages[messageId] else {
|
||||
return
|
||||
}
|
||||
self.selectionState = self.selectionState?.toggleMessage(id: messageId)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
if self.selectionState == nil {
|
||||
self.openMessage(message: message)
|
||||
} else {
|
||||
self.selectionState = self.selectionState?.toggleMessage(id: messageId)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
}
|
||||
},
|
||||
contextAction: { [weak self] messageId, containerView, sourceRect, gesture in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.messageGaleryContextAction(messageId: messageId, sourceView: containerView, sourceRect: sourceRect, gesture: gesture)
|
||||
}
|
||||
))
|
||||
))
|
||||
@@ -1594,15 +1687,25 @@ final class StorageUsageScreenComponent: Component {
|
||||
context: component.context,
|
||||
items: self.fileItems,
|
||||
selectionState: self.selectionState,
|
||||
peerAction: { [weak self] messageId in
|
||||
action: { [weak self] messageId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.selectionState == nil {
|
||||
self.selectionState = SelectionState()
|
||||
guard let message = self.currentMessages[messageId] else {
|
||||
return
|
||||
}
|
||||
self.selectionState = self.selectionState?.toggleMessage(id: messageId)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
if self.selectionState == nil {
|
||||
self.openMessage(message: message)
|
||||
} else {
|
||||
self.selectionState = self.selectionState?.toggleMessage(id: messageId)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
}
|
||||
},
|
||||
contextAction: { [weak self] messageId, containerView, gesture in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.messageContextAction(messageId: messageId, sourceView: containerView, gesture: gesture)
|
||||
}
|
||||
))
|
||||
))
|
||||
@@ -1615,15 +1718,30 @@ final class StorageUsageScreenComponent: Component {
|
||||
context: component.context,
|
||||
items: self.musicItems,
|
||||
selectionState: self.selectionState,
|
||||
peerAction: { [weak self] messageId in
|
||||
action: { [weak self] messageId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.selectionState == nil {
|
||||
self.selectionState = SelectionState()
|
||||
guard let message = self.currentMessages[messageId] else {
|
||||
return
|
||||
}
|
||||
self.selectionState = self.selectionState?.toggleMessage(id: messageId)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
if self.selectionState == nil {
|
||||
//self.openMessage(message: message)
|
||||
let _ = message
|
||||
|
||||
self.selectionState = SelectionState()
|
||||
self.selectionState = self.selectionState?.toggleMessage(id: messageId)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
} else {
|
||||
self.selectionState = self.selectionState?.toggleMessage(id: messageId)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
}
|
||||
},
|
||||
contextAction: { [weak self] messageId, containerView, gesture in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.messageContextAction(messageId: messageId, sourceView: containerView, gesture: gesture)
|
||||
}
|
||||
))
|
||||
))
|
||||
@@ -1974,6 +2092,316 @@ final class StorageUsageScreenComponent: Component {
|
||||
controller.push(childController)
|
||||
}
|
||||
|
||||
private func messageGaleryContextAction(messageId: EngineMessage.Id, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture) {
|
||||
guard let component = self.component, let message = self.currentMessages[messageId] else {
|
||||
gesture.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (chatMediaListPreviewControllerData(
|
||||
context: component.context,
|
||||
chatLocation: .peer(id: message.id.peerId),
|
||||
chatLocationContextHolder: nil,
|
||||
message: message,
|
||||
standalone: true,
|
||||
reverseMessageGalleryOrder: false,
|
||||
navigationController: self.controller?()?.navigationController as? NavigationController
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] previewData in
|
||||
guard let self, let component = self.component, let previewData else {
|
||||
gesture.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
let context = component.context
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
|
||||
c.dismiss(completion: { [weak self] in
|
||||
guard let self, let component = self.component, let controller = self.controller?(), let navigationController = controller.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
guard let peer = message.peers[message.id.peerId].flatMap(EnginePeer.init) else {
|
||||
return
|
||||
}
|
||||
|
||||
var chatLocation: NavigateToChatControllerParams.Location = .peer(peer)
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum), let threadId = message.threadId {
|
||||
chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false))
|
||||
}
|
||||
|
||||
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
|
||||
navigationController: navigationController,
|
||||
context: component.context,
|
||||
chatLocation: chatLocation,
|
||||
subject: .message(id: .id(message.id), highlight: true, timecode: nil),
|
||||
keepStack: .always
|
||||
))
|
||||
})
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strings.Conversation_ContextMenuSelect, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
c.dismiss(completion: {
|
||||
})
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.selectionState == nil {
|
||||
self.selectionState = SelectionState()
|
||||
}
|
||||
self.selectionState = self.selectionState?.toggleMessage(id: message.id)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
})))
|
||||
|
||||
switch previewData {
|
||||
case let .gallery(gallery):
|
||||
gallery.setHintWillBePresentedInPreviewingContext(true)
|
||||
let contextController = ContextController(
|
||||
account: component.context.account,
|
||||
presentationData: presentationData,
|
||||
source: .controller(StorageUsageListContextGalleryContentSourceImpl(
|
||||
controller: gallery,
|
||||
sourceView: sourceView,
|
||||
sourceRect: sourceRect
|
||||
)),
|
||||
items: .single(ContextController.Items(content: .list(items))),
|
||||
gesture: gesture
|
||||
)
|
||||
self.controller?()?.presentInGlobalOverlay(contextController)
|
||||
case .instantPage:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func messageContextAction(messageId: EngineMessage.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let message = self.currentMessages[messageId] else {
|
||||
return
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
var openTitle: String = "Open"
|
||||
var isAudio: Bool = false
|
||||
for media in message.media {
|
||||
if let _ = media as? TelegramMediaImage {
|
||||
openTitle = "Open Photo"
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
if file.isVideo {
|
||||
openTitle = "Open Video"
|
||||
} else {
|
||||
openTitle = "Open File"
|
||||
}
|
||||
isAudio = file.isMusic || file.isVoice
|
||||
}
|
||||
}
|
||||
|
||||
var itemList: [ContextMenuItem] = []
|
||||
//TODO:localize
|
||||
if !isAudio {
|
||||
itemList.append(.action(ContextMenuActionItem(
|
||||
text: openTitle,
|
||||
icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Expand"), color: theme.contextMenu.primaryColor) },
|
||||
action: { [weak self] c, _ in
|
||||
c.dismiss(completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.openMessage(message: message)
|
||||
})
|
||||
})
|
||||
))
|
||||
}
|
||||
itemList.append(.action(ContextMenuActionItem(
|
||||
text: "View in Chat",
|
||||
icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)
|
||||
},
|
||||
action: { [weak self] c, _ in
|
||||
c.dismiss(completion: { [weak self] in
|
||||
guard let self, let component = self.component, let controller = self.controller?(), let navigationController = controller.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
guard let peer = message.peers[message.id.peerId].flatMap(EnginePeer.init) else {
|
||||
return
|
||||
}
|
||||
|
||||
var chatLocation: NavigateToChatControllerParams.Location = .peer(peer)
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum), let threadId = message.threadId {
|
||||
chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false))
|
||||
}
|
||||
|
||||
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
|
||||
navigationController: navigationController,
|
||||
context: component.context,
|
||||
chatLocation: chatLocation,
|
||||
subject: .message(id: .id(message.id), highlight: true, timecode: nil),
|
||||
keepStack: .always
|
||||
))
|
||||
})
|
||||
})
|
||||
))
|
||||
itemList.append(.action(ContextMenuActionItem(
|
||||
text: "Select",
|
||||
icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) },
|
||||
action: { [weak self] c, _ in
|
||||
c.dismiss(completion: {
|
||||
})
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.selectionState == nil {
|
||||
self.selectionState = SelectionState()
|
||||
}
|
||||
self.selectionState = self.selectionState?.toggleMessage(id: message.id)
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
})
|
||||
))
|
||||
let items = ContextController.Items(content: .list(itemList))
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let controller = ContextController(
|
||||
account: component.context.account,
|
||||
presentationData: presentationData,
|
||||
source: .extracted(StorageUsageListContextExtractedContentSource(contentView: sourceView)), items: .single(items), recognizer: nil, gesture: gesture)
|
||||
|
||||
self.controller?()?.forEachController({ controller in
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
controller.dismiss()
|
||||
}
|
||||
return true
|
||||
})
|
||||
self.controller?()?.presentInGlobalOverlay(controller)
|
||||
}
|
||||
|
||||
private func openMessage(message: Message) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
guard let controller = self.controller?(), let navigationController = controller.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
let foundGalleryMessage: Message? = message
|
||||
guard let galleryMessage = foundGalleryMessage else {
|
||||
return
|
||||
}
|
||||
self.endEditing(true)
|
||||
|
||||
let _ = component.context.sharedContext.openChatMessage(OpenChatMessageParams(
|
||||
context: component.context,
|
||||
chatLocation: .peer(id: message.id.peerId),
|
||||
chatLocationContextHolder: nil,
|
||||
message: galleryMessage,
|
||||
standalone: true,
|
||||
reverseMessageGalleryOrder: true,
|
||||
navigationController: navigationController,
|
||||
dismissInput: { [weak self] in
|
||||
self?.endEditing(true)
|
||||
}, present: { [weak self] c, a in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?()?.present(c, in: .window(.root), with: a, blockInteraction: true)
|
||||
},
|
||||
transitionNode: { [weak self] messageId, media in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let panelContainerView = self.panelContainer.view as? StorageUsagePanelContainerComponent.View {
|
||||
if let currentPanelView = panelContainerView.currentPanelView as? StorageMediaGridPanelComponent.View {
|
||||
return currentPanelView.transitionNodeForGallery(messageId: messageId, media: media)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, addToTransitionSurface: { [weak self] view in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let panelContainerView = self.panelContainer.view as? StorageUsagePanelContainerComponent.View {
|
||||
panelContainerView.currentPanelView?.addSubview(view)
|
||||
}
|
||||
}, openUrl: { [weak self] url in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, openPeer: { [weak self] peer, navigation in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
},
|
||||
callPeer: { _, _ in
|
||||
//self?.controllerInteraction?.callPeer(peerId)
|
||||
},
|
||||
enqueueMessage: { _ in
|
||||
},
|
||||
sendSticker: nil,
|
||||
sendEmoji: nil,
|
||||
setupTemporaryHiddenMedia: { _, _, _ in },
|
||||
chatAvatarHiddenMedia: { _, _ in },
|
||||
actionInteraction: GalleryControllerActionInteraction(openUrl: { [weak self] url, concealed in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
//strongSelf.openUrl(url: url, concealed: false, external: false)
|
||||
}, openUrlIn: { [weak self] url in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, openPeerMention: { [weak self] mention in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, openPeer: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, openHashtag: { [weak self] peerName, hashtag in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, openBotCommand: { _ in
|
||||
}, addContact: { _ in
|
||||
}, storeMediaPlaybackState: { [weak self] messageId, timestamp, playbackRate in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}, editMedia: { [weak self] messageId, snapshots, transitionCompletion in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}),
|
||||
centralItemUpdated: { [weak self] messageId in
|
||||
//let _ = self?.paneContainerNode.requestExpandTabs?()
|
||||
//self?.paneContainerNode.currentPane?.node.ensureMessageIsVisible(id: messageId)
|
||||
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = self
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
private func requestClear(categories: Set<Category>, peers: Set<PeerId>, messages: Set<EngineMessage.Id>) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
@@ -2730,3 +3158,59 @@ private class StorageUsageClearProgressOverlayNode: ASDisplayNode {
|
||||
self.descriptionTextNode.frame = descriptionTextFrame
|
||||
}
|
||||
}
|
||||
|
||||
private final class StorageUsageListContextGalleryContentSourceImpl: ContextControllerContentSource {
|
||||
let controller: ViewController
|
||||
weak var sourceView: UIView?
|
||||
let sourceRect: CGRect
|
||||
|
||||
let navigationController: NavigationController? = nil
|
||||
|
||||
let passthroughTouches: Bool
|
||||
|
||||
init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect = CGRect(origin: CGPoint(), size: CGSize()), passthroughTouches: Bool = false) {
|
||||
self.controller = controller
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
self.passthroughTouches = passthroughTouches
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerTakeControllerInfo? {
|
||||
let sourceView = self.sourceView
|
||||
let sourceRect = self.sourceRect
|
||||
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceView] in
|
||||
if let sourceView = sourceView {
|
||||
let rect = sourceRect.isEmpty ? sourceView.bounds : sourceRect
|
||||
return (sourceView, rect)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func animatedIn() {
|
||||
self.controller.didAppearInContextPreview()
|
||||
}
|
||||
}
|
||||
|
||||
private final class StorageUsageListContextExtractedContentSource: ContextExtractedContentSource {
|
||||
let keepInPlace: Bool = false
|
||||
let ignoreContentTouches: Bool = false
|
||||
let blurBackground: Bool = true
|
||||
|
||||
//let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center
|
||||
|
||||
private let contentView: ContextExtractedContentContainingView
|
||||
|
||||
init(contentView: ContextExtractedContentContainingView) {
|
||||
self.contentView = contentView
|
||||
}
|
||||
|
||||
func takeView() -> ContextControllerTakeViewInfo? {
|
||||
return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
|
||||
func putBack() -> ContextControllerPutBackViewInfo? {
|
||||
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user