Swiftgram/submodules/InstantPageUI/Sources/InstantPageImageNode.swift
2023-04-19 23:47:38 +04:00

329 lines
17 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import RadialStatusNode
import PhotoResources
import MediaResources
import LocationResources
import LiveLocationPositionNode
import AppBundle
import TelegramUIPreferences
import ContextUI
private struct FetchControls {
let fetch: (Bool) -> Void
let cancel: () -> Void
}
final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
private let context: AccountContext
private let webPage: TelegramMediaWebpage
private var theme: InstantPageTheme
let media: InstantPageMedia
let attributes: [InstantPageImageAttribute]
private let interactive: Bool
private let roundCorners: Bool
private let fit: Bool
private let openMedia: (InstantPageMedia) -> Void
private let longPressMedia: (InstantPageMedia) -> Void
private var fetchControls: FetchControls?
private let pinchContainerNode: PinchSourceContainerNode
private let imageNode: TransformImageNode
private let statusNode: RadialStatusNode
private let linkIconNode: ASImageNode
private let pinNode: ChatMessageLiveLocationPositionNode
private var currentSize: CGSize?
private var fetchStatus: EngineMediaResource.FetchStatus?
private var fetchedDisposable = MetaDisposable()
private var statusDisposable = MetaDisposable()
private var themeUpdated: Bool = false
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?) {
self.context = context
self.theme = theme
self.webPage = webPage
self.media = media
self.attributes = attributes
self.interactive = interactive
self.roundCorners = roundCorners
self.fit = fit
self.openMedia = openMedia
self.longPressMedia = longPressMedia
self.pinchContainerNode = PinchSourceContainerNode()
self.imageNode = TransformImageNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
self.linkIconNode = ASImageNode()
self.pinNode = ChatMessageLiveLocationPositionNode()
super.init()
self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
self.addSubnode(self.pinchContainerNode)
if case let .image(image) = media.media, let largest = largestImageRepresentation(image.representations) {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: image) {
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
}
self.fetchControls = FetchControls(fetch: { [weak self] manual in
if let strongSelf = self {
strongSelf.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
}
}, cancel: {
chatMessagePhotoCancelInteractiveFetch(account: context.account, photoReference: imageReference)
})
if interactive {
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch {
if let strongSelf = self {
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.updateFetchStatus()
}
}
}))
if media.url != nil {
self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink")
self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode)
}
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
}
} else if case let .file(file) = media.media {
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
if file.mimeType.hasPrefix("image/") {
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) {
_ = freeMediaFileInteractiveFetched(account: context.account, userLocation: sourceLocation.userLocation, fileReference: fileReference).start()
}
self.imageNode.setSignal(instantPageImageFile(account: context.account, userLocation: sourceLocation.userLocation, fileReference: fileReference, fetched: true))
} else {
self.imageNode.setSignal(chatMessageVideo(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, videoReference: fileReference))
}
if file.isVideo {
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
}
} else if case let .geo(map) = media.media {
self.addSubnode(self.pinNode)
var dimensions = CGSize(width: 200.0, height: 100.0)
for attribute in self.attributes {
if let mapAttribute = attribute as? InstantPageMapAttribute {
dimensions = mapAttribute.dimensions
break
}
}
let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(dimensions.width), height: Int32(dimensions.height))
self.imageNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource))
} else if case let .webpage(webPage) = media.media, case let .Loaded(content) = webPage.content, let image = content.image {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
}
if let activatePinchPreview = activatePinchPreview {
self.pinchContainerNode.activate = { sourceNode in
activatePinchPreview(sourceNode)
}
self.pinchContainerNode.animatedOut = { [weak self] in
guard let strongSelf = self else {
return
}
pinchPreviewFinished?(strongSelf)
}
}
}
deinit {
self.fetchedDisposable.dispose()
self.statusDisposable.dispose()
}
override func didLoad() {
super.didLoad()
if self.interactive {
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
recognizer.delaysTouchesBegan = false
self.view.addGestureRecognizer(recognizer)
} else {
self.view.isUserInteractionEnabled = false
}
}
func updateIsVisible(_ isVisible: Bool) {
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
}
func update(strings: PresentationStrings, theme: InstantPageTheme) {
if self.theme.imageTintColor != theme.imageTintColor {
self.theme = theme
self.themeUpdated = true
self.setNeedsLayout()
}
}
private func updateFetchStatus() {
var state: RadialStatusNodeState = .none
if let fetchStatus = self.fetchStatus {
switch fetchStatus {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
case .Remote:
state = .download(.white)
default:
break
}
}
self.statusNode.transitionToState(state, completion: { [weak statusNode] in
if state == .none {
statusNode?.removeFromSupernode()
}
})
}
override func layout() {
super.layout()
let size = self.bounds.size
if self.currentSize != size || self.themeUpdated {
self.currentSize = size
self.themeUpdated = false
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.pinchContainerNode.update(size: size, transition: .immediate)
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
let radialStatusSize: CGFloat = 50.0
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize)
if case let .image(image) = self.media.media, let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.dimensions.cgSize.aspectFilled(size)
let boundingSize = size
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
let makeLayout = self.imageNode.asyncLayout()
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: self.theme.panelBackgroundColor))
apply()
self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0)
} else if case let .file(file) = self.media.media, let dimensions = file.dimensions {
let emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil
let imageSize = dimensions.cgSize.aspectFilled(size)
let boundingSize = size
let makeLayout = self.imageNode.asyncLayout()
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: emptyColor))
apply()
} else if case .geo = self.media.media {
for attribute in self.attributes {
if let mapAttribute = attribute as? InstantPageMapAttribute {
let imageSize = mapAttribute.dimensions.aspectFilled(size)
let boundingSize = size
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
let makeLayout = self.imageNode.asyncLayout()
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
apply()
break
}
}
let makePinLayout = self.pinNode.asyncLayout()
let theme = self.context.sharedContext.currentPresentationData.with { $0 }.theme
let (pinSize, pinApply) = makePinLayout(self.context, theme, .location(nil))
self.pinNode.frame = CGRect(origin: CGPoint(x: floor((size.width - pinSize.width) / 2.0), y: floor(size.height * 0.5 - 10.0 - pinSize.height / 2.0)), size: pinSize)
pinApply()
} else if case let .webpage(webPage) = media.media, case let .Loaded(content) = webPage.content, let image = content.image, let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.dimensions.cgSize.aspectFilled(size)
let boundingSize = size
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
let makeLayout = self.imageNode.asyncLayout()
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: self.theme.pageBackgroundColor))
apply()
}
}
}
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if media == self.media {
let imageNode = self.imageNode
return (self.imageNode, self.imageNode.bounds, { [weak imageNode] in
return (imageNode?.view.snapshotContentTree(unhide: true), nil)
})
} else {
return nil
}
}
func updateHiddenMedia(media: InstantPageMedia?) {
self.imageNode.isHidden = self.media == media
self.statusNode.isHidden = self.imageNode.isHidden
}
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
if let fetchStatus = self.fetchStatus {
switch fetchStatus {
case .Local:
switch gesture {
case .tap:
if case .image = self.media.media, self.media.index == -1 {
return
}
self.openMedia(self.media)
case .longTap:
self.longPressMedia(self.media)
default:
break
}
case .Remote, .Paused:
if case .tap = gesture {
self.fetchControls?.fetch(true)
}
case .Fetching:
if case .tap = gesture {
self.fetchControls?.cancel()
}
}
} else {
switch gesture {
case .tap:
if case .image = self.media.media, self.media.index == -1 {
return
}
self.openMedia(self.media)
case .longTap:
self.longPressMedia(self.media)
default:
break
}
}
}
default:
break
}
}
}