mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
2443 lines
100 KiB
Swift
2443 lines
100 KiB
Swift
import AsyncDisplayKit
|
|
import Display
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import ContextUI
|
|
import PhotoResources
|
|
import RadialStatusNode
|
|
import TelegramStringFormatting
|
|
import GridMessageSelectionNode
|
|
import UniversalMediaPlayer
|
|
import ListMessageItem
|
|
import ChatMessageInteractiveMediaBadge
|
|
import SparseItemGrid
|
|
import ShimmerEffect
|
|
import QuartzCore
|
|
import DirectMediaImageCache
|
|
import ComponentFlow
|
|
import TelegramNotices
|
|
import TelegramUIPreferences
|
|
import CheckNode
|
|
import AppBundle
|
|
|
|
private final class FrameSequenceThumbnailNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
private let file: FileMediaReference
|
|
private let imageNode: ASImageNode
|
|
|
|
private var isPlaying: Bool = false
|
|
private var isPlayingInternal: Bool = false
|
|
|
|
private var frames: [Int: UIImage] = [:]
|
|
|
|
private var frameTimes: [Double] = []
|
|
private var sources: [UniversalSoftwareVideoSource] = []
|
|
private var disposables: [Int: Disposable] = [:]
|
|
|
|
private var currentFrameIndex: Int = 0
|
|
private var timer: SwiftSignalKit.Timer?
|
|
|
|
init(
|
|
context: AccountContext,
|
|
file: FileMediaReference
|
|
) {
|
|
self.context = context
|
|
self.file = file
|
|
|
|
self.imageNode = ASImageNode()
|
|
self.imageNode.isUserInteractionEnabled = false
|
|
self.imageNode.contentMode = .scaleAspectFill
|
|
self.imageNode.clipsToBounds = true
|
|
|
|
if let duration = file.media.duration {
|
|
let frameCount = 5
|
|
let frameInterval: Double = Double(duration) / Double(frameCount)
|
|
for i in 0 ..< frameCount {
|
|
self.frameTimes.append(Double(i) * frameInterval)
|
|
}
|
|
}
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.imageNode)
|
|
|
|
for i in 0 ..< self.frameTimes.count {
|
|
let framePts = self.frameTimes[i]
|
|
let index = i
|
|
|
|
let source = UniversalSoftwareVideoSource(
|
|
mediaBox: self.context.account.postbox.mediaBox,
|
|
fileReference: self.file,
|
|
automaticallyFetchHeader: true
|
|
)
|
|
self.sources.append(source)
|
|
self.disposables[index] = (source.takeFrame(at: framePts)
|
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if case let .image(image) = result {
|
|
if let image = image {
|
|
strongSelf.frames[index] = image
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
for (_, disposable) in self.disposables {
|
|
disposable.dispose()
|
|
}
|
|
self.timer?.invalidate()
|
|
}
|
|
|
|
func updateIsPlaying(_ isPlaying: Bool) {
|
|
if self.isPlaying == isPlaying {
|
|
return
|
|
}
|
|
self.isPlaying = isPlaying
|
|
}
|
|
|
|
func updateLayout(size: CGSize) {
|
|
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
}
|
|
|
|
func tick() {
|
|
let isPlayingInternal = self.isPlaying && self.frames.count == self.frameTimes.count
|
|
if isPlayingInternal {
|
|
self.currentFrameIndex = (self.currentFrameIndex + 1) % self.frames.count
|
|
|
|
if self.currentFrameIndex < self.frames.count {
|
|
self.imageNode.image = self.frames[self.currentFrameIndex]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private let mediaBadgeBackgroundColor = UIColor(white: 0.0, alpha: 0.6)
|
|
private let mediaBadgeTextColor = UIColor.white
|
|
|
|
private final class VisualMediaItemInteraction {
|
|
let openMessage: (Message) -> Void
|
|
let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
|
let toggleSelection: (MessageId, Bool) -> Void
|
|
|
|
var hiddenMedia: [MessageId: [Media]] = [:]
|
|
var selectedMessageIds: Set<MessageId>?
|
|
|
|
init(
|
|
openMessage: @escaping (Message) -> Void,
|
|
openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void,
|
|
toggleSelection: @escaping (MessageId, Bool) -> Void
|
|
) {
|
|
self.openMessage = openMessage
|
|
self.openMessageContextActions = openMessageContextActions
|
|
self.toggleSelection = toggleSelection
|
|
}
|
|
}
|
|
|
|
/*private final class VisualMediaItemNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
private let interaction: VisualMediaItemInteraction
|
|
|
|
private var videoLayerFrameManager: SoftwareVideoLayerFrameManager?
|
|
private var sampleBufferLayer: SampleBufferLayer?
|
|
private var displayLink: ConstantDisplayLinkAnimator?
|
|
private var displayLinkTimestamp: Double = 0.0
|
|
|
|
private var frameSequenceThumbnailNode: FrameSequenceThumbnailNode?
|
|
|
|
private let containerNode: ContextControllerSourceNode
|
|
|
|
private var placeholderNode: ShimmerEffectNode?
|
|
private var absoluteLocation: (CGRect, CGSize)?
|
|
|
|
private let imageNode: TransformImageNode
|
|
private var statusNode: RadialStatusNode
|
|
private let mediaBadgeNode: ChatMessageInteractiveMediaBadge
|
|
private var selectionNode: GridMessageSelectionNode?
|
|
|
|
private let fetchStatusDisposable = MetaDisposable()
|
|
private let fetchDisposable = MetaDisposable()
|
|
private var resourceStatus: MediaResourceStatus?
|
|
|
|
private var item: (VisualMediaItem, Media?, CGSize, CGSize?)?
|
|
private var theme: PresentationTheme?
|
|
|
|
private var hasVisibility: Bool = false
|
|
|
|
init(context: AccountContext, interaction: VisualMediaItemInteraction) {
|
|
self.context = context
|
|
self.interaction = interaction
|
|
|
|
self.containerNode = ContextControllerSourceNode()
|
|
self.imageNode = TransformImageNode()
|
|
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
|
|
let progressDiameter: CGFloat = 40.0
|
|
self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter)
|
|
self.statusNode.isUserInteractionEnabled = false
|
|
|
|
self.mediaBadgeNode = ChatMessageInteractiveMediaBadge()
|
|
self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 50.0, height: 50.0))
|
|
|
|
let shimmerNode = ShimmerEffectNode()
|
|
self.placeholderNode = shimmerNode
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.containerNode)
|
|
self.containerNode.addSubnode(self.imageNode)
|
|
self.containerNode.addSubnode(self.mediaBadgeNode)
|
|
|
|
self.containerNode.activated = { [weak self] gesture, _ in
|
|
guard let strongSelf = self, let item = strongSelf.item, let message = item.0.message else {
|
|
return
|
|
}
|
|
strongSelf.interaction.openMessageContextActions(message, strongSelf.containerNode, strongSelf.containerNode.bounds, gesture)
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
self.fetchStatusDisposable.dispose()
|
|
self.fetchDisposable.dispose()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
|
recognizer.tapActionAtPoint = { _ in
|
|
return .waitForSingleTap
|
|
}
|
|
self.imageNode.view.addGestureRecognizer(recognizer)
|
|
|
|
self.mediaBadgeNode.pressed = { [weak self] in
|
|
self?.progressPressed()
|
|
}
|
|
}
|
|
|
|
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
|
self.absoluteLocation = (rect, containerSize)
|
|
if let shimmerNode = self.placeholderNode {
|
|
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
|
|
}
|
|
}
|
|
|
|
@objc func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
|
|
if case .tap = gesture {
|
|
if let (item, _, _, _) = self.item, let message = item.message {
|
|
var media: Media?
|
|
for value in message.media {
|
|
if let image = value as? TelegramMediaImage {
|
|
media = image
|
|
break
|
|
} else if let file = value as? TelegramMediaFile {
|
|
media = file
|
|
break
|
|
}
|
|
}
|
|
|
|
if let media = media {
|
|
if let file = media as? TelegramMediaFile {
|
|
if isMediaStreamable(message: message, media: file) {
|
|
self.interaction.openMessage(message)
|
|
} else {
|
|
self.progressPressed()
|
|
}
|
|
} else {
|
|
self.interaction.openMessage(message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func progressPressed() {
|
|
guard let message = self.item?.0.message else {
|
|
return
|
|
}
|
|
|
|
var media: Media?
|
|
for value in message.media {
|
|
if let image = value as? TelegramMediaImage {
|
|
media = image
|
|
break
|
|
} else if let file = value as? TelegramMediaFile {
|
|
media = file
|
|
break
|
|
}
|
|
}
|
|
|
|
if let resourceStatus = self.resourceStatus, let file = media as? TelegramMediaFile {
|
|
switch resourceStatus {
|
|
case .Fetching:
|
|
messageMediaFileCancelInteractiveFetch(context: self.context, messageId: message.id, file: file)
|
|
case .Local:
|
|
self.interaction.openMessage(message)
|
|
case .Remote:
|
|
self.fetchDisposable.set(messageMediaFileInteractiveFetched(context: self.context, message: message, file: file, userInitiated: true).start())
|
|
}
|
|
}
|
|
}
|
|
|
|
func cancelPreviewGesture() {
|
|
self.containerNode.cancelGesture()
|
|
}
|
|
|
|
func update(size: CGSize, item: VisualMediaItem?, theme: PresentationTheme, synchronousLoad: Bool) {
|
|
if item === self.item?.0 && size == self.item?.2 {
|
|
return
|
|
}
|
|
self.theme = theme
|
|
var media: Media?
|
|
if let item = item, let message = item.message {
|
|
for value in message.media {
|
|
if let image = value as? TelegramMediaImage {
|
|
media = image
|
|
break
|
|
} else if let file = value as? TelegramMediaFile {
|
|
media = file
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if let shimmerNode = self.placeholderNode {
|
|
shimmerNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
if let (rect, size) = self.absoluteLocation {
|
|
shimmerNode.updateAbsoluteRect(rect, within: size)
|
|
}
|
|
|
|
var shapes: [ShimmerEffectNode.Shape] = []
|
|
shapes.append(.rect(rect: CGRect(origin: CGPoint(), size: size)))
|
|
|
|
shimmerNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: size)
|
|
}
|
|
|
|
if let item = item, let message = item.message, let file = media as? TelegramMediaFile, file.isAnimated {
|
|
if self.videoLayerFrameManager == nil {
|
|
let sampleBufferLayer: SampleBufferLayer
|
|
if let current = self.sampleBufferLayer {
|
|
sampleBufferLayer = current
|
|
} else {
|
|
sampleBufferLayer = takeSampleBufferLayer()
|
|
self.sampleBufferLayer = sampleBufferLayer
|
|
self.imageNode.layer.addSublayer(sampleBufferLayer.layer)
|
|
}
|
|
|
|
self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: FileMediaReference.message(message: MessageReference(message), media: file), layerHolder: sampleBufferLayer)
|
|
self.videoLayerFrameManager?.start()
|
|
}
|
|
} else {
|
|
if let sampleBufferLayer = self.sampleBufferLayer {
|
|
sampleBufferLayer.layer.removeFromSuperlayer()
|
|
self.sampleBufferLayer = nil
|
|
}
|
|
self.videoLayerFrameManager = nil
|
|
}
|
|
|
|
if let item = item, let message = item.message, let media = media, (self.item?.1 == nil || !media.isEqual(to: self.item!.1!)) {
|
|
var mediaDimensions: CGSize?
|
|
if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions {
|
|
mediaDimensions = largestSize.cgSize
|
|
|
|
if let placeholderNode = self.placeholderNode, placeholderNode.supernode == nil {
|
|
self.containerNode.insertSubnode(placeholderNode, at: 0)
|
|
}
|
|
self.imageNode.imageUpdated = { [weak self] image in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if image != nil {
|
|
strongSelf.placeholderNode?.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
self.imageNode.setSignal(mediaGridMessagePhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image), fullRepresentationSize: CGSize(width: 300.0, height: 300.0), synchronousLoad: synchronousLoad), attemptSynchronously: synchronousLoad, dispatchOnDisplayLink: true)
|
|
|
|
self.fetchStatusDisposable.set(nil)
|
|
self.statusNode.transitionToState(.none, completion: { [weak self] in
|
|
self?.statusNode.isHidden = true
|
|
})
|
|
self.mediaBadgeNode.isHidden = true
|
|
self.resourceStatus = nil
|
|
} else if let file = media as? TelegramMediaFile, file.isVideo {
|
|
if let placeholderNode = self.placeholderNode, placeholderNode.supernode == nil {
|
|
self.containerNode.insertSubnode(placeholderNode, at: 0)
|
|
}
|
|
self.imageNode.imageUpdated = { [weak self] image in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if image != nil {
|
|
strongSelf.placeholderNode?.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
mediaDimensions = file.dimensions?.cgSize
|
|
self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad)
|
|
|
|
self.mediaBadgeNode.isHidden = file.isAnimated
|
|
|
|
self.resourceStatus = nil
|
|
|
|
self.item = (item, media, size, mediaDimensions)
|
|
|
|
self.fetchStatusDisposable.set((messageMediaFileStatus(context: context, messageId: message.id, file: file)
|
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
|
if let strongSelf = self, let (item, _, _, _) = strongSelf.item, let message = item.message {
|
|
strongSelf.resourceStatus = status
|
|
|
|
let isStreamable = isMediaStreamable(message: message, media: file)
|
|
|
|
var statusState: RadialStatusNodeState = .none
|
|
if isStreamable || file.isAnimated {
|
|
statusState = .none
|
|
} else {
|
|
switch status {
|
|
case let .Fetching(_, progress):
|
|
let adjustedProgress = max(progress, 0.027)
|
|
statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
|
|
case .Local:
|
|
statusState = .none
|
|
case .Remote:
|
|
statusState = .download(.white)
|
|
}
|
|
}
|
|
|
|
switch statusState {
|
|
case .none:
|
|
break
|
|
default:
|
|
strongSelf.statusNode.isHidden = false
|
|
}
|
|
|
|
strongSelf.statusNode.transitionToState(statusState, animated: true, completion: {
|
|
if let strongSelf = self {
|
|
if case .none = statusState {
|
|
strongSelf.statusNode.isHidden = true
|
|
}
|
|
}
|
|
})
|
|
|
|
if let duration = file.duration {
|
|
let durationString = stringForDuration(duration)
|
|
|
|
var badgeContent: ChatMessageInteractiveMediaBadgeContent?
|
|
var mediaDownloadState: ChatMessageInteractiveMediaDownloadState?
|
|
|
|
if isStreamable {
|
|
switch status {
|
|
case let .Fetching(_, progress):
|
|
let progressString = String(format: "%d%%", Int(progress * 100.0))
|
|
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString))
|
|
mediaDownloadState = .compactFetching(progress: 0.0)
|
|
case .Local:
|
|
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
|
|
case .Remote:
|
|
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
|
|
mediaDownloadState = .compactRemote
|
|
}
|
|
} else {
|
|
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
|
|
}
|
|
|
|
strongSelf.mediaBadgeNode.update(theme: nil, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false)
|
|
}
|
|
}
|
|
}))
|
|
if self.statusNode.supernode == nil {
|
|
self.imageNode.addSubnode(self.statusNode)
|
|
}
|
|
} else {
|
|
self.mediaBadgeNode.isHidden = true
|
|
}
|
|
self.item = (item, media, size, mediaDimensions)
|
|
|
|
self.updateHiddenMedia()
|
|
} else {
|
|
if let placeholderNode = self.placeholderNode, placeholderNode.supernode == nil {
|
|
self.containerNode.insertSubnode(placeholderNode, at: 0)
|
|
}
|
|
}
|
|
|
|
let progressDiameter: CGFloat = 40.0
|
|
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((size.width - progressDiameter) / 2.0), y: floor((size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter))
|
|
|
|
self.mediaBadgeNode.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 18.0 - 3.0), size: CGSize(width: 50.0, height: 50.0))
|
|
|
|
self.selectionNode?.frame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
if let (item, media, _, mediaDimensions) = self.item {
|
|
self.item = (item, media, size, mediaDimensions)
|
|
|
|
let imageFrame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
self.containerNode.frame = imageFrame
|
|
self.imageNode.frame = imageFrame
|
|
if let sampleBufferLayer = self.sampleBufferLayer {
|
|
sampleBufferLayer.layer.frame = imageFrame
|
|
}
|
|
|
|
if let mediaDimensions = mediaDimensions {
|
|
let imageSize = mediaDimensions.aspectFilled(imageFrame.size)
|
|
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: theme.list.mediaPlaceholderColor))()
|
|
}
|
|
|
|
self.updateSelectionState(animated: false)
|
|
}
|
|
}
|
|
|
|
func updateIsVisible(_ isVisible: Bool) {
|
|
self.hasVisibility = isVisible
|
|
if let _ = self.videoLayerFrameManager {
|
|
let displayLink: ConstantDisplayLinkAnimator
|
|
if let current = self.displayLink {
|
|
displayLink = current
|
|
} else {
|
|
displayLink = ConstantDisplayLinkAnimator { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.videoLayerFrameManager?.tick(timestamp: strongSelf.displayLinkTimestamp)
|
|
strongSelf.displayLinkTimestamp += 1.0 / 30.0
|
|
}
|
|
displayLink.frameInterval = 2
|
|
self.displayLink = displayLink
|
|
}
|
|
}
|
|
self.displayLink?.isPaused = !self.hasVisibility || self.isHidden
|
|
|
|
/*if isVisible {
|
|
if let item = self.item?.0, let file = self.item?.1 as? TelegramMediaFile, !file.isAnimated {
|
|
if self.frameSequenceThumbnailNode == nil {
|
|
let frameSequenceThumbnailNode = FrameSequenceThumbnailNode(context: context, file: .message(message: MessageReference(item.message), media: file))
|
|
self.frameSequenceThumbnailNode = frameSequenceThumbnailNode
|
|
self.imageNode.addSubnode(frameSequenceThumbnailNode)
|
|
}
|
|
if let frameSequenceThumbnailNode = self.frameSequenceThumbnailNode {
|
|
let size = self.bounds.size
|
|
frameSequenceThumbnailNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
frameSequenceThumbnailNode.updateLayout(size: size)
|
|
}
|
|
} else {
|
|
if let frameSequenceThumbnailNode = self.frameSequenceThumbnailNode {
|
|
self.frameSequenceThumbnailNode = nil
|
|
frameSequenceThumbnailNode.removeFromSupernode()
|
|
}
|
|
}
|
|
} else {
|
|
if let frameSequenceThumbnailNode = self.frameSequenceThumbnailNode {
|
|
self.frameSequenceThumbnailNode = nil
|
|
frameSequenceThumbnailNode.removeFromSupernode()
|
|
}
|
|
}*/
|
|
|
|
self.frameSequenceThumbnailNode?.updateIsPlaying(isVisible)
|
|
}
|
|
|
|
func tick() {
|
|
self.frameSequenceThumbnailNode?.tick()
|
|
}
|
|
|
|
func updateSelectionState(animated: Bool) {
|
|
if let (item, _, _, _) = self.item, let message = item.message, let theme = self.theme {
|
|
self.containerNode.isGestureEnabled = self.interaction.selectedMessageIds == nil
|
|
|
|
if let selectedIds = self.interaction.selectedMessageIds {
|
|
let selected = selectedIds.contains(message.id)
|
|
|
|
if let selectionNode = self.selectionNode {
|
|
selectionNode.updateSelected(selected, animated: animated)
|
|
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
|
} else {
|
|
let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in
|
|
if let strongSelf = self, let messageId = strongSelf.item?.0.message?.id {
|
|
var toggledValue = true
|
|
if let selectedMessageIds = strongSelf.interaction.selectedMessageIds, selectedMessageIds.contains(messageId) {
|
|
toggledValue = false
|
|
}
|
|
strongSelf.interaction.toggleSelection(messageId, toggledValue)
|
|
}
|
|
})
|
|
|
|
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
|
self.containerNode.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() -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
let imageNode = self.imageNode
|
|
return (self.imageNode, self.imageNode.bounds, { [weak self, weak imageNode] in
|
|
var statusNodeHidden = false
|
|
var accessoryHidden = false
|
|
if let strongSelf = self {
|
|
statusNodeHidden = strongSelf.statusNode.isHidden
|
|
accessoryHidden = strongSelf.mediaBadgeNode.isHidden
|
|
strongSelf.statusNode.isHidden = true
|
|
strongSelf.mediaBadgeNode.isHidden = true
|
|
}
|
|
let view = imageNode?.view.snapshotView(afterScreenUpdates: false)
|
|
if let strongSelf = self {
|
|
strongSelf.statusNode.isHidden = statusNodeHidden
|
|
strongSelf.mediaBadgeNode.isHidden = accessoryHidden
|
|
}
|
|
return (view, nil)
|
|
})
|
|
}
|
|
|
|
func updateHiddenMedia() {
|
|
if let (item, _, _, _) = self.item {
|
|
if let _ = self.interaction.hiddenMedia[item.id] {
|
|
self.isHidden = true
|
|
} else {
|
|
self.isHidden = false
|
|
}
|
|
} else {
|
|
self.isHidden = false
|
|
}
|
|
self.displayLink?.isPaused = !self.hasVisibility || self.isHidden
|
|
}
|
|
}
|
|
*/
|
|
|
|
private final class VisualMediaHoleAnchor: SparseItemGrid.HoleAnchor {
|
|
let messageId: MessageId
|
|
override var id: AnyHashable {
|
|
return AnyHashable(self.messageId)
|
|
}
|
|
|
|
let indexValue: Int
|
|
override var index: Int {
|
|
return self.indexValue
|
|
}
|
|
|
|
let localMonthTimestamp: Int32
|
|
override var tag: Int32 {
|
|
return self.localMonthTimestamp
|
|
}
|
|
|
|
init(index: Int, messageId: MessageId, localMonthTimestamp: Int32) {
|
|
self.indexValue = index
|
|
self.messageId = messageId
|
|
self.localMonthTimestamp = localMonthTimestamp
|
|
}
|
|
}
|
|
|
|
private final class VisualMediaItem: SparseItemGrid.Item {
|
|
let indexValue: Int
|
|
override var index: Int {
|
|
return self.indexValue
|
|
}
|
|
let localMonthTimestamp: Int32
|
|
let message: Message
|
|
|
|
override var id: AnyHashable {
|
|
return AnyHashable(self.message.stableId)
|
|
}
|
|
|
|
override var tag: Int32 {
|
|
return self.localMonthTimestamp
|
|
}
|
|
|
|
override var holeAnchor: SparseItemGrid.HoleAnchor {
|
|
return VisualMediaHoleAnchor(index: self.index, messageId: self.message.id, localMonthTimestamp: self.localMonthTimestamp)
|
|
}
|
|
|
|
init(index: Int, message: Message, localMonthTimestamp: Int32) {
|
|
self.indexValue = index
|
|
self.message = message
|
|
self.localMonthTimestamp = localMonthTimestamp
|
|
}
|
|
}
|
|
|
|
private final class NullActionClass: NSObject, CAAction {
|
|
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
|
|
}
|
|
}
|
|
|
|
private let nullAction = NullActionClass()
|
|
|
|
private struct Month: Equatable {
|
|
var packedValue: Int32
|
|
|
|
init(packedValue: Int32) {
|
|
self.packedValue = packedValue
|
|
}
|
|
|
|
init(localTimestamp: Int32) {
|
|
var time: time_t = time_t(localTimestamp)
|
|
var timeinfo: tm = tm()
|
|
gmtime_r(&time, &timeinfo)
|
|
|
|
let year = UInt32(timeinfo.tm_year)
|
|
let month = UInt32(timeinfo.tm_mon)
|
|
|
|
self.packedValue = Int32(bitPattern: year | (month << 16))
|
|
}
|
|
|
|
var year: Int32 {
|
|
return Int32(bitPattern: (UInt32(bitPattern: self.packedValue) >> 0) & 0xffff)
|
|
}
|
|
|
|
var month: Int32 {
|
|
return Int32(bitPattern: (UInt32(bitPattern: self.packedValue) >> 16) & 0xffff)
|
|
}
|
|
}
|
|
|
|
private let durationFont = Font.regular(12.0)
|
|
private let minDurationImage: UIImage = {
|
|
let image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
|
if let image = UIImage(bundleImageName: "Chat/GridPlayIcon") {
|
|
UIGraphicsPushContext(context)
|
|
image.draw(in: CGRect(origin: CGPoint(x: (size.width - image.size.width) / 2.0, y: (size.height - image.size.height) / 2.0), size: image.size))
|
|
UIGraphicsPopContext()
|
|
}
|
|
})
|
|
return image!
|
|
}()
|
|
|
|
private final class DurationLayer: CALayer {
|
|
override init() {
|
|
super.init()
|
|
|
|
self.contentsGravity = .topRight
|
|
self.contentsScale = UIScreenScale
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func action(forKey event: String) -> CAAction? {
|
|
return nullAction
|
|
}
|
|
|
|
func update(duration: Int32, isMin: Bool) {
|
|
if isMin {
|
|
self.contents = minDurationImage.cgImage
|
|
} else {
|
|
let string = NSAttributedString(string: stringForDuration(duration), font: durationFont, textColor: .white)
|
|
let bounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
|
let textSize = CGSize(width: ceil(bounds.width), height: ceil(bounds.height))
|
|
let sideInset: CGFloat = 6.0
|
|
let verticalInset: CGFloat = 2.0
|
|
let image = generateImage(CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
|
|
context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor)
|
|
context.setBlendMode(.copy)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height)))
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height)))
|
|
context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height)))
|
|
|
|
context.setBlendMode(.normal)
|
|
UIGraphicsPushContext(context)
|
|
string.draw(in: bounds.offsetBy(dx: sideInset, dy: verticalInset))
|
|
UIGraphicsPopContext()
|
|
})
|
|
self.contents = image?.cgImage
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ItemLayer: CALayer, SparseItemGridLayer {
|
|
var item: VisualMediaItem?
|
|
var durationLayer: DurationLayer?
|
|
var minFactor: CGFloat = 1.0
|
|
var selectionLayer: GridMessageSelectionLayer?
|
|
var disposable: Disposable?
|
|
|
|
var hasContents: Bool = false
|
|
|
|
override init() {
|
|
super.init()
|
|
|
|
self.contentsGravity = .resize
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
}
|
|
|
|
override func action(forKey event: String) -> CAAction? {
|
|
return nullAction
|
|
}
|
|
|
|
func bind(item: VisualMediaItem) {
|
|
self.item = item
|
|
}
|
|
|
|
func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) {
|
|
self.minFactor = minFactor
|
|
|
|
if let duration = duration {
|
|
if let durationLayer = self.durationLayer {
|
|
durationLayer.update(duration: duration, isMin: isMin)
|
|
} else {
|
|
let durationLayer = DurationLayer()
|
|
durationLayer.update(duration: duration, isMin: isMin)
|
|
self.addSublayer(durationLayer)
|
|
durationLayer.frame = CGRect(origin: CGPoint(x: self.bounds.width - 3.0, y: self.bounds.height - 3.0), size: CGSize())
|
|
durationLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0)
|
|
self.durationLayer = durationLayer
|
|
}
|
|
} else if let durationLayer = self.durationLayer {
|
|
self.durationLayer = nil
|
|
durationLayer.removeFromSuperlayer()
|
|
}
|
|
}
|
|
|
|
func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) {
|
|
if let isSelected = isSelected {
|
|
if let selectionLayer = self.selectionLayer {
|
|
selectionLayer.updateSelected(isSelected, animated: animated)
|
|
} else {
|
|
let selectionLayer = GridMessageSelectionLayer(theme: theme)
|
|
selectionLayer.updateSelected(isSelected, animated: false)
|
|
self.selectionLayer = selectionLayer
|
|
self.addSublayer(selectionLayer)
|
|
if !self.bounds.isEmpty {
|
|
selectionLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
|
selectionLayer.updateLayout(size: self.bounds.size)
|
|
if animated {
|
|
selectionLayer.animateIn()
|
|
}
|
|
}
|
|
}
|
|
} else if let selectionLayer = self.selectionLayer {
|
|
self.selectionLayer = nil
|
|
if animated {
|
|
selectionLayer.animateOut { [weak selectionLayer] in
|
|
selectionLayer?.removeFromSuperlayer()
|
|
}
|
|
} else {
|
|
selectionLayer.removeFromSuperlayer()
|
|
}
|
|
}
|
|
}
|
|
|
|
func unbind() {
|
|
self.item = nil
|
|
}
|
|
|
|
func needsShimmer() -> Bool {
|
|
return !self.hasContents
|
|
}
|
|
|
|
func update(size: CGSize) {
|
|
/*if let durationLayer = self.durationLayer {
|
|
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize())
|
|
}*/
|
|
}
|
|
}
|
|
|
|
private final class ItemView: UIView, SparseItemGridView {
|
|
var item: VisualMediaItem?
|
|
var disposable: Disposable?
|
|
|
|
var messageItem: ListMessageItem?
|
|
var messageItemNode: ListViewItemNode?
|
|
var interaction: ListMessageItemInteraction?
|
|
let buttonNode: HighlightTrackingButtonNode
|
|
|
|
override init(frame: CGRect) {
|
|
self.buttonNode = HighlightTrackingButtonNode()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubnode(self.buttonNode)
|
|
self.buttonNode.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
|
|
|
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.messageItemNode?.setHighlighted(highlighted, at: CGPoint(), animated: !highlighted)
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
}
|
|
|
|
@objc func pressed() {
|
|
guard let itemNode = self.messageItemNode else {
|
|
return
|
|
}
|
|
|
|
if let item = self.item, let messageItem = self.messageItem, let itemNode = itemNode as? ListMessageFileItemNode {
|
|
if case let .selectable(selected) = messageItem.selection {
|
|
self.interaction?.toggleMessagesSelection([item.message.id], !selected)
|
|
} else {
|
|
itemNode.activateMedia()
|
|
}
|
|
}
|
|
}
|
|
|
|
func bind(
|
|
item: VisualMediaItem,
|
|
presentationData: ChatPresentationData,
|
|
context: AccountContext,
|
|
chatLocation: ChatLocation,
|
|
interaction: ListMessageItemInteraction,
|
|
isSelected: Bool?,
|
|
size: CGSize
|
|
) {
|
|
self.item = item
|
|
self.interaction = interaction
|
|
|
|
let messageItem = ListMessageItem(
|
|
presentationData: presentationData,
|
|
context: context,
|
|
chatLocation: chatLocation,
|
|
interaction: interaction,
|
|
message: item.message,
|
|
selection: isSelected.flatMap { isSelected in
|
|
return .selectable(selected: isSelected)
|
|
} ?? .none,
|
|
displayHeader: false
|
|
)
|
|
self.messageItem = messageItem
|
|
|
|
let messageItemNode: ListViewItemNode
|
|
if let current = self.messageItemNode {
|
|
messageItemNode = current
|
|
messageItem.updateNode(async: { f in f() }, node: { return current }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 0.0), previousItem: nil, nextItem: nil, animation: .System(duration: 0.2), completion: { layout, apply in
|
|
current.contentSize = layout.contentSize
|
|
current.insets = layout.insets
|
|
|
|
apply(ListViewItemApply(isOnScreen: true))
|
|
})
|
|
} else {
|
|
var itemNode: ListViewItemNode?
|
|
messageItem.nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 0.0), synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
|
|
itemNode = node
|
|
apply().1(ListViewItemApply(isOnScreen: true))
|
|
})
|
|
messageItemNode = itemNode!
|
|
self.messageItemNode = messageItemNode
|
|
self.buttonNode.addSubnode(messageItemNode)
|
|
}
|
|
|
|
messageItemNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
}
|
|
func unbind() {
|
|
self.item = nil
|
|
}
|
|
|
|
func needsShimmer() -> Bool {
|
|
return false
|
|
}
|
|
|
|
func update(size: CGSize) {
|
|
}
|
|
}
|
|
|
|
protocol ListShimmerLayerImageProvider: AnyObject {
|
|
func getListShimmerImage(height: CGFloat) -> UIImage
|
|
func getSeparatorColor() -> UIColor
|
|
}
|
|
|
|
private final class ListShimmerLayer: CALayer, SparseItemGridShimmerLayer {
|
|
final class OverlayLayer: CALayer {
|
|
override func action(forKey event: String) -> CAAction? {
|
|
return nullAction
|
|
}
|
|
}
|
|
|
|
let imageProvider: ListShimmerLayerImageProvider
|
|
let shimmerOverlay: OverlayLayer
|
|
let separatorLayer: OverlayLayer
|
|
|
|
private var validHeight: CGFloat?
|
|
|
|
init(imageProvider: ListShimmerLayerImageProvider) {
|
|
self.imageProvider = imageProvider
|
|
self.shimmerOverlay = OverlayLayer()
|
|
self.separatorLayer = OverlayLayer()
|
|
|
|
super.init()
|
|
|
|
self.addSublayer(self.shimmerOverlay)
|
|
self.addSublayer(self.separatorLayer)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func action(forKey event: String) -> CAAction? {
|
|
return nullAction
|
|
}
|
|
|
|
func update(size: CGSize) {
|
|
if self.validHeight != size.height {
|
|
self.validHeight = size.height
|
|
ASDisplayNodeSetResizableContents(self.shimmerOverlay, self.imageProvider.getListShimmerImage(height: size.height))
|
|
self.separatorLayer.backgroundColor = self.imageProvider.getSeparatorColor().cgColor
|
|
}
|
|
self.shimmerOverlay.frame = CGRect(origin: CGPoint(), size: size)
|
|
self.separatorLayer.frame = CGRect(origin: CGPoint(x: 65.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width - 65.0, height: UIScreenPixel))
|
|
}
|
|
}
|
|
|
|
private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimmerLayerImageProvider {
|
|
let context: AccountContext
|
|
let chatLocation: ChatLocation
|
|
let directMediaImageCache: DirectMediaImageCache
|
|
var strings: PresentationStrings
|
|
let useListItems: Bool
|
|
let listItemInteraction: ListMessageItemInteraction
|
|
let chatControllerInteraction: ChatControllerInteraction
|
|
var chatPresentationData: ChatPresentationData
|
|
var checkNodeTheme: CheckNodeTheme
|
|
|
|
var loadHoleImpl: ((SparseItemGrid.HoleAnchor, SparseItemGrid.HoleLocation) -> Signal<Never, NoError>)?
|
|
var onTapImpl: ((VisualMediaItem) -> Void)?
|
|
var onTagTapImpl: (() -> Void)?
|
|
var didScrollImpl: (() -> Void)?
|
|
var coveringInsetOffsetUpdatedImpl: ((ContainedViewLayoutTransition) -> Void)?
|
|
var onBeginFastScrollingImpl: (() -> Void)?
|
|
var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)?
|
|
var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)?
|
|
|
|
private var shimmerImages: [CGFloat: UIImage] = [:]
|
|
|
|
init(context: AccountContext, chatLocation: ChatLocation, useListItems: Bool, listItemInteraction: ListMessageItemInteraction, chatControllerInteraction: ChatControllerInteraction, directMediaImageCache: DirectMediaImageCache) {
|
|
self.context = context
|
|
self.chatLocation = chatLocation
|
|
self.useListItems = useListItems
|
|
self.listItemInteraction = listItemInteraction
|
|
self.chatControllerInteraction = chatControllerInteraction
|
|
self.directMediaImageCache = directMediaImageCache
|
|
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
self.strings = presentationData.strings
|
|
|
|
let themeData = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper)
|
|
self.chatPresentationData = ChatPresentationData(theme: themeData, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: 1.0)
|
|
|
|
self.checkNodeTheme = CheckNodeTheme(theme: presentationData.theme, style: .overlay, hasInset: true)
|
|
}
|
|
|
|
func updatePresentationData(presentationData: PresentationData) {
|
|
self.strings = presentationData.strings
|
|
|
|
let themeData = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper)
|
|
self.chatPresentationData = ChatPresentationData(theme: themeData, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: 1.0)
|
|
|
|
self.checkNodeTheme = CheckNodeTheme(theme: presentationData.theme, style: .overlay, hasInset: true)
|
|
}
|
|
|
|
func getListShimmerImage(height: CGFloat) -> UIImage {
|
|
if let image = self.shimmerImages[height] {
|
|
return image
|
|
} else {
|
|
let fakeFile = TelegramMediaFile(
|
|
fileId: MediaId(namespace: 0, id: 1),
|
|
partialReference: nil,
|
|
resource: EmptyMediaResource(),
|
|
previewRepresentations: [],
|
|
videoThumbnails: [],
|
|
immediateThumbnailData: nil,
|
|
mimeType: "image/jpeg",
|
|
size: nil,
|
|
attributes: [.FileName(fileName: "file")]
|
|
)
|
|
let fakeMessage = Message(
|
|
stableId: 1,
|
|
stableVersion: 1,
|
|
id: MessageId(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(1)), namespace: 0, id: 1),
|
|
globallyUniqueId: nil,
|
|
groupingKey: nil,
|
|
groupInfo: nil,
|
|
threadId: nil,
|
|
timestamp: 1, flags: [],
|
|
tags: [],
|
|
globalTags: [],
|
|
localTags: [],
|
|
forwardInfo: nil,
|
|
author: nil,
|
|
text: "",
|
|
attributes: [],
|
|
media: [fakeFile],
|
|
peers: SimpleDictionary<PeerId, Peer>(),
|
|
associatedMessages: SimpleDictionary<MessageId, Message>(),
|
|
associatedMessageIds: []
|
|
)
|
|
let messageItem = ListMessageItem(
|
|
presentationData: self.chatPresentationData,
|
|
context: self.context,
|
|
chatLocation: self.chatLocation,
|
|
interaction: self.listItemInteraction,
|
|
message: fakeMessage,
|
|
selection: .none,
|
|
displayHeader: false
|
|
)
|
|
|
|
var itemNode: ListViewItemNode?
|
|
messageItem.nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: 400.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 0.0), synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
|
|
itemNode = node
|
|
apply().1(ListViewItemApply(isOnScreen: true))
|
|
})
|
|
|
|
guard let fileItemNode = itemNode as? ListMessageFileItemNode else {
|
|
return UIImage()
|
|
}
|
|
|
|
let image = generateImage(CGSize(width: 320.0, height: height), rotatedContext: { size, context in
|
|
UIGraphicsPushContext(context)
|
|
|
|
context.setFillColor(self.chatPresentationData.theme.theme.list.plainBackgroundColor.cgColor)
|
|
context.fill(CGRect(origin: CGPoint(), size: size))
|
|
|
|
context.setBlendMode(.copy)
|
|
context.setFillColor(UIColor.clear.cgColor)
|
|
|
|
func fillRoundedRect(rect: CGRect, radius: CGFloat) {
|
|
UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: radius, height: radius)).fill()
|
|
}
|
|
|
|
let lineHeight: CGFloat = 8.0
|
|
let titleOrigin = CGPoint(x: fileItemNode.titleNode.frame.minX, y: fileItemNode.titleNode.frame.midY)
|
|
let dateOrigin = CGPoint(x: fileItemNode.descriptionNode.frame.minX, y: fileItemNode.descriptionNode.frame.midY)
|
|
|
|
fillRoundedRect(rect: CGRect(origin: CGPoint(x: titleOrigin.x, y: titleOrigin.y - lineHeight / 2.0), size: CGSize(width: 160.0, height: lineHeight)), radius: lineHeight / 2.0)
|
|
fillRoundedRect(rect: CGRect(origin: CGPoint(x: dateOrigin.x, y: dateOrigin.y - lineHeight / 2.0), size: CGSize(width: 220.0, height: lineHeight)), radius: lineHeight / 2.0)
|
|
|
|
fillRoundedRect(rect: fileItemNode.extensionIconNode.frame, radius: 6.0)
|
|
|
|
UIGraphicsPopContext()
|
|
})!.stretchableImage(withLeftCapWidth: 299, topCapHeight: 0)
|
|
self.shimmerImages[height] = image
|
|
return image
|
|
}
|
|
}
|
|
|
|
func getSeparatorColor() -> UIColor {
|
|
return self.chatPresentationData.theme.theme.list.itemPlainSeparatorColor
|
|
}
|
|
|
|
func createLayer() -> SparseItemGridLayer? {
|
|
if self.useListItems {
|
|
return nil
|
|
}
|
|
return ItemLayer()
|
|
}
|
|
|
|
func createView() -> SparseItemGridView? {
|
|
if !self.useListItems {
|
|
return nil
|
|
}
|
|
return ItemView()
|
|
}
|
|
|
|
func createShimmerLayer() -> SparseItemGridShimmerLayer? {
|
|
if self.useListItems {
|
|
let layer = ListShimmerLayer(imageProvider: self)
|
|
return layer
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static let widthSpecs: ([Int], [Int]) = {
|
|
let list: [(Int, Int)] = [
|
|
(50, 64),
|
|
(100, 150),
|
|
(140, 200),
|
|
(Int.max, 280)
|
|
]
|
|
return (list.map(\.0), list.map(\.1))
|
|
}()
|
|
|
|
func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridDisplayItem], synchronous: SparseItemGrid.Synchronous) {
|
|
for i in 0 ..< items.count {
|
|
guard let item = items[i] as? VisualMediaItem else {
|
|
continue
|
|
}
|
|
|
|
let displayItem = layers[i]
|
|
|
|
if self.useListItems {
|
|
guard let view = displayItem.view as? ItemView else {
|
|
continue
|
|
}
|
|
view.bind(
|
|
item: item,
|
|
presentationData: chatPresentationData,
|
|
context: self.context,
|
|
chatLocation: self.chatLocation,
|
|
interaction: self.listItemInteraction,
|
|
isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id),
|
|
size: view.bounds.size
|
|
)
|
|
} else {
|
|
guard let layer = displayItem.layer as? ItemLayer else {
|
|
continue
|
|
}
|
|
if layer.bounds.isEmpty {
|
|
continue
|
|
}
|
|
|
|
var imageWidthSpec: Int = SparseItemGridBindingImpl.widthSpecs.1[0]
|
|
for i in 0 ..< SparseItemGridBindingImpl.widthSpecs.0.count {
|
|
if Int(layer.bounds.width) <= SparseItemGridBindingImpl.widthSpecs.0[i] {
|
|
imageWidthSpec = SparseItemGridBindingImpl.widthSpecs.1[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
let message = item.message
|
|
|
|
var selectedMedia: Media?
|
|
for media in message.media {
|
|
if let image = media as? TelegramMediaImage {
|
|
selectedMedia = image
|
|
break
|
|
} else if let file = media as? TelegramMediaFile {
|
|
selectedMedia = file
|
|
break
|
|
}
|
|
}
|
|
|
|
if let selectedMedia = selectedMedia {
|
|
if let result = directMediaImageCache.getImage(message: message, media: selectedMedia, width: imageWidthSpec, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, synchronous: synchronous == .full) {
|
|
if let image = result.image {
|
|
layer.contents = image.cgImage
|
|
switch synchronous {
|
|
case .none:
|
|
layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak self, weak layer, weak displayItem] _ in
|
|
layer?.hasContents = true
|
|
if let displayItem = displayItem {
|
|
self?.updateShimmerLayersImpl?(displayItem)
|
|
}
|
|
})
|
|
default:
|
|
layer.hasContents = true
|
|
}
|
|
}
|
|
if let loadSignal = result.loadSignal {
|
|
layer.disposable?.dispose()
|
|
let startTimestamp = CFAbsoluteTimeGetCurrent()
|
|
layer.disposable = (loadSignal
|
|
|> deliverOnMainQueue).start(next: { [weak self, weak layer, weak displayItem] image in
|
|
guard let layer = layer else {
|
|
return
|
|
}
|
|
let deltaTime = CFAbsoluteTimeGetCurrent() - startTimestamp
|
|
let synchronousValue: Bool
|
|
switch synchronous {
|
|
case .none, .full:
|
|
synchronousValue = false
|
|
case .semi:
|
|
synchronousValue = deltaTime < 0.1
|
|
}
|
|
|
|
if layer.contents != nil && !synchronousValue {
|
|
let copyLayer = ItemLayer()
|
|
copyLayer.contents = layer.contents
|
|
copyLayer.contentsRect = layer.contentsRect
|
|
copyLayer.frame = layer.bounds
|
|
if let durationLayer = layer.durationLayer {
|
|
layer.insertSublayer(copyLayer, below: durationLayer)
|
|
} else {
|
|
layer.addSublayer(copyLayer)
|
|
}
|
|
copyLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyLayer] _ in
|
|
copyLayer?.removeFromSuperlayer()
|
|
})
|
|
|
|
layer.contents = image?.cgImage
|
|
|
|
layer.hasContents = true
|
|
if let displayItem = displayItem {
|
|
self?.updateShimmerLayersImpl?(displayItem)
|
|
}
|
|
} else {
|
|
layer.contents = image?.cgImage
|
|
|
|
if !synchronousValue {
|
|
layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { [weak layer] _ in
|
|
layer?.hasContents = true
|
|
if let displayItem = displayItem {
|
|
self?.updateShimmerLayersImpl?(displayItem)
|
|
}
|
|
})
|
|
} else {
|
|
layer.hasContents = true
|
|
if let displayItem = displayItem {
|
|
self?.updateShimmerLayersImpl?(displayItem)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var duration: Int32?
|
|
var isMin: Bool = false
|
|
if let file = selectedMedia as? TelegramMediaFile, !file.isAnimated {
|
|
duration = file.duration
|
|
isMin = layer.bounds.width < 80.0
|
|
}
|
|
layer.updateDuration(duration: duration, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0))
|
|
}
|
|
|
|
if let selectionState = self.chatControllerInteraction.selectionState {
|
|
layer.updateSelection(theme: self.checkNodeTheme, isSelected: selectionState.selectedIds.contains(message.id), animated: false)
|
|
} else {
|
|
layer.updateSelection(theme: self.checkNodeTheme, isSelected: nil, animated: false)
|
|
}
|
|
|
|
layer.bind(item: item)
|
|
}
|
|
}
|
|
}
|
|
|
|
func unbindLayer(layer: SparseItemGridLayer) {
|
|
guard let layer = layer as? ItemLayer else {
|
|
return
|
|
}
|
|
layer.unbind()
|
|
}
|
|
|
|
func scrollerTextForTag(tag: Int32) -> String? {
|
|
let month = Month(packedValue: tag)
|
|
return stringForMonth(strings: self.strings, month: month.month, ofYear: month.year)
|
|
}
|
|
|
|
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> {
|
|
if let loadHoleImpl = self.loadHoleImpl {
|
|
return loadHoleImpl(anchor, location)
|
|
} else {
|
|
return .never()
|
|
}
|
|
}
|
|
|
|
func onTap(item: SparseItemGrid.Item) {
|
|
guard let item = item as? VisualMediaItem else {
|
|
return
|
|
}
|
|
self.onTapImpl?(item)
|
|
}
|
|
|
|
func onTagTap() {
|
|
self.onTagTapImpl?()
|
|
}
|
|
|
|
func didScroll() {
|
|
self.didScrollImpl?()
|
|
}
|
|
|
|
func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition) {
|
|
self.coveringInsetOffsetUpdatedImpl?(transition)
|
|
}
|
|
|
|
func onBeginFastScrolling() {
|
|
self.onBeginFastScrollingImpl?()
|
|
}
|
|
|
|
func getShimmerColors() -> SparseItemGrid.ShimmerColors {
|
|
if let getShimmerColorsImpl = self.getShimmerColorsImpl {
|
|
return getShimmerColorsImpl()
|
|
} else {
|
|
return SparseItemGrid.ShimmerColors(background: 0xffffff, foreground: 0xffffff)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func tagMaskForType(_ type: PeerInfoVisualMediaPaneNode.ContentType) -> MessageTags {
|
|
switch type {
|
|
case .photoOrVideo:
|
|
return .photoOrVideo
|
|
case .photo:
|
|
return .photo
|
|
case .video:
|
|
return .video
|
|
case .gifs:
|
|
return .gif
|
|
case .files:
|
|
return .file
|
|
case .voiceAndVideoMessages:
|
|
return .voiceOrInstantVideo
|
|
case .music:
|
|
return .music
|
|
}
|
|
}
|
|
|
|
final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
|
|
enum ContentType {
|
|
case photoOrVideo
|
|
case photo
|
|
case video
|
|
case gifs
|
|
case files
|
|
case voiceAndVideoMessages
|
|
case music
|
|
}
|
|
|
|
struct ZoomLevel {
|
|
fileprivate var value: SparseItemGrid.ZoomLevel
|
|
|
|
init(_ value: SparseItemGrid.ZoomLevel) {
|
|
self.value = value
|
|
}
|
|
|
|
var rawValue: Int32 {
|
|
return Int32(self.value.rawValue)
|
|
}
|
|
|
|
public init(rawValue: Int32) {
|
|
self.value = SparseItemGrid.ZoomLevel(rawValue: Int(rawValue))
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let peerId: PeerId
|
|
private let chatControllerInteraction: ChatControllerInteraction
|
|
private(set) var contentType: ContentType
|
|
private var contentTypePromise: ValuePromise<ContentType>
|
|
|
|
weak var parentController: ViewController?
|
|
|
|
private let contextGestureContainerNode: ContextControllerSourceNode
|
|
private let itemGrid: SparseItemGrid
|
|
private let itemGridBinding: SparseItemGridBindingImpl
|
|
private let directMediaImageCache: DirectMediaImageCache
|
|
private var items: SparseItemGrid.Items?
|
|
private var didUpdateItemsOnce: Bool = false
|
|
|
|
private var isDeceleratingAfterTracking = false
|
|
|
|
private var _itemInteraction: VisualMediaItemInteraction?
|
|
private var itemInteraction: VisualMediaItemInteraction {
|
|
return self._itemInteraction!
|
|
}
|
|
|
|
private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
|
|
|
|
private let ready = Promise<Bool>()
|
|
private var didSetReady: Bool = false
|
|
var isReady: Signal<Bool, NoError> {
|
|
return self.ready.get()
|
|
}
|
|
|
|
private let statusPromise = Promise<PeerInfoStatusData?>(nil)
|
|
var status: Signal<PeerInfoStatusData?, NoError> {
|
|
self.statusPromise.get()
|
|
}
|
|
|
|
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
|
var tabBarOffset: CGFloat {
|
|
return self.itemGrid.coveringInsetOffset
|
|
}
|
|
|
|
private let listDisposable = MetaDisposable()
|
|
private var hiddenMediaDisposable: Disposable?
|
|
|
|
private var numberOfItemsToRequest: Int = 50
|
|
private var isRequestingView: Bool = false
|
|
private var isFirstHistoryView: Bool = true
|
|
|
|
private var decelerationAnimator: ConstantDisplayLinkAnimator?
|
|
|
|
private var animationTimer: SwiftSignalKit.Timer?
|
|
|
|
private(set) var calendarSource: SparseMessageCalendar?
|
|
private var listSource: SparseMessageList
|
|
|
|
var openCurrentDate: (() -> Void)?
|
|
var paneDidScroll: (() -> Void)?
|
|
|
|
private let stateTag: MessageTags
|
|
private var storedStateDisposable: Disposable?
|
|
|
|
private weak var currentGestureItem: SparseItemGridDisplayItem?
|
|
|
|
private var presentationData: PresentationData
|
|
private var presentationDataDisposable: Disposable?
|
|
|
|
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, contentType: ContentType) {
|
|
self.context = context
|
|
self.peerId = peerId
|
|
self.chatControllerInteraction = chatControllerInteraction
|
|
self.contentType = contentType
|
|
self.contentTypePromise = ValuePromise<ContentType>(contentType)
|
|
self.stateTag = tagMaskForType(contentType)
|
|
|
|
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
self.contextGestureContainerNode = ContextControllerSourceNode()
|
|
self.itemGrid = SparseItemGrid(theme: self.presentationData.theme)
|
|
self.directMediaImageCache = DirectMediaImageCache(account: context.account)
|
|
|
|
let useListItems: Bool
|
|
switch contentType {
|
|
case .files, .voiceAndVideoMessages, .music:
|
|
useListItems = true
|
|
default:
|
|
useListItems = false
|
|
}
|
|
|
|
let listItemInteraction = ListMessageItemInteraction(
|
|
openMessage: { message, mode in
|
|
return chatControllerInteraction.openMessage(message, mode)
|
|
},
|
|
openMessageContextMenu: { message, bool, node, rect, gesture in
|
|
chatControllerInteraction.openMessageContextMenu(message, bool, node, rect, gesture)
|
|
},
|
|
toggleMessagesSelection: { messageId, selected in
|
|
chatControllerInteraction.toggleMessagesSelection(messageId, selected)
|
|
},
|
|
openUrl: { url, param1, param2, message in
|
|
chatControllerInteraction.openUrl(url, param1, param2, message)
|
|
},
|
|
openInstantPage: { message, data in
|
|
chatControllerInteraction.openInstantPage(message, data)
|
|
},
|
|
longTap: { action, message in
|
|
chatControllerInteraction.longTap(action, message)
|
|
},
|
|
getHiddenMedia: {
|
|
return chatControllerInteraction.hiddenMedia
|
|
}
|
|
)
|
|
|
|
self.itemGridBinding = SparseItemGridBindingImpl(
|
|
context: context,
|
|
chatLocation: .peer(peerId),
|
|
useListItems: useListItems,
|
|
listItemInteraction: listItemInteraction,
|
|
chatControllerInteraction: chatControllerInteraction,
|
|
directMediaImageCache: self.directMediaImageCache
|
|
)
|
|
|
|
self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, tag: tagMaskForType(self.contentType))
|
|
switch contentType {
|
|
case .photoOrVideo, .photo, .video:
|
|
self.calendarSource = self.context.engine.messages.sparseMessageCalendar(peerId: self.peerId, tag: tagMaskForType(self.contentType))
|
|
default:
|
|
self.calendarSource = nil
|
|
}
|
|
|
|
super.init()
|
|
|
|
let _ = (ApplicationSpecificNotice.getSharedMediaScrollingTooltip(accountManager: context.sharedContext.accountManager)
|
|
|> deliverOnMainQueue).start(next: { [weak self] count in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if count < 1 {
|
|
strongSelf.itemGrid.updateScrollingAreaTooltip(tooltip: SparseItemGridScrollingArea.DisplayTooltip(animation: "anim_infotip", text: strongSelf.itemGridBinding.chatPresentationData.strings.SharedMedia_FastScrollTooltip, completed: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let _ = ApplicationSpecificNotice.incrementSharedMediaScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager, count: 1).start()
|
|
}))
|
|
}
|
|
})
|
|
|
|
self.itemGridBinding.loadHoleImpl = { [weak self] hole, location in
|
|
guard let strongSelf = self else {
|
|
return .never()
|
|
}
|
|
return strongSelf.loadHole(anchor: hole, at: location)
|
|
}
|
|
|
|
self.itemGridBinding.onTapImpl = { [weak self] item in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if let selectionState = strongSelf.chatControllerInteraction.selectionState {
|
|
var toggledValue = true
|
|
if selectionState.selectedIds.contains(item.message.id) {
|
|
toggledValue = false
|
|
}
|
|
strongSelf.chatControllerInteraction.toggleMessagesSelection([item.message.id], toggledValue)
|
|
} else {
|
|
let _ = strongSelf.chatControllerInteraction.openMessage(item.message, .default)
|
|
}
|
|
}
|
|
|
|
self.itemGridBinding.onTagTapImpl = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.openCurrentDate?()
|
|
}
|
|
|
|
self.itemGridBinding.didScrollImpl = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.paneDidScroll?()
|
|
|
|
strongSelf.cancelPreviewGestures()
|
|
}
|
|
|
|
self.itemGridBinding.coveringInsetOffsetUpdatedImpl = { [weak self] transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.tabBarOffsetUpdated?(transition)
|
|
}
|
|
|
|
var processedOnBeginFastScrolling = false
|
|
self.itemGridBinding.onBeginFastScrollingImpl = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if processedOnBeginFastScrolling {
|
|
return
|
|
}
|
|
processedOnBeginFastScrolling = true
|
|
|
|
let _ = (ApplicationSpecificNotice.getSharedMediaFastScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager)
|
|
|> deliverOnMainQueue).start(next: { count in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if count < 1 {
|
|
let _ = ApplicationSpecificNotice.incrementSharedMediaFastScrollingTooltip(accountManager: strongSelf.context.sharedContext.accountManager).start()
|
|
|
|
var currentNode: ASDisplayNode = strongSelf
|
|
var result: PeerInfoScreenNode?
|
|
while true {
|
|
if let currentNode = currentNode as? PeerInfoScreenNode {
|
|
result = currentNode
|
|
break
|
|
} else if let supernode = currentNode.supernode {
|
|
currentNode = supernode
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if let result = result {
|
|
result.displaySharedMediaFastScrollingTooltip()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
self.itemGridBinding.getShimmerColorsImpl = { [weak self] in
|
|
guard let strongSelf = self, let presentationData = strongSelf.currentParams?.presentationData else {
|
|
return SparseItemGrid.ShimmerColors(background: 0xffffff, foreground: 0xffffff)
|
|
}
|
|
|
|
let backgroundColor = presentationData.theme.list.mediaPlaceholderColor
|
|
let foregroundColor = presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6)
|
|
|
|
return SparseItemGrid.ShimmerColors(background: backgroundColor.argb, foreground: foregroundColor.argb)
|
|
}
|
|
|
|
self.itemGridBinding.updateShimmerLayersImpl = { [weak self] layer in
|
|
self?.itemGrid.updateShimmerLayers(item: layer)
|
|
}
|
|
|
|
self.itemGrid.cancelExternalContentGestures = { [weak self] in
|
|
self?.contextGestureContainerNode.cancelGesture()
|
|
}
|
|
|
|
self.itemGrid.zoomLevelUpdated = { [weak self] zoomLevel in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let _ = updateVisualMediaStoredState(postbox: strongSelf.context.account.postbox, peerId: strongSelf.peerId, messageTag: strongSelf.stateTag, state: VisualMediaStoredState(zoomLevel: Int32(zoomLevel.rawValue))).start()
|
|
}
|
|
|
|
self._itemInteraction = VisualMediaItemInteraction(
|
|
openMessage: { [weak self] message in
|
|
let _ = self?.chatControllerInteraction.openMessage(message, .default)
|
|
},
|
|
openMessageContextActions: { [weak self] message, sourceNode, sourceRect, gesture in
|
|
self?.chatControllerInteraction.openMessageContextActions(message, sourceNode, sourceRect, gesture)
|
|
},
|
|
toggleSelection: { [weak self] id, value in
|
|
self?.chatControllerInteraction.toggleMessagesSelection([id], value)
|
|
}
|
|
)
|
|
self.itemInteraction.selectedMessageIds = chatControllerInteraction.selectionState.flatMap { $0.selectedIds }
|
|
|
|
self.contextGestureContainerNode.isGestureEnabled = !useListItems
|
|
self.contextGestureContainerNode.addSubnode(self.itemGrid)
|
|
self.addSubnode(self.contextGestureContainerNode)
|
|
|
|
self.contextGestureContainerNode.shouldBegin = { [weak self] point in
|
|
guard let strongSelf = self else {
|
|
return false
|
|
}
|
|
guard let item = strongSelf.itemGrid.item(at: point) else {
|
|
return false
|
|
}
|
|
|
|
if let result = strongSelf.view.hitTest(point, with: nil) {
|
|
if result.asyncdisplaykit_node is SparseItemGridScrollingArea {
|
|
return false
|
|
}
|
|
}
|
|
|
|
strongSelf.currentGestureItem = item
|
|
|
|
return true
|
|
}
|
|
|
|
self.contextGestureContainerNode.customActivationProgress = { [weak self] progress, update in
|
|
guard let strongSelf = self, let currentGestureItem = strongSelf.currentGestureItem else {
|
|
return
|
|
}
|
|
guard let itemLayer = currentGestureItem.layer 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.contextGestureContainerNode.activated = { [weak self] gesture, _ in
|
|
guard let strongSelf = self, let currentGestureItem = strongSelf.currentGestureItem else {
|
|
return
|
|
}
|
|
strongSelf.currentGestureItem = nil
|
|
|
|
guard let itemLayer = currentGestureItem.layer as? ItemLayer else {
|
|
return
|
|
}
|
|
guard let message = itemLayer.item?.message else {
|
|
return
|
|
}
|
|
let rect = strongSelf.itemGrid.frameForItem(layer: itemLayer)
|
|
|
|
/*let proxyNode = ASDisplayNode()
|
|
proxyNode.frame = rect
|
|
proxyNode.contents = itemLayer.contents
|
|
proxyNode.isHidden = true
|
|
strongSelf.addSubnode(proxyNode)
|
|
|
|
let escapeNotification = EscapeNotification {
|
|
proxyNode.removeFromSupernode()
|
|
}
|
|
|
|
Queue.mainQueue().after(1.0, {
|
|
escapeNotification.keep()
|
|
})*/
|
|
|
|
strongSelf.chatControllerInteraction.openMessageContextActions(message, strongSelf, rect, gesture)
|
|
|
|
strongSelf.itemGrid.cancelGestures()
|
|
}
|
|
|
|
self.storedStateDisposable = (visualMediaStoredState(postbox: context.account.postbox, peerId: peerId, messageTag: self.stateTag)
|
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if let value = value {
|
|
strongSelf.updateZoomLevel(level: ZoomLevel(rawValue: value.zoomLevel))
|
|
}
|
|
strongSelf.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false)
|
|
})
|
|
|
|
self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().start(next: { [weak self] ids in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
var hiddenMedia: [MessageId: [Media]] = [:]
|
|
for id in ids {
|
|
if case let .chat(accountId, messageId, media) = id, accountId == strongSelf.context.account.id {
|
|
hiddenMedia[messageId] = [media]
|
|
}
|
|
}
|
|
strongSelf.itemInteraction.hiddenMedia = hiddenMedia
|
|
|
|
if let items = strongSelf.items {
|
|
for item in items.items {
|
|
if let item = item as? VisualMediaItem {
|
|
if hiddenMedia[item.message.id] != nil {
|
|
strongSelf.itemGrid.ensureItemVisible(index: item.index)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
strongSelf.updateHiddenMedia()
|
|
})
|
|
|
|
/*let animationTimer = SwiftSignalKit.Timer(timeout: 0.3, repeat: true, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
for (_, itemNode) in strongSelf.visibleMediaItems {
|
|
itemNode.tick()
|
|
}
|
|
}, queue: .mainQueue())
|
|
self.animationTimer = animationTimer
|
|
animationTimer.start()*/
|
|
|
|
self.statusPromise.set((self.contentTypePromise.get()
|
|
|> distinctUntilChanged
|
|
|> mapToSignal { contentType -> Signal<(ContentType, [MessageTags: Int32]), NoError> in
|
|
var summaries: [MessageTags] = []
|
|
switch contentType {
|
|
case .photoOrVideo:
|
|
summaries.append(.photo)
|
|
summaries.append(.video)
|
|
case .photo:
|
|
summaries.append(.photo)
|
|
case .video:
|
|
summaries.append(.video)
|
|
case .gifs:
|
|
summaries.append(.gif)
|
|
case .files:
|
|
summaries.append(.file)
|
|
case .voiceAndVideoMessages:
|
|
summaries.append(.voiceOrInstantVideo)
|
|
case .music:
|
|
summaries.append(.music)
|
|
}
|
|
return context.account.postbox.combinedView(keys: summaries.map { tag in
|
|
return PostboxViewKey.historyTagSummaryView(tag: tag, peerId: peerId, namespace: Namespaces.Message.Cloud)
|
|
})
|
|
|> map { views -> (ContentType, [MessageTags: Int32]) in
|
|
switch contentType {
|
|
case .photoOrVideo:
|
|
summaries.append(.photo)
|
|
summaries.append(.video)
|
|
case .photo:
|
|
summaries.append(.photo)
|
|
case .video:
|
|
summaries.append(.video)
|
|
case .gifs:
|
|
summaries.append(.gif)
|
|
case .files:
|
|
summaries.append(.file)
|
|
case .voiceAndVideoMessages:
|
|
summaries.append(.voiceOrInstantVideo)
|
|
case .music:
|
|
summaries.append(.music)
|
|
}
|
|
var result: [MessageTags: Int32] = [:]
|
|
for tag in summaries {
|
|
if let view = views.views[PostboxViewKey.historyTagSummaryView(tag: tag, peerId: peerId, namespace: Namespaces.Message.Cloud)] as? MessageHistoryTagSummaryView {
|
|
result[tag] = view.count ?? 0
|
|
} else {
|
|
result[tag] = 0
|
|
}
|
|
}
|
|
return (contentType, result)
|
|
}
|
|
}
|
|
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
|
if lhs.0 != rhs.0 {
|
|
return false
|
|
}
|
|
if lhs.1 != rhs.1 {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|> map { contentType, dict -> PeerInfoStatusData? in
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
switch contentType {
|
|
case .photoOrVideo:
|
|
let photoCount: Int32 = dict[.photo] ?? 0
|
|
let videoCount: Int32 = dict[.video] ?? 0
|
|
|
|
if photoCount != 0 && videoCount != 0 {
|
|
return PeerInfoStatusData(text: "\(presentationData.strings.SharedMedia_PhotoCount(Int32(photoCount))), \(presentationData.strings.SharedMedia_VideoCount(Int32(videoCount)))", isActivity: false)
|
|
} else if photoCount != 0 {
|
|
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_PhotoCount(Int32(photoCount)), isActivity: false)
|
|
} else if videoCount != 0 {
|
|
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_VideoCount(Int32(videoCount)), isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
case .photo:
|
|
let photoCount: Int32 = dict[.photo] ?? 0
|
|
|
|
if photoCount != 0 {
|
|
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_PhotoCount(Int32(photoCount)), isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
case .video:
|
|
let videoCount: Int32 = dict[.video] ?? 0
|
|
|
|
if videoCount != 0 {
|
|
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_VideoCount(Int32(videoCount)), isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
case .gifs:
|
|
let gifCount: Int32 = dict[.gif] ?? 0
|
|
|
|
if gifCount != 0 {
|
|
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_GifCount(Int32(gifCount)), isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
case .files:
|
|
let fileCount: Int32 = dict[.file] ?? 0
|
|
|
|
if fileCount != 0 {
|
|
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_FileCount(Int32(fileCount)), isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
case .voiceAndVideoMessages:
|
|
let itemCount: Int32 = dict[.voiceOrInstantVideo] ?? 0
|
|
|
|
if itemCount != 0 {
|
|
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_VoiceMessageCount(Int32(itemCount)), isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
case .music:
|
|
let itemCount: Int32 = dict[.music] ?? 0
|
|
|
|
if itemCount != 0 {
|
|
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_MusicCount(Int32(itemCount)), isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}))
|
|
|
|
self.presentationDataDisposable = (self.context.sharedContext.presentationData
|
|
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.itemGridBinding.updatePresentationData(presentationData: presentationData)
|
|
|
|
strongSelf.itemGrid.updatePresentationData(theme: presentationData.theme)
|
|
|
|
strongSelf.itemGrid.forEachVisibleItem { item in
|
|
guard let itemView = item.view as? ItemView else {
|
|
return
|
|
}
|
|
if let item = itemView.item {
|
|
itemView.bind(
|
|
item: item,
|
|
presentationData: strongSelf.itemGridBinding.chatPresentationData,
|
|
context: strongSelf.itemGridBinding.context,
|
|
chatLocation: strongSelf.itemGridBinding.chatLocation,
|
|
interaction: strongSelf.itemGridBinding.listItemInteraction,
|
|
isSelected: strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id),
|
|
size: itemView.bounds.size
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.listDisposable.dispose()
|
|
self.hiddenMediaDisposable?.dispose()
|
|
self.animationTimer?.invalidate()
|
|
self.presentationDataDisposable?.dispose()
|
|
}
|
|
|
|
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> {
|
|
guard let anchor = anchor as? VisualMediaHoleAnchor else {
|
|
return .never()
|
|
}
|
|
let mappedDirection: SparseMessageList.LoadHoleDirection
|
|
switch location {
|
|
case .around:
|
|
mappedDirection = .around
|
|
case .toLower:
|
|
mappedDirection = .later
|
|
case .toUpper:
|
|
mappedDirection = .earlier
|
|
}
|
|
let listSource = self.listSource
|
|
return Signal { subscriber in
|
|
listSource.loadHole(anchor: anchor.messageId, direction: mappedDirection, completion: {
|
|
subscriber.putCompletion()
|
|
})
|
|
|
|
return EmptyDisposable
|
|
}
|
|
}
|
|
|
|
func updateContentType(contentType: ContentType) {
|
|
if self.contentType == contentType {
|
|
return
|
|
}
|
|
self.contentType = contentType
|
|
self.contentTypePromise.set(contentType)
|
|
|
|
self.itemGrid.hideScrollingArea()
|
|
|
|
self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, tag: tagMaskForType(self.contentType))
|
|
self.isRequestingView = false
|
|
self.requestHistoryAroundVisiblePosition(synchronous: true, reloadAtTop: true)
|
|
}
|
|
|
|
func updateZoomLevel(level: ZoomLevel) {
|
|
self.itemGrid.setZoomLevel(level: level.value)
|
|
|
|
let _ = updateVisualMediaStoredState(postbox: self.context.account.postbox, peerId: self.peerId, messageTag: self.stateTag, state: VisualMediaStoredState(zoomLevel: level.rawValue)).start()
|
|
}
|
|
|
|
func ensureMessageIsVisible(id: MessageId) {
|
|
}
|
|
|
|
private func requestHistoryAroundVisiblePosition(synchronous: Bool, reloadAtTop: Bool) {
|
|
if self.isRequestingView {
|
|
return
|
|
}
|
|
self.isRequestingView = true
|
|
var firstTime = true
|
|
let queue = Queue()
|
|
|
|
self.listDisposable.set((self.listSource.state
|
|
|> deliverOn(queue)).start(next: { [weak self] list in
|
|
let timezoneOffset = Int32(TimeZone.current.secondsFromGMT())
|
|
|
|
var mappedItems: [SparseItemGrid.Item] = []
|
|
var mappedHoles: [SparseItemGrid.HoleAnchor] = []
|
|
for item in list.items {
|
|
switch item.content {
|
|
case let .message(message, isLocal):
|
|
mappedItems.append(VisualMediaItem(index: item.index, message: message, localMonthTimestamp: Month(localTimestamp: message.timestamp + timezoneOffset).packedValue))
|
|
if !isLocal {
|
|
mappedHoles.append(VisualMediaHoleAnchor(index: item.index, messageId: message.id, localMonthTimestamp: Month(localTimestamp: message.timestamp + timezoneOffset).packedValue))
|
|
}
|
|
case let .placeholder(id, timestamp):
|
|
mappedHoles.append(VisualMediaHoleAnchor(index: item.index, messageId: id, localMonthTimestamp: Month(localTimestamp: timestamp + timezoneOffset).packedValue))
|
|
}
|
|
}
|
|
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let items = SparseItemGrid.Items(
|
|
items: mappedItems,
|
|
holeAnchors: mappedHoles,
|
|
count: list.totalCount,
|
|
itemBinding: strongSelf.itemGridBinding
|
|
)
|
|
|
|
let currentSynchronous = synchronous && firstTime
|
|
let currentReloadAtTop = reloadAtTop && firstTime
|
|
firstTime = false
|
|
strongSelf.updateHistory(items: items, synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop)
|
|
strongSelf.isRequestingView = false
|
|
}
|
|
}))
|
|
}
|
|
|
|
private func updateHistory(items: SparseItemGrid.Items, synchronous: Bool, reloadAtTop: Bool) {
|
|
self.items = items
|
|
|
|
if let (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams {
|
|
var gridSnapshot: UIView?
|
|
if reloadAtTop {
|
|
gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false)
|
|
}
|
|
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate)
|
|
if let gridSnapshot = gridSnapshot {
|
|
self.view.addSubview(gridSnapshot)
|
|
gridSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak gridSnapshot] _ in
|
|
gridSnapshot?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
if !self.didSetReady {
|
|
self.didSetReady = true
|
|
self.ready.set(.single(true))
|
|
}
|
|
}
|
|
|
|
func scrollToTop() -> Bool {
|
|
return self.itemGrid.scrollToTop()
|
|
}
|
|
|
|
func hitTestResultForScrolling() -> UIView? {
|
|
return self.itemGrid.hitTestResultForScrolling()
|
|
}
|
|
|
|
func brieflyDisableTouchActions() {
|
|
self.itemGrid.brieflyDisableTouchActions()
|
|
}
|
|
|
|
func findLoadedMessage(id: MessageId) -> Message? {
|
|
guard let items = self.items else {
|
|
return nil
|
|
}
|
|
for item in items.items {
|
|
guard let item = item as? VisualMediaItem else {
|
|
continue
|
|
}
|
|
if item.message.id == id {
|
|
return item.message
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func updateHiddenMedia() {
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemLayer = item.layer as? ItemLayer else {
|
|
return
|
|
}
|
|
if let item = itemLayer.item {
|
|
if self.itemInteraction.hiddenMedia[item.message.id] != nil {
|
|
itemLayer.isHidden = true
|
|
} else {
|
|
itemLayer.isHidden = false
|
|
}
|
|
} else {
|
|
itemLayer.isHidden = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func transferVelocity(_ velocity: CGFloat) {
|
|
self.itemGrid.transferVelocity(velocity)
|
|
}
|
|
|
|
func cancelPreviewGestures() {
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemView = item.view as? ItemView else {
|
|
return
|
|
}
|
|
if let messageItemNode = itemView.messageItemNode as? ListMessageFileItemNode {
|
|
messageItemNode.cancelPreviewGesture()
|
|
}
|
|
}
|
|
}
|
|
|
|
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
var foundItemLayer: SparseItemGridLayer?
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemLayer = item.layer as? ItemLayer else {
|
|
return
|
|
}
|
|
if let item = itemLayer.item, item.message.id == messageId {
|
|
foundItemLayer = itemLayer
|
|
}
|
|
}
|
|
if let itemLayer = foundItemLayer {
|
|
let itemFrame = self.view.convert(self.itemGrid.frameForItem(layer: itemLayer), from: self.itemGrid.view)
|
|
let proxyNode = ASDisplayNode()
|
|
proxyNode.frame = itemFrame
|
|
proxyNode.contents = itemLayer.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)
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addToTransitionSurface(view: UIView) {
|
|
self.itemGrid.addToTransitionSurface(view: view)
|
|
}
|
|
|
|
func updateSelectedMessages(animated: Bool) {
|
|
switch self.contentType {
|
|
case .files, .music, .voiceAndVideoMessages:
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemView = item.view as? ItemView else {
|
|
return
|
|
}
|
|
if let item = itemView.item {
|
|
itemView.bind(
|
|
item: item,
|
|
presentationData: self.itemGridBinding.chatPresentationData,
|
|
context: self.itemGridBinding.context,
|
|
chatLocation: self.itemGridBinding.chatLocation,
|
|
interaction: self.itemGridBinding.listItemInteraction,
|
|
isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id),
|
|
size: itemView.bounds.size
|
|
)
|
|
}
|
|
}
|
|
case .photo, .video, .photoOrVideo, .gifs:
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else {
|
|
return
|
|
}
|
|
itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.chatControllerInteraction.selectionState?.selectedIds.contains(item.message.id), animated: animated)
|
|
}
|
|
|
|
self.itemGrid.pinchEnabled = self.chatControllerInteraction.selectionState == nil
|
|
}
|
|
}
|
|
|
|
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
|
self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
|
|
|
transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
|
|
|
transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
|
|
if let items = self.items {
|
|
let wasFirstTime = !self.didUpdateItemsOnce
|
|
self.didUpdateItemsOnce = true
|
|
let fixedItemHeight: CGFloat?
|
|
switch self.contentType {
|
|
case .files, .music, .voiceAndVideoMessages:
|
|
let fakeFile = TelegramMediaFile(
|
|
fileId: MediaId(namespace: 0, id: 1),
|
|
partialReference: nil,
|
|
resource: EmptyMediaResource(),
|
|
previewRepresentations: [],
|
|
videoThumbnails: [],
|
|
immediateThumbnailData: nil,
|
|
mimeType: "image/jpeg",
|
|
size: nil,
|
|
attributes: [.FileName(fileName: "file")]
|
|
)
|
|
let fakeMessage = Message(
|
|
stableId: 1,
|
|
stableVersion: 1,
|
|
id: MessageId(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(1)), namespace: 0, id: 1),
|
|
globallyUniqueId: nil,
|
|
groupingKey: nil,
|
|
groupInfo: nil,
|
|
threadId: nil,
|
|
timestamp: 1, flags: [],
|
|
tags: [],
|
|
globalTags: [],
|
|
localTags: [],
|
|
forwardInfo: nil,
|
|
author: nil,
|
|
text: "",
|
|
attributes: [],
|
|
media: [fakeFile],
|
|
peers: SimpleDictionary<PeerId, Peer>(),
|
|
associatedMessages: SimpleDictionary<MessageId, Message>(),
|
|
associatedMessageIds: []
|
|
)
|
|
let messageItem = ListMessageItem(
|
|
presentationData: self.itemGridBinding.chatPresentationData,
|
|
context: self.itemGridBinding.context,
|
|
chatLocation: self.itemGridBinding.chatLocation,
|
|
interaction: self.itemGridBinding.listItemInteraction,
|
|
message: fakeMessage,
|
|
selection: .none,
|
|
displayHeader: false
|
|
)
|
|
|
|
var itemNode: ListViewItemNode?
|
|
messageItem.nodeConfiguredForParams(async: { f in f() }, params: ListViewItemLayoutParams(width: size.width, leftInset: 0.0, rightInset: 0.0, availableHeight: 0.0), synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
|
|
itemNode = node
|
|
apply().1(ListViewItemApply(isOnScreen: true))
|
|
})
|
|
|
|
if let itemNode = itemNode {
|
|
fixedItemHeight = itemNode.contentSize.height
|
|
} else {
|
|
preconditionFailure()
|
|
}
|
|
default:
|
|
fixedItemHeight = nil
|
|
}
|
|
|
|
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none)
|
|
}
|
|
}
|
|
|
|
func currentTopTimestamp() -> Int32? {
|
|
var timestamp: Int32?
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemLayer = item.layer as? ItemLayer else {
|
|
return
|
|
}
|
|
if let item = itemLayer.item {
|
|
if let timestampValue = timestamp {
|
|
timestamp = max(timestampValue, item.message.timestamp)
|
|
} else {
|
|
timestamp = item.message.timestamp
|
|
}
|
|
}
|
|
}
|
|
return timestamp
|
|
}
|
|
|
|
func scrollToTimestamp(timestamp: Int32) {
|
|
if let items = self.items, !items.items.isEmpty {
|
|
var previousIndex: Int?
|
|
for item in items.items {
|
|
guard let item = item as? VisualMediaItem else {
|
|
continue
|
|
}
|
|
if item.message.timestamp <= timestamp {
|
|
break
|
|
}
|
|
previousIndex = item.index
|
|
}
|
|
if previousIndex == nil {
|
|
previousIndex = (items.items[0] as? VisualMediaItem)?.index
|
|
}
|
|
if let index = previousIndex {
|
|
self.itemGrid.scrollToItem(at: index)
|
|
|
|
if let item = self.itemGrid.item(at: index) {
|
|
if let layer = item.layer as? ItemLayer {
|
|
Queue.mainQueue().after(0.1, { [weak layer] in
|
|
guard let layer = layer else {
|
|
return
|
|
}
|
|
|
|
let overlayLayer = ListShimmerLayer.OverlayLayer()
|
|
overlayLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.6).cgColor
|
|
overlayLayer.frame = layer.bounds
|
|
layer.addSublayer(overlayLayer)
|
|
overlayLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.8, delay: 0.3, removeOnCompletion: false, completion: { [weak overlayLayer] _ in
|
|
overlayLayer?.removeFromSuperlayer()
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func scrollToItem(index: Int) {
|
|
guard let _ = self.items else {
|
|
return
|
|
}
|
|
self.itemGrid.scrollToItem(at: index)
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
guard let result = super.hitTest(point, with: event) else {
|
|
return nil
|
|
}
|
|
/*if self.decelerationAnimator != nil {
|
|
self.decelerationAnimator?.isPaused = true
|
|
self.decelerationAnimator = nil
|
|
|
|
return self.scrollNode.view
|
|
}*/
|
|
return result
|
|
}
|
|
|
|
func availableZoomLevels() -> (decrement: ZoomLevel?, increment: ZoomLevel?) {
|
|
let levels = self.itemGrid.availableZoomLevels()
|
|
return (levels.decrement.flatMap(ZoomLevel.init), levels.increment.flatMap(ZoomLevel.init))
|
|
}
|
|
}
|
|
|
|
final class VisualMediaStoredState: Codable {
|
|
let zoomLevel: Int32
|
|
|
|
public init(zoomLevel: Int32) {
|
|
self.zoomLevel = zoomLevel
|
|
}
|
|
}
|
|
|
|
func visualMediaStoredState(postbox: Postbox, peerId: PeerId, messageTag: MessageTags) -> Signal<VisualMediaStoredState?, NoError> {
|
|
return postbox.transaction { transaction -> VisualMediaStoredState? in
|
|
let key = ValueBoxKey(length: 8 + 4)
|
|
key.setInt64(0, value: peerId.toInt64())
|
|
key.setUInt32(8, value: messageTag.rawValue)
|
|
if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.visualMediaStoredState, key: key))?.get(VisualMediaStoredState.self) {
|
|
return entry
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 25, highWaterItemCount: 50)
|
|
|
|
func updateVisualMediaStoredState(postbox: Postbox, peerId: PeerId, messageTag: MessageTags, state: VisualMediaStoredState?) -> Signal<Void, NoError> {
|
|
return postbox.transaction { transaction -> Void in
|
|
let key = ValueBoxKey(length: 8 + 4)
|
|
key.setInt64(0, value: peerId.toInt64())
|
|
key.setUInt32(8, value: messageTag.rawValue)
|
|
|
|
let id = ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.visualMediaStoredState, key: key)
|
|
if let state = state, let entry = CodableEntry(state) {
|
|
transaction.putItemCacheEntry(id: id, entry: entry, collectionSpec: collectionSpec)
|
|
} else {
|
|
transaction.removeItemCacheEntry(id: id)
|
|
}
|
|
}
|
|
}
|