mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
1872 lines
75 KiB
Swift
1872 lines
75 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
|
|
|
|
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 timestamp: Int32
|
|
override var tag: Int32 {
|
|
return self.timestamp
|
|
}
|
|
|
|
init(index: Int, messageId: MessageId, timestamp: Int32) {
|
|
self.indexValue = index
|
|
self.messageId = messageId
|
|
self.timestamp = timestamp
|
|
}
|
|
}
|
|
|
|
private final class VisualMediaItem: SparseItemGrid.Item {
|
|
let indexValue: Int
|
|
override var index: Int {
|
|
return self.indexValue
|
|
}
|
|
let timestamp: Int32
|
|
let message: Message?
|
|
let isLocal: Bool
|
|
|
|
enum StableId: Hashable {
|
|
case message(UInt32)
|
|
case placeholder(MessageId)
|
|
case hole(UInt32)
|
|
}
|
|
|
|
var stableId: StableId {
|
|
if let message = self.message {
|
|
return .message(message.stableId)
|
|
} else {
|
|
preconditionFailure()
|
|
//return .placeholder(self.id)
|
|
}
|
|
}
|
|
|
|
override var id: AnyHashable {
|
|
return AnyHashable(self.stableId)
|
|
}
|
|
|
|
override var tag: Int32 {
|
|
return self.timestamp
|
|
}
|
|
|
|
init(index: Int, message: Message, isLocal: Bool) {
|
|
self.indexValue = index
|
|
self.message = message
|
|
self.timestamp = message.timestamp
|
|
self.isLocal = isLocal
|
|
}
|
|
}
|
|
|
|
private final class NullActionClass: NSObject, CAAction {
|
|
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
|
|
}
|
|
}
|
|
|
|
private let nullAction = NullActionClass()
|
|
|
|
private final class ItemLayer: CALayer, SparseItemGridLayer {
|
|
var item: VisualMediaItem?
|
|
var shimmerLayer: SparseItemGrid.ShimmerLayer?
|
|
var disposable: Disposable?
|
|
|
|
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
|
|
|
|
/*if self.contents == nil, let message = item.message {
|
|
self.backgroundColor = UIColor(rgb: UInt32(clamping: UInt(bitPattern: String("\(message.id)").hashValue) & 0xffffffff)).cgColor
|
|
}*/
|
|
|
|
self.updateShimmerLayer()
|
|
}
|
|
|
|
func updateShimmerLayer() {
|
|
if self.contents == nil {
|
|
if self.shimmerLayer == nil {
|
|
let shimmerLayer = SparseItemGrid.ShimmerLayer()
|
|
self.shimmerLayer = shimmerLayer
|
|
shimmerLayer.frame = self.bounds
|
|
self.addSublayer(shimmerLayer)
|
|
}
|
|
} else if let shimmerLayer = self.shimmerLayer {
|
|
self.shimmerLayer = nil
|
|
shimmerLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak shimmerLayer] _ in
|
|
shimmerLayer?.removeFromSuperlayer()
|
|
})
|
|
}
|
|
}
|
|
|
|
func unbind() {
|
|
self.item = nil
|
|
}
|
|
|
|
func update(size: CGSize) {
|
|
if let shimmerLayer = self.shimmerLayer {
|
|
shimmerLayer.frame = CGRect(origin: CGPoint(), size: size)
|
|
}
|
|
/*var dimensions: CGSize?
|
|
|
|
if let item = self.item, let message = item.message {
|
|
for media in message.media {
|
|
if let image = media as? TelegramMediaImage, let representation = image.representations.last {
|
|
dimensions = representation.dimensions.cgSize
|
|
} else if let file = media as? TelegramMediaFile {
|
|
dimensions = file.dimensions?.cgSize ?? CGSize(width: 640.0, height: 480.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
if let dimensions = dimensions {
|
|
let scaledSize = dimensions.aspectFilled(size)
|
|
let scaledRect = CGRect(origin: CGPoint(x: (size.width - scaledSize.width) / 2.0, y: (size.height - scaledSize.height) / 2.0), size: scaledSize)
|
|
self.contentsRect = CGRect(origin: CGPoint(x: scaledRect.minX / size.width, y: scaledRect.minY / size.height), size: CGSize(width: scaledRect.width / size.width, height: scaledRect.height / size.height))
|
|
} else {
|
|
self.contentsRect = CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0))
|
|
}*/
|
|
}
|
|
}
|
|
|
|
private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
|
private let context: AccountContext
|
|
private let directMediaImageCache: DirectMediaImageCache
|
|
private let strings: PresentationStrings
|
|
|
|
var loadHoleImpl: ((SparseItemGrid.HoleAnchor, SparseItemGrid.HoleLocation) -> Signal<Never, NoError>)?
|
|
var onTapImpl: ((VisualMediaItem) -> Void)?
|
|
var onTagTapImpl: (() -> Void)?
|
|
var didScrollImpl: (() -> Void)?
|
|
|
|
init(context: AccountContext, directMediaImageCache: DirectMediaImageCache) {
|
|
self.context = context
|
|
self.directMediaImageCache = directMediaImageCache
|
|
self.strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
|
|
}
|
|
|
|
func createLayer() -> SparseItemGridLayer {
|
|
return ItemLayer()
|
|
}
|
|
|
|
func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridLayer]) {
|
|
for i in 0 ..< items.count {
|
|
guard let item = items[i] as? VisualMediaItem, let layer = layers[i] as? ItemLayer else {
|
|
continue
|
|
}
|
|
if layer.bounds.isEmpty {
|
|
continue
|
|
}
|
|
|
|
let imageWidthSpec: Int
|
|
if layer.bounds.width <= 50 {
|
|
imageWidthSpec = 64
|
|
} else if layer.bounds.width <= 100 {
|
|
imageWidthSpec = 150
|
|
} else if layer.bounds.width <= 140 {
|
|
imageWidthSpec = 200
|
|
} else {
|
|
imageWidthSpec = 280
|
|
}
|
|
|
|
if 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) {
|
|
layer.contents = result.image?.cgImage
|
|
if let loadSignal = result.loadSignal {
|
|
layer.disposable = (loadSignal
|
|
|> deliverOnMainQueue).start(next: { [weak layer] image in
|
|
guard let layer = layer else {
|
|
return
|
|
}
|
|
layer.contents = image?.cgImage
|
|
layer.updateShimmerLayer()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
layer.bind(item: item)
|
|
}
|
|
}
|
|
|
|
func unbindLayer(layer: SparseItemGridLayer) {
|
|
guard let layer = layer as? ItemLayer else {
|
|
return
|
|
}
|
|
layer.unbind()
|
|
}
|
|
|
|
func scrollerTextForTag(tag: Int32) -> String? {
|
|
let (year, month) = listMessageDateHeaderInfo(timestamp: tag)
|
|
return stringForMonth(strings: self.strings, month: month, ofYear: 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?()
|
|
}
|
|
}
|
|
|
|
/*private struct VisualMediaItemCollection {
|
|
var items: [VisualMediaItem]
|
|
var totalCount: Int
|
|
|
|
func item(at index: Int) -> VisualMediaItem? {
|
|
func binarySearch<A, T: Comparable>(_ inputArr: [A], extract: (A) -> T, searchItem: T) -> Int? {
|
|
var lowerIndex = 0
|
|
var upperIndex = inputArr.count - 1
|
|
|
|
if lowerIndex > upperIndex {
|
|
return nil
|
|
}
|
|
|
|
while true {
|
|
let currentIndex = (lowerIndex + upperIndex) / 2
|
|
let value = extract(inputArr[currentIndex])
|
|
|
|
if value == searchItem {
|
|
return currentIndex
|
|
} else if lowerIndex > upperIndex {
|
|
return nil
|
|
} else {
|
|
if (value > searchItem) {
|
|
upperIndex = currentIndex - 1
|
|
} else {
|
|
lowerIndex = currentIndex + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let itemIndex = binarySearch(self.items, extract: \.index, searchItem: index) {
|
|
return self.items[itemIndex]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func closestHole(at index: Int) -> (anchor: MessageId, direction: SparseMessageList.LoadHoleDirection)? {
|
|
var minDistance: Int?
|
|
for i in 0 ..< self.items.count {
|
|
if self.items[i].isLocal {
|
|
continue
|
|
}
|
|
if let minDistanceValue = minDistance {
|
|
if abs(self.items[i].index - index) < abs(self.items[minDistanceValue].index - index) {
|
|
minDistance = i
|
|
}
|
|
} else {
|
|
minDistance = i
|
|
}
|
|
}
|
|
if let minDistance = minDistance {
|
|
let distance = index - self.items[minDistance].index
|
|
if abs(distance) <= 2 {
|
|
return (self.items[minDistance].id, .around)
|
|
} else if distance < 0 {
|
|
return (self.items[minDistance].id, .earlier)
|
|
} else {
|
|
return (self.items[minDistance].id, .later)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func closestItem(at index: Int) -> VisualMediaItem? {
|
|
if let item = self.item(at: index) {
|
|
return item
|
|
}
|
|
var minDistance: Int?
|
|
for i in 0 ..< self.items.count {
|
|
if self.items[i].isLocal {
|
|
continue
|
|
}
|
|
if let minDistanceValue = minDistance {
|
|
if abs(self.items[i].index - index) < abs(self.items[minDistanceValue].index - index) {
|
|
minDistance = i
|
|
}
|
|
} else {
|
|
minDistance = i
|
|
}
|
|
}
|
|
if let minDistance = minDistance {
|
|
return self.items[minDistance]
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}*/
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
/*private enum ItemsLayout {
|
|
final class Grid {
|
|
let containerWidth: CGFloat
|
|
let itemCount: Int
|
|
let itemSpacing: CGFloat
|
|
let itemsInRow: Int
|
|
let itemSize: CGFloat
|
|
let rowCount: Int
|
|
let contentHeight: CGFloat
|
|
|
|
init(containerWidth: CGFloat, zoomLevel: PeerInfoVisualMediaPaneNode.ZoomLevel, itemCount: Int, bottomInset: CGFloat) {
|
|
self.containerWidth = containerWidth
|
|
self.itemCount = itemCount
|
|
self.itemSpacing = 1.0
|
|
let minItemsInRow: Int
|
|
let maxItemsInRow: Int
|
|
switch zoomLevel {
|
|
case .level2:
|
|
minItemsInRow = 2
|
|
maxItemsInRow = 4
|
|
case .level3:
|
|
minItemsInRow = 3
|
|
maxItemsInRow = 6
|
|
case .level4:
|
|
minItemsInRow = 4
|
|
maxItemsInRow = 8
|
|
case .level5:
|
|
minItemsInRow = 5
|
|
maxItemsInRow = 10
|
|
}
|
|
self.itemsInRow = max(minItemsInRow, min(maxItemsInRow, Int(containerWidth / 140.0)))
|
|
self.itemSize = floor(containerWidth / CGFloat(itemsInRow))
|
|
|
|
self.rowCount = itemCount / self.itemsInRow + (itemCount % self.itemsInRow == 0 ? 0 : 1)
|
|
|
|
self.contentHeight = CGFloat(self.rowCount + 1) * self.itemSpacing + CGFloat(rowCount) * itemSize + bottomInset
|
|
}
|
|
|
|
func visibleRange(rect: CGRect) -> (Int, Int) {
|
|
var minVisibleRow = Int(floor((rect.minY - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
|
|
minVisibleRow = max(0, minVisibleRow)
|
|
var maxVisibleRow = Int(ceil((rect.maxY - self.itemSpacing) / (self.itemSize + itemSpacing)))
|
|
maxVisibleRow = min(self.rowCount - 1, maxVisibleRow)
|
|
|
|
let minVisibleIndex = minVisibleRow * itemsInRow
|
|
let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * itemsInRow - 1)
|
|
|
|
return (minVisibleIndex, maxVisibleIndex)
|
|
}
|
|
|
|
func frame(forItemAt index: Int, sideInset: CGFloat) -> CGRect {
|
|
let rowIndex = index / Int(self.itemsInRow)
|
|
let columnIndex = index % Int(self.itemsInRow)
|
|
let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (self.itemSize + self.itemSpacing), y: self.itemSpacing + CGFloat(rowIndex) * (self.itemSize + self.itemSpacing))
|
|
return CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == self.itemsInRow ? (self.containerWidth - itemOrigin.x) : self.itemSize, height: self.itemSize))
|
|
}
|
|
}
|
|
|
|
case grid(Grid)
|
|
|
|
var contentHeight: CGFloat {
|
|
switch self {
|
|
case let .grid(grid):
|
|
return grid.contentHeight
|
|
}
|
|
}
|
|
|
|
func visibleRange(rect: CGRect) -> (Int, Int) {
|
|
switch self {
|
|
case let .grid(grid):
|
|
return grid.visibleRange(rect: rect)
|
|
}
|
|
}
|
|
|
|
func frame(forItemAt index: Int, sideInset: CGFloat) -> CGRect {
|
|
switch self {
|
|
case let .grid(grid):
|
|
return grid.frame(forItemAt: index, sideInset: sideInset)
|
|
}
|
|
}
|
|
}*/
|
|
|
|
final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
|
|
enum ContentType {
|
|
case photoOrVideo
|
|
case photo
|
|
case video
|
|
case gifs
|
|
}
|
|
|
|
struct ZoomLevel {
|
|
fileprivate var value: SparseItemGrid.ZoomLevel
|
|
|
|
init(_ value: SparseItemGrid.ZoomLevel) {
|
|
self.value = value
|
|
}
|
|
}
|
|
|
|
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 scrollingArea: SparseItemGridScrollingArea
|
|
//private let scrollNode: ASScrollNode
|
|
private let itemGrid: SparseItemGrid
|
|
private let itemGridBinding: SparseItemGridBindingImpl
|
|
private let directMediaImageCache: DirectMediaImageCache
|
|
private var items: SparseItemGrid.Items?
|
|
|
|
private var isDeceleratingAfterTracking = false
|
|
|
|
private var _itemInteraction: VisualMediaItemInteraction?
|
|
private var itemInteraction: VisualMediaItemInteraction {
|
|
return self._itemInteraction!
|
|
}
|
|
|
|
private var currentParams: (size: CGSize, 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()
|
|
}
|
|
|
|
private let listDisposable = MetaDisposable()
|
|
private var hiddenMediaDisposable: Disposable?
|
|
//private var mediaItems = VisualMediaItemCollection(items: [], totalCount: 0)
|
|
//private var itemsLayout: ItemsLayout?
|
|
//private var visibleMediaItems: [VisualMediaItem.StableId: VisualMediaItemNode] = [:]
|
|
|
|
private var numberOfItemsToRequest: Int = 50
|
|
//private var currentView: MessageHistoryView?
|
|
private var isRequestingView: Bool = false
|
|
private var isFirstHistoryView: Bool = true
|
|
|
|
private var decelerationAnimator: ConstantDisplayLinkAnimator?
|
|
|
|
private var animationTimer: SwiftSignalKit.Timer?
|
|
|
|
private var listSource: SparseMessageList
|
|
private var requestedPlaceholderIds = Set<MessageId>()
|
|
|
|
var openCurrentDate: (() -> Void)?
|
|
var paneDidScroll: (() -> Void)?
|
|
|
|
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.scrollingArea = SparseItemGridScrollingArea()
|
|
//self.scrollNode = ASScrollNode()
|
|
self.itemGrid = SparseItemGrid()
|
|
self.directMediaImageCache = DirectMediaImageCache(account: context.account)
|
|
self.itemGridBinding = SparseItemGridBindingImpl(context: context, directMediaImageCache: self.directMediaImageCache)
|
|
|
|
self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, tag: tagMaskForType(self.contentType))
|
|
|
|
super.init()
|
|
|
|
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
|
|
}
|
|
guard let message = item.message else {
|
|
return
|
|
}
|
|
let _ = strongSelf.chatControllerInteraction.openMessage(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?()
|
|
}
|
|
|
|
/*self.scrollingArea.beginScrolling = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return nil
|
|
}
|
|
return strongSelf.scrollNode.view
|
|
}
|
|
|
|
self.scrollingArea.openCurrentDate = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.openCurrentDate?()
|
|
}*/
|
|
|
|
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.scrollNode.view.delaysContentTouches = false
|
|
self.scrollNode.view.canCancelContentTouches = true
|
|
self.scrollNode.view.showsVerticalScrollIndicator = false
|
|
if #available(iOS 11.0, *) {
|
|
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
self.scrollNode.view.scrollsToTop = false
|
|
self.scrollNode.view.delegate = self
|
|
|
|
self.addSubnode(self.scrollNode)
|
|
self.addSubnode(self.scrollingArea)*/
|
|
|
|
self.addSubnode(self.itemGrid)
|
|
|
|
self.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
|
|
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)
|
|
}
|
|
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)
|
|
}
|
|
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
|
|
switch contentType {
|
|
case .photoOrVideo:
|
|
let photoCount: Int32 = dict[.photo] ?? 0
|
|
let videoCount: Int32 = dict[.video] ?? 0
|
|
|
|
//TODO:localize
|
|
if photoCount != 0 && videoCount != 0 {
|
|
return PeerInfoStatusData(text: "\(photoCount) photos, \(videoCount) videos", isActivity: false)
|
|
} else if photoCount != 0 {
|
|
return PeerInfoStatusData(text: "\(photoCount) photos", isActivity: false)
|
|
} else if videoCount != 0 {
|
|
return PeerInfoStatusData(text: "\(photoCount) videos", isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
case .photo:
|
|
let photoCount: Int32 = dict[.photo] ?? 0
|
|
|
|
//TODO:localize
|
|
if photoCount != 0 {
|
|
return PeerInfoStatusData(text: "\(photoCount) photos", isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
case .video:
|
|
let videoCount: Int32 = dict[.video] ?? 0
|
|
|
|
//TODO:localize
|
|
if videoCount != 0 {
|
|
return PeerInfoStatusData(text: "\(videoCount) videos", isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
case .gifs:
|
|
let gifCount: Int32 = dict[.gif] ?? 0
|
|
|
|
//TODO:localize
|
|
if gifCount != 0 {
|
|
return PeerInfoStatusData(text: "\(gifCount) gifs", isActivity: false)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
deinit {
|
|
self.listDisposable.dispose()
|
|
self.hiddenMediaDisposable?.dispose()
|
|
self.animationTimer?.invalidate()
|
|
}
|
|
|
|
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.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)
|
|
|
|
/*if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams {
|
|
|
|
var currentTopVisibleItemFrame: CGRect?
|
|
if let itemsLayout = self.itemsLayout {
|
|
let headerItemMinY = self.scrollNode.view.bounds.minY + 1.0
|
|
let (minVisibleIndex, maxVisibleIndex) = itemsLayout.visibleRange(rect: self.scrollNode.view.bounds)
|
|
|
|
if minVisibleIndex <= maxVisibleIndex {
|
|
for i in minVisibleIndex ... maxVisibleIndex {
|
|
let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: sideInset)
|
|
|
|
if currentTopVisibleItemFrame == nil && itemFrame.maxY > headerItemMinY {
|
|
currentTopVisibleItemFrame = self.scrollNode.view.convert(itemFrame, to: self.view)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.itemsLayout = nil
|
|
|
|
let copyView = self.scrollNode.view.snapshotView(afterScreenUpdates: false)
|
|
|
|
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate)
|
|
|
|
var updatedTopVisibleItemFrame: CGRect?
|
|
if let itemsLayout = self.itemsLayout {
|
|
let headerItemMinY = self.scrollNode.view.bounds.minY + 1.0
|
|
let (updatedMinVisibleIndex, updatedMaxVisibleIndex) = itemsLayout.visibleRange(rect: self.scrollNode.view.bounds)
|
|
|
|
if updatedMinVisibleIndex <= updatedMaxVisibleIndex {
|
|
for i in updatedMinVisibleIndex ... updatedMaxVisibleIndex {
|
|
let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: sideInset)
|
|
|
|
if updatedTopVisibleItemFrame == nil && itemFrame.maxY > headerItemMinY {
|
|
updatedTopVisibleItemFrame = self.scrollNode.view.convert(itemFrame, to: self.view)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let copyView = copyView, let currentTopVisibleItemFrame = currentTopVisibleItemFrame, let updatedTopVisibleItemFrame = updatedTopVisibleItemFrame {
|
|
self.view.addSubview(copyView)
|
|
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in
|
|
copyView?.removeFromSuperview()
|
|
})
|
|
|
|
let additionalOffset = CGPoint(x: updatedTopVisibleItemFrame.minX - currentTopVisibleItemFrame.minX, y: updatedTopVisibleItemFrame.minY - currentTopVisibleItemFrame.minY)
|
|
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: self.scrollNode.view.contentOffset.y + additionalOffset.y), animated: false)
|
|
|
|
let widthFactor = updatedTopVisibleItemFrame.width / currentTopVisibleItemFrame.width
|
|
copyView.layer.animateScale(from: 1.0, to: widthFactor, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in })
|
|
let copyOffset = CGPoint(x: -self.scrollNode.bounds.width * (1.0 - widthFactor) * 0.5, y: -self.scrollNode.bounds.height * (1.0 - widthFactor) * 0.5)//.offsetBy(dx: additionalOffset.x, dy: additionalOffset.y)
|
|
copyView.layer.animatePosition(from: CGPoint(), to: copyOffset, duration: 0.2, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
|
|
|
self.scrollNode.layer.animateScale(from: 1.0 / widthFactor, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: true)
|
|
let originalOffset = CGPoint(x: -self.scrollNode.bounds.width * (1.0 - 1.0 / widthFactor) * 0.5, y: -self.scrollNode.bounds.height * (1.0 - 1.0 / widthFactor) * 0.5)//.offsetBy(dx: additionalOffset.x, dy: additionalOffset.y)
|
|
self.scrollNode.layer.animatePosition(from: originalOffset, to: CGPoint(), duration: 0.2, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: true, additive: true)
|
|
}
|
|
}*/
|
|
}
|
|
|
|
func ensureMessageIsVisible(id: MessageId) {
|
|
/*let activeRect = self.scrollNode.bounds
|
|
for item in self.mediaItems.items {
|
|
if let message = item.message, message.id == id {
|
|
if let itemNode = self.visibleMediaItems[item.stableId] {
|
|
if !activeRect.contains(itemNode.frame) {
|
|
let targetContentOffset = CGPoint(x: 0.0, y: max(-self.scrollNode.view.contentInset.top, itemNode.frame.minY - (self.scrollNode.frame.height - itemNode.frame.height) / 2.0))
|
|
self.scrollNode.view.setContentOffset(targetContentOffset, animated: false)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}*/
|
|
}
|
|
|
|
private func requestHistoryAroundVisiblePosition(synchronous: Bool, reloadAtTop: Bool) {
|
|
if self.isRequestingView {
|
|
return
|
|
}
|
|
self.isRequestingView = true
|
|
var firstTime = true
|
|
self.listDisposable.set((self.listSource.state
|
|
|> deliverOnMainQueue).start(next: { [weak self] list in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let currentSynchronous = synchronous && firstTime
|
|
let currentReloadAtTop = reloadAtTop && firstTime
|
|
firstTime = false
|
|
strongSelf.updateHistory(list: list, synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop)
|
|
strongSelf.isRequestingView = false
|
|
}))
|
|
}
|
|
|
|
private func updateHistory(list: SparseMessageList.State, synchronous: Bool, reloadAtTop: Bool) {
|
|
var mappedItems: [SparseItemGrid.Item] = []
|
|
var mappeHoles: [SparseItemGrid.HoleAnchor] = []
|
|
for item in list.items {
|
|
switch item.content {
|
|
case let .message(message, isLocal):
|
|
mappedItems.append(VisualMediaItem(index: item.index, message: message, isLocal: isLocal))
|
|
case let .placeholder(id, timestamp):
|
|
mappeHoles.append(VisualMediaHoleAnchor(index: item.index, messageId: id, timestamp: timestamp))
|
|
}
|
|
}
|
|
|
|
self.items = SparseItemGrid.Items(
|
|
items: mappedItems,
|
|
holeAnchors: mappeHoles,
|
|
count: list.totalCount,
|
|
itemBinding: self.itemGridBinding
|
|
)
|
|
|
|
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams {
|
|
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate)
|
|
}
|
|
|
|
/*self.mediaItems = VisualMediaItemCollection(items: [], totalCount: list.totalCount)
|
|
for item in list.items {
|
|
switch item.content {
|
|
case let .message(message, isLocal):
|
|
self.mediaItems.items.append(VisualMediaItem(index: item.index, message: message, isLocal: isLocal))
|
|
case let .placeholder(id, timestamp):
|
|
self.mediaItems.items.append(VisualMediaItem(index: item.index, id: id, timestamp: timestamp))
|
|
}
|
|
}
|
|
self.itemsLayout = nil
|
|
|
|
let wasFirstHistoryView = self.isFirstHistoryView
|
|
self.isFirstHistoryView = false
|
|
|
|
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = self.currentParams {
|
|
if synchronous {
|
|
if let copyView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) {
|
|
copyView.backgroundColor = self.context.sharedContext.currentPresentationData.with({ $0 }).theme.list.plainBackgroundColor
|
|
self.view.addSubview(copyView)
|
|
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in
|
|
copyView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
self.ignoreScrolling = true
|
|
if reloadAtTop {
|
|
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false)
|
|
}
|
|
self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: wasFirstHistoryView || synchronous, transition: .immediate)
|
|
self.ignoreScrolling = false
|
|
}*/
|
|
|
|
if !self.didSetReady {
|
|
self.didSetReady = true
|
|
self.ready.set(.single(true))
|
|
}
|
|
}
|
|
|
|
func scrollToTop() -> Bool {
|
|
/*if self.scrollNode.view.contentOffset.y > 0.0 {
|
|
self.scrollNode.view.setContentOffset(CGPoint(), animated: true)
|
|
return true
|
|
} else {
|
|
return false
|
|
}*/
|
|
return false
|
|
}
|
|
|
|
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 let message = item.message, message.id == id {
|
|
return item.message
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func updateHiddenMedia() {
|
|
self.itemGrid.forEachVisibleItem { itemLayer in
|
|
guard let itemLayer = itemLayer as? ItemLayer else {
|
|
return
|
|
}
|
|
if let item = itemLayer.item, let message = item.message {
|
|
if self.itemInteraction.hiddenMedia[message.id] != nil {
|
|
itemLayer.isHidden = true
|
|
} else {
|
|
itemLayer.isHidden = false
|
|
}
|
|
} else {
|
|
itemLayer.isHidden = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func transferVelocity(_ velocity: CGFloat) {
|
|
/*if velocity > 0.0 {
|
|
self.decelerationAnimator?.isPaused = true
|
|
let startTime = CACurrentMediaTime()
|
|
var currentOffset = self.scrollNode.view.contentOffset
|
|
let decelerationRate: CGFloat = 0.998
|
|
self.scrollViewDidEndDragging(self.scrollNode.view, willDecelerate: true)
|
|
self.decelerationAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let t = CACurrentMediaTime() - startTime
|
|
var currentVelocity = velocity * 15.0 * CGFloat(pow(Double(decelerationRate), 1000.0 * t))
|
|
currentOffset.y += currentVelocity
|
|
let maxOffset = strongSelf.scrollNode.view.contentSize.height - strongSelf.scrollNode.bounds.height
|
|
if currentOffset.y >= maxOffset {
|
|
currentOffset.y = maxOffset
|
|
currentVelocity = 0.0
|
|
}
|
|
if currentOffset.y < 0.0 {
|
|
currentOffset.y = 0.0
|
|
currentVelocity = 0.0
|
|
}
|
|
|
|
var didEnd = false
|
|
if abs(currentVelocity) < 0.1 {
|
|
strongSelf.decelerationAnimator?.isPaused = true
|
|
strongSelf.decelerationAnimator = nil
|
|
didEnd = true
|
|
}
|
|
var contentOffset = strongSelf.scrollNode.view.contentOffset
|
|
contentOffset.y = floorToScreenPixels(currentOffset.y)
|
|
strongSelf.scrollNode.view.setContentOffset(contentOffset, animated: false)
|
|
strongSelf.scrollViewDidScroll(strongSelf.scrollNode.view)
|
|
if didEnd {
|
|
strongSelf.scrollViewDidEndDecelerating(strongSelf.scrollNode.view)
|
|
}
|
|
})
|
|
self.decelerationAnimator?.isPaused = false
|
|
}*/
|
|
}
|
|
|
|
func cancelPreviewGestures() {
|
|
/*for (_, itemNode) in self.visibleMediaItems {
|
|
itemNode.cancelPreviewGesture()
|
|
}*/
|
|
}
|
|
|
|
func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
var foundItemLayer: SparseItemGridLayer?
|
|
self.itemGrid.forEachVisibleItem { itemLayer in
|
|
guard let itemLayer = itemLayer as? ItemLayer else {
|
|
return
|
|
}
|
|
if let item = itemLayer.item, let message = item.message, 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) {
|
|
/*self.itemInteraction.selectedMessageIds = self.chatControllerInteraction.selectionState.flatMap { $0.selectedIds }
|
|
for (_, itemNode) in self.visibleMediaItems {
|
|
itemNode.updateSelectionState(animated: animated)
|
|
}*/
|
|
}
|
|
|
|
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
|
//let previousParams = self.currentParams
|
|
self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
|
|
|
transition.updateFrame(node: self.itemGrid, frame: CGRect(origin: CGPoint(), size: size))
|
|
if let items = self.items {
|
|
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, items: items)
|
|
}
|
|
}
|
|
|
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
}
|
|
|
|
private var previousDidScrollTimestamp: Double = 0.0
|
|
private var ignoreScrolling: Bool = false
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
if self.ignoreScrolling {
|
|
return
|
|
}
|
|
}
|
|
|
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
}
|
|
|
|
private func updateScrollingArea(transition: ContainedViewLayoutTransition) {
|
|
}
|
|
|
|
func currentTopTimestamp() -> Int32? {
|
|
var timestamp: Int32?
|
|
self.itemGrid.forEachVisibleItem { itemLayer in
|
|
if timestamp != nil {
|
|
return
|
|
}
|
|
guard let itemLayer = itemLayer as? ItemLayer else {
|
|
return
|
|
}
|
|
if let item = itemLayer.item, let message = item.message {
|
|
if let timestampValue = timestamp {
|
|
timestamp = max(timestampValue, message.timestamp)
|
|
} else {
|
|
timestamp = 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, let message = item.message else {
|
|
continue
|
|
}
|
|
if 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)
|
|
}
|
|
}
|
|
/*guard let currentParams = self.currentParams else {
|
|
return
|
|
}
|
|
guard let itemsLayout = self.itemsLayout else {
|
|
return
|
|
}
|
|
for item in self.mediaItems.items {
|
|
if item.timestamp <= timestamp {
|
|
let frame = itemsLayout.frame(forItemAt: item.index, sideInset: currentParams.sideInset)
|
|
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: frame.minY), animated: false)
|
|
|
|
break
|
|
}
|
|
}*/
|
|
}
|
|
|
|
/*private func updateVisibleItems(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool) {
|
|
guard let itemsLayout = self.itemsLayout else {
|
|
return
|
|
}
|
|
|
|
let activeRect = self.scrollNode.view.bounds
|
|
let visibleRect = activeRect.insetBy(dx: 0.0, dy: -400.0)
|
|
|
|
let (minActuallyVisibleIndex, maxActuallyVisibleIndex) = itemsLayout.visibleRange(rect: activeRect)
|
|
let (minVisibleIndex, maxVisibleIndex) = itemsLayout.visibleRange(rect: visibleRect)
|
|
|
|
var requestHole: (anchor: MessageId, direction: SparseMessageList.LoadHoleDirection)?
|
|
|
|
var validIds = Set<VisualMediaItem.StableId>()
|
|
if minVisibleIndex <= maxVisibleIndex {
|
|
for itemIndex in minVisibleIndex ... maxVisibleIndex {
|
|
let maybeItem = self.mediaItems.item(at: itemIndex)
|
|
var findHole = false
|
|
if let item = maybeItem {
|
|
if item.message == nil {
|
|
findHole = true
|
|
}
|
|
} else {
|
|
findHole = true
|
|
}
|
|
if findHole {
|
|
if let hole = self.mediaItems.closestHole(at: itemIndex) {
|
|
if requestHole == nil {
|
|
requestHole = hole
|
|
}
|
|
}
|
|
}
|
|
|
|
let stableId: VisualMediaItem.StableId
|
|
if let item = maybeItem {
|
|
stableId = item.stableId
|
|
} else {
|
|
stableId = .hole(UInt32(itemIndex))
|
|
}
|
|
|
|
validIds.insert(stableId)
|
|
|
|
let itemFrame = itemsLayout.frame(forItemAt: itemIndex, sideInset: sideInset)
|
|
|
|
let itemNode: VisualMediaItemNode
|
|
if let current = self.visibleMediaItems[stableId] {
|
|
itemNode = current
|
|
} else {
|
|
itemNode = VisualMediaItemNode(context: self.context, interaction: self.itemInteraction)
|
|
self.visibleMediaItems[stableId] = itemNode
|
|
self.scrollNode.addSubnode(itemNode)
|
|
}
|
|
itemNode.frame = itemFrame
|
|
itemNode.updateAbsoluteRect(itemFrame.offsetBy(dx: 0.0, dy: -activeRect.origin.y), within: activeRect.size)
|
|
|
|
var itemSynchronousLoad = false
|
|
if itemIndex >= minActuallyVisibleIndex && itemIndex <= maxActuallyVisibleIndex {
|
|
itemSynchronousLoad = synchronousLoad
|
|
}
|
|
itemNode.update(size: itemFrame.size, item: maybeItem, theme: theme, synchronousLoad: itemSynchronousLoad)
|
|
itemNode.updateIsVisible(itemFrame.intersects(activeRect))
|
|
}
|
|
}
|
|
var removeKeys: [VisualMediaItem.StableId] = []
|
|
for (id, _) in self.visibleMediaItems {
|
|
if !validIds.contains(id) {
|
|
removeKeys.append(id)
|
|
}
|
|
}
|
|
for id in removeKeys {
|
|
if let itemNode = self.visibleMediaItems.removeValue(forKey: id) {
|
|
itemNode.removeFromSupernode()
|
|
}
|
|
}
|
|
|
|
if let requestHole = requestHole {
|
|
self.listSource.loadHole(anchor: requestHole.anchor, direction: requestHole.direction)
|
|
}
|
|
}*/
|
|
|
|
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))
|
|
}
|
|
}
|