Swiftgram/TelegramUI/InstantPageImageNode.swift
2019-01-22 21:58:59 +04:00

253 lines
12 KiB
Swift

import Foundation
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
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 let imageNode: TransformImageNode
private let statusNode: RadialStatusNode
private let linkIconNode: ASImageNode
private let pinNode: ChatMessageLiveLocationPositionNode
private var currentSize: CGSize?
private var fetchStatus: MediaResourceStatus?
private var fetchedDisposable = MetaDisposable()
private var statusDisposable = MetaDisposable()
private var themeUpdated: Bool = false
init(context: AccountContext, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> 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.imageNode = TransformImageNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
self.linkIconNode = ASImageNode()
self.pinNode = ChatMessageLiveLocationPositionNode()
super.init()
self.addSubnode(self.imageNode)
if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, photoReference: imageReference))
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, storeToDownloadsPeerType: nil).start())
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 = status
strongSelf.updateFetchStatus()
}
}
}))
if media.url != nil {
self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink")
self.addSubnode(self.linkIconNode)
}
self.addSubnode(self.statusNode)
}
} else if let file = media.media as? TelegramMediaFile {
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
if file.mimeType.hasPrefix("image/") {
_ = freeMediaFileInteractiveFetched(account: context.account, fileReference: fileReference).start()
self.imageNode.setSignal(instantPageImageFile(account: context.account, fileReference: fileReference, fetched: true))
} else {
self.imageNode.setSignal(chatMessageVideo(postbox: context.account.postbox, videoReference: fileReference))
}
} else if let map = media.media as? TelegramMediaMap {
self.addSubnode(self.pinNode)
var zoom: Int32 = 12
var dimensions = CGSize(width: 200.0, height: 100.0)
for attribute in self.attributes {
if let mapAttribute = attribute as? InstantPageMapAttribute {
zoom = mapAttribute.zoom
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(account: context.account, resource: resource))
} else if let webPage = media.media as? TelegramMediaWebpage, 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, photoReference: imageReference))
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, photoReference: imageReference, storeToDownloadsPeerType: nil).start())
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
self.addSubnode(self.statusNode)
}
}
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(isActive, progress):
let adjustedProgress = max(progress, 0.027)
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: false)
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.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 let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.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(), 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 let file = self.media.media as? TelegramMediaFile, let dimensions = file.dimensions {
let emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil
let imageSize = dimensions.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 self.media.media is TelegramMediaMap {
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.currentPresentationData.with { $0 }.theme
let (pinSize, pinApply) = makePinLayout(self.context.account, theme, nil, false)
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 let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image, let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.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(), emptyColor: self.theme.pageBackgroundColor))
apply()
}
}
}
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, () -> UIView?)? {
if media == self.media {
let imageNode = self.imageNode
return (self.imageNode, { [weak imageNode] in
return imageNode?.view.snapshotContentTree(unhide: true)
})
} 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 {
switch gesture {
case .tap:
if self.media.media is TelegramMediaImage && self.media.index == -1 {
return
}
self.openMedia(self.media)
case .longTap:
self.longPressMedia(self.media)
default:
break
}
}
default:
break
}
}
}