Swiftgram/TelegramUI/GridMessageItem.swift
2017-10-03 15:57:32 +03:00

324 lines
13 KiB
Swift

import Foundation
import Display
import AsyncDisplayKit
import TelegramCore
import Postbox
import SwiftSignalKit
private func mediaForMessage(_ message: Message) -> Media? {
for media in message.media {
if let media = media as? TelegramMediaImage {
return media
} else if let file = media as? TelegramMediaFile {
if file.mimeType.hasPrefix("audio/") {
return nil
} else if !file.isVideo && file.mimeType.hasPrefix("video/") {
return file
} else {
return file
}
}
}
return nil
}
private let timezoneOffset: Int32 = {
let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var now: time_t = time_t(nowTimestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
return Int32(timeinfoNow.tm_gmtoff)
}()
final class GridMessageItemSection: GridSection {
let height: CGFloat = 44.0
private let theme: PresentationTheme
private let strings: PresentationStrings
private let roundedTimestamp: Int32
private let month: Int32
private let year: Int32
var hashValue: Int {
return self.roundedTimestamp.hashValue
}
init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings) {
self.theme = theme
self.strings = strings
var now = time_t(timestamp)
var timeinfoNow: tm = tm()
localtime_r(&now, &timeinfoNow)
self.roundedTimestamp = timeinfoNow.tm_year * 100 + timeinfoNow.tm_mon
self.month = timeinfoNow.tm_mon
self.year = timeinfoNow.tm_year
}
func isEqual(to: GridSection) -> Bool {
if let to = to as? GridMessageItemSection {
return self.roundedTimestamp == to.roundedTimestamp
} else {
return false
}
}
func node() -> ASDisplayNode {
return GridMessageItemSectionNode(theme: self.theme, strings: self.strings, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year)
}
}
private let sectionTitleFont = Font.regular(17.0)
final class GridMessageItemSectionNode: ASDisplayNode {
var theme: PresentationTheme
var strings: PresentationStrings
let titleNode: ASTextNode
init(theme: PresentationTheme, strings: PresentationStrings, roundedTimestamp: Int32, month: Int32, year: Int32) {
self.theme = theme
self.strings = strings
self.titleNode = ASTextNode()
self.titleNode.isLayerBacked = true
super.init()
self.backgroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.9)
let dateText = stringForMonth(strings: strings, month: month, ofYear: year)
self.addSubnode(self.titleNode)
self.titleNode.attributedText = NSAttributedString(string: dateText, font: sectionTitleFont, textColor: theme.list.itemPrimaryTextColor)
self.titleNode.maximumNumberOfLines = 1
self.titleNode.truncationMode = .byTruncatingTail
}
override func layout() {
super.layout()
let bounds = self.bounds
let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 18.0), size: titleSize)
}
}
final class GridMessageItem: GridItem {
private let theme: PresentationTheme
private let strings: PresentationStrings
private let account: Account
private let message: Message
private let controllerInteraction: ChatControllerInteraction
let section: GridSection?
init(theme: PresentationTheme, strings: PresentationStrings, account: Account, message: Message, controllerInteraction: ChatControllerInteraction) {
self.theme = theme
self.strings = strings
self.account = account
self.message = message
self.controllerInteraction = controllerInteraction
self.section = GridMessageItemSection(timestamp: message.timestamp, theme: theme, strings: strings)
}
func node(layout: GridNodeLayout) -> GridItemNode {
let node = GridMessageItemNode()
if let media = mediaForMessage(self.message) {
node.setup(account: self.account, media: media, messageId: self.message.id, controllerInteraction: self.controllerInteraction)
}
return node
}
func update(node: GridItemNode) {
guard let node = node as? GridMessageItemNode else {
assertionFailure()
return
}
if let media = mediaForMessage(self.message) {
node.setup(account: self.account, media: media, messageId: self.message.id, controllerInteraction: self.controllerInteraction)
}
}
}
final class GridMessageItemNode: GridItemNode {
private var currentState: (Account, Media, CGSize)?
private let imageNode: TransformImageNode
private var messageId: MessageId?
private var controllerInteraction: ChatControllerInteraction?
private var progressNode: RadialProgressNode
private var selectionNode: GridMessageSelectionNode?
private let fetchStatusDisposable = MetaDisposable()
private let fetchDisposable = MetaDisposable()
private var resourceStatus: MediaResourceStatus?
override init() {
self.imageNode = TransformImageNode()
self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor.white, icon: nil))
self.progressNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.imageNode)
}
deinit {
self.fetchStatusDisposable.dispose()
self.fetchDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
}
func setup(account: Account, media: Media, messageId: MessageId, controllerInteraction: ChatControllerInteraction) {
if self.currentState == nil || self.currentState!.0 !== account || !self.currentState!.1.isEqual(media) {
var mediaDimensions: CGSize?
if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions {
mediaDimensions = largestSize
self.imageNode.setSignal(account: account, signal: mediaGridMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: true)
self.fetchStatusDisposable.set(nil)
self.progressNode.removeFromSupernode()
self.progressNode.isHidden = true
self.resourceStatus = nil
} else if let file = media as? TelegramMediaFile, file.isVideo {
mediaDimensions = file.dimensions
self.imageNode.setSignal(account: account, signal: mediaGridMessageVideo(account: account, video: file))
self.resourceStatus = nil
self.fetchStatusDisposable.set((account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
strongSelf.resourceStatus = status
switch status {
case let .Fetching(isActive, progress):
var adjustedProgress = progress
if isActive {
adjustedProgress = max(adjustedProgress, 0.027)
}
strongSelf.progressNode.state = .Fetching(progress: adjustedProgress)
strongSelf.progressNode.isHidden = false
case .Local:
strongSelf.progressNode.state = .None
strongSelf.progressNode.isHidden = true
case .Remote:
strongSelf.progressNode.state = .Remote
strongSelf.progressNode.isHidden = false
}
}
}))
if self.progressNode.supernode == nil {
self.addSubnode(self.progressNode)
}
}
if let mediaDimensions = mediaDimensions {
self.currentState = (account, media, mediaDimensions)
self.setNeedsLayout()
}
}
self.messageId = messageId
self.controllerInteraction = controllerInteraction
self.updateSelectionState(animated: false)
self.updateHiddenMedia()
}
override func layout() {
super.layout()
let imageFrame = self.bounds.insetBy(dx: 1.0, dy: 1.0)
self.imageNode.frame = imageFrame
if let (_, _, mediaDimensions) = self.currentState {
let imageSize = mediaDimensions.aspectFilled(imageFrame.size)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets()))()
}
self.selectionNode?.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
let progressDiameter: CGFloat = 40.0
self.progressNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + floor((imageFrame.size.width - progressDiameter) / 2.0), y: imageFrame.minY + floor((imageFrame.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter))
}
func updateSelectionState(animated: Bool) {
if let messageId = self.messageId, let controllerInteraction = self.controllerInteraction {
if let selectionState = controllerInteraction.selectionState {
let selected = selectionState.selectedIds.contains(messageId)
if let selectionNode = self.selectionNode {
selectionNode.updateSelected(selected, animated: animated)
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
} else {
let selectionNode = GridMessageSelectionNode(toggle: { [weak self] in
if let strongSelf = self, let messageId = strongSelf.messageId {
strongSelf.controllerInteraction?.toggleMessageSelection(messageId)
}
})
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
self.addSubnode(selectionNode)
self.selectionNode = selectionNode
selectionNode.updateSelected(selected, animated: false)
if animated {
selectionNode.animateIn()
}
}
} else {
if let selectionNode = self.selectionNode {
self.selectionNode = nil
if animated {
selectionNode.animateOut { [weak selectionNode] in
selectionNode?.removeFromSupernode()
}
} else {
selectionNode.removeFromSupernode()
}
}
}
}
}
func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? {
if self.messageId == id {
return self.imageNode
} else {
return nil
}
}
func updateHiddenMedia() {
if let controllerInteraction = self.controllerInteraction, let messageId = self.messageId, controllerInteraction.hiddenMedia[messageId] != nil {
self.imageNode.isHidden = true
} else {
self.imageNode.isHidden = false
}
}
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
if let controllerInteraction = self.controllerInteraction, let messageId = self.messageId, case .ended = recognizer.state {
if let (account, media, _) = self.currentState {
if let file = media as? TelegramMediaFile {
if let resourceStatus = self.resourceStatus {
switch resourceStatus {
case .Fetching:
messageMediaFileCancelInteractiveFetch(account: account, messageId: messageId, file: file)
case .Local:
controllerInteraction.openMessage(messageId)
case .Remote:
self.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, messageId: messageId, file: file).start())
}
}
} else {
controllerInteraction.openMessage(messageId)
}
}
}
}
}