mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
2005 lines
82 KiB
Swift
2005 lines
82 KiB
Swift
import AsyncDisplayKit
|
|
import AVFoundation
|
|
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
|
|
import ChatControllerInteraction
|
|
import InvisibleInkDustNode
|
|
import MediaPickerUI
|
|
import StoryContainerScreen
|
|
import StoryContentComponent
|
|
|
|
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 VisualMediaHoleAnchor: SparseItemGrid.HoleAnchor {
|
|
let storyId: Int32
|
|
override var id: AnyHashable {
|
|
return AnyHashable(self.storyId)
|
|
}
|
|
|
|
let indexValue: Int
|
|
override var index: Int {
|
|
return self.indexValue
|
|
}
|
|
|
|
let localMonthTimestamp: Int32
|
|
override var tag: Int32 {
|
|
return self.localMonthTimestamp
|
|
}
|
|
|
|
init(index: Int, storyId: Int32, localMonthTimestamp: Int32) {
|
|
self.indexValue = index
|
|
self.storyId = storyId
|
|
self.localMonthTimestamp = localMonthTimestamp
|
|
}
|
|
}
|
|
|
|
private final class VisualMediaItem: SparseItemGrid.Item {
|
|
let indexValue: Int
|
|
override var index: Int {
|
|
return self.indexValue
|
|
}
|
|
let localMonthTimestamp: Int32
|
|
let peer: PeerReference
|
|
let story: EngineStoryItem
|
|
|
|
override var id: AnyHashable {
|
|
return AnyHashable(self.story.id)
|
|
}
|
|
|
|
override var tag: Int32 {
|
|
return self.localMonthTimestamp
|
|
}
|
|
|
|
override var holeAnchor: SparseItemGrid.HoleAnchor {
|
|
return VisualMediaHoleAnchor(index: self.index, storyId: self.story.id, localMonthTimestamp: self.localMonthTimestamp)
|
|
}
|
|
|
|
init(index: Int, peer: PeerReference, story: EngineStoryItem, localMonthTimestamp: Int32) {
|
|
self.indexValue = index
|
|
self.peer = peer
|
|
self.story = story
|
|
self.localMonthTimestamp = localMonthTimestamp
|
|
}
|
|
}
|
|
|
|
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 protocol ItemLayer: SparseItemGridLayer {
|
|
var item: VisualMediaItem? { get set }
|
|
var durationLayer: DurationLayer? { get set }
|
|
var minFactor: CGFloat { get set }
|
|
var selectionLayer: GridMessageSelectionLayer? { get set }
|
|
var disposable: Disposable? { get set }
|
|
|
|
var hasContents: Bool { get set }
|
|
func setSpoilerContents(_ contents: Any?)
|
|
|
|
func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat)
|
|
func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool)
|
|
func updateHasSpoiler(hasSpoiler: Bool)
|
|
|
|
func bind(item: VisualMediaItem)
|
|
func unbind()
|
|
}
|
|
|
|
private final class GenericItemLayer: CALayer, ItemLayer {
|
|
var item: VisualMediaItem?
|
|
var durationLayer: DurationLayer?
|
|
var minFactor: CGFloat = 1.0
|
|
var selectionLayer: GridMessageSelectionLayer?
|
|
var dustLayer: MediaDustLayer?
|
|
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()
|
|
}
|
|
|
|
func getContents() -> Any? {
|
|
return self.contents
|
|
}
|
|
|
|
func setContents(_ contents: Any?) {
|
|
if let image = contents as? UIImage {
|
|
self.contents = image.cgImage
|
|
}
|
|
}
|
|
|
|
func setSpoilerContents(_ contents: Any?) {
|
|
if let image = contents as? UIImage {
|
|
self.dustLayer?.contents = image.cgImage
|
|
}
|
|
}
|
|
|
|
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 updateHasSpoiler(hasSpoiler: Bool) {
|
|
if hasSpoiler {
|
|
if let _ = self.dustLayer {
|
|
} else {
|
|
let dustLayer = MediaDustLayer()
|
|
self.dustLayer = dustLayer
|
|
self.addSublayer(dustLayer)
|
|
if !self.bounds.isEmpty {
|
|
dustLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
|
dustLayer.updateLayout(size: self.bounds.size)
|
|
}
|
|
}
|
|
} else if let dustLayer = self.dustLayer {
|
|
self.dustLayer = nil
|
|
dustLayer.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 CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemLayer {
|
|
var item: VisualMediaItem?
|
|
var durationLayer: DurationLayer?
|
|
var minFactor: CGFloat = 1.0
|
|
var selectionLayer: GridMessageSelectionLayer?
|
|
var dustLayer: MediaDustLayer?
|
|
var disposable: Disposable?
|
|
|
|
var hasContents: Bool = false
|
|
|
|
override init() {
|
|
super.init()
|
|
|
|
self.contentsGravity = .resize
|
|
if #available(iOS 13.0, *) {
|
|
self.preventsCapture = true
|
|
self.preventsDisplaySleepDuringVideoPlayback = false
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
}
|
|
|
|
override func action(forKey event: String) -> CAAction? {
|
|
return nullAction
|
|
}
|
|
|
|
private var layerContents: Any?
|
|
func getContents() -> Any? {
|
|
return self.layerContents
|
|
}
|
|
|
|
func setContents(_ contents: Any?) {
|
|
self.layerContents = contents
|
|
|
|
if let image = contents as? UIImage {
|
|
self.layerContents = image.cgImage
|
|
if let cmSampleBuffer = image.cmSampleBuffer {
|
|
self.enqueue(cmSampleBuffer)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setSpoilerContents(_ contents: Any?) {
|
|
if let image = contents as? UIImage {
|
|
self.dustLayer?.contents = image.cgImage
|
|
}
|
|
}
|
|
|
|
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 updateHasSpoiler(hasSpoiler: Bool) {
|
|
if hasSpoiler {
|
|
if let _ = self.dustLayer {
|
|
} else {
|
|
let dustLayer = MediaDustLayer()
|
|
self.dustLayer = dustLayer
|
|
self.addSublayer(dustLayer)
|
|
if !self.bounds.isEmpty {
|
|
dustLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
|
dustLayer.updateLayout(size: self.bounds.size)
|
|
}
|
|
}
|
|
} else if let dustLayer = self.dustLayer {
|
|
self.dustLayer = nil
|
|
dustLayer.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 SparseItemGridBindingImpl: SparseItemGridBinding {
|
|
let context: AccountContext
|
|
let chatLocation: ChatLocation
|
|
let directMediaImageCache: DirectMediaImageCache
|
|
let captureProtected: Bool
|
|
var strings: PresentationStrings
|
|
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)?
|
|
|
|
var revealedSpoilerMessageIds = Set<MessageId>()
|
|
|
|
private var shimmerImages: [CGFloat: UIImage] = [:]
|
|
|
|
init(context: AccountContext, chatLocation: ChatLocation, chatControllerInteraction: ChatControllerInteraction, directMediaImageCache: DirectMediaImageCache, captureProtected: Bool) {
|
|
self.context = context
|
|
self.chatLocation = chatLocation
|
|
self.chatControllerInteraction = chatControllerInteraction
|
|
self.directMediaImageCache = directMediaImageCache
|
|
self.captureProtected = false
|
|
|
|
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 getSeparatorColor() -> UIColor {
|
|
return self.chatPresentationData.theme.theme.list.itemPlainSeparatorColor
|
|
}
|
|
|
|
func createLayer() -> SparseItemGridLayer? {
|
|
if self.captureProtected {
|
|
return CaptureProtectedItemLayer()
|
|
} else {
|
|
return GenericItemLayer()
|
|
}
|
|
}
|
|
|
|
func createView() -> SparseItemGridView? {
|
|
return nil
|
|
}
|
|
|
|
func createShimmerLayer() -> SparseItemGridShimmerLayer? {
|
|
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], size: CGSize, insets: UIEdgeInsets, synchronous: SparseItemGrid.Synchronous) {
|
|
for i in 0 ..< items.count {
|
|
guard let item = items[i] as? VisualMediaItem else {
|
|
continue
|
|
}
|
|
|
|
let displayItem = layers[i]
|
|
|
|
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 story = item.story
|
|
let hasSpoiler = false
|
|
layer.updateHasSpoiler(hasSpoiler: hasSpoiler)
|
|
|
|
var selectedMedia: Media?
|
|
if let image = story.media._asMedia() as? TelegramMediaImage {
|
|
selectedMedia = image
|
|
} else if let file = story.media._asMedia() as? TelegramMediaFile {
|
|
selectedMedia = file
|
|
}
|
|
|
|
if let selectedMedia = selectedMedia {
|
|
if let result = directMediaImageCache.getImage(peer: item.peer, story: story, media: selectedMedia, width: imageWidthSpec, aspectRatio: 0.56, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, includeBlurred: hasSpoiler, synchronous: synchronous == .full) {
|
|
if let image = result.image {
|
|
layer.setContents(image)
|
|
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 image = result.blurredImage {
|
|
layer.setSpoilerContents(image)
|
|
}
|
|
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 let contents = layer.getContents(), !synchronousValue {
|
|
let copyLayer = GenericItemLayer()
|
|
copyLayer.contents = 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.setContents(image)
|
|
layer.hasContents = true
|
|
if let displayItem = displayItem {
|
|
self?.updateShimmerLayersImpl?(displayItem)
|
|
}
|
|
} else {
|
|
layer.setContents(image)
|
|
|
|
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.flatMap(Int32.init)
|
|
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 {
|
|
//TODO:selection
|
|
let _ = selectionState
|
|
layer.updateSelection(theme: self.checkNodeTheme, isSelected: false, 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
|
public enum ContentType {
|
|
case photoOrVideo
|
|
case photo
|
|
case video
|
|
}
|
|
|
|
public 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 chatLocation: ChatLocation
|
|
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
|
|
private let chatControllerInteraction: ChatControllerInteraction
|
|
public private(set) var contentType: ContentType
|
|
private var contentTypePromise: ValuePromise<ContentType>
|
|
|
|
public 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
|
|
public var isReady: Signal<Bool, NoError> {
|
|
return self.ready.get()
|
|
}
|
|
|
|
private let statusPromise = Promise<PeerInfoStatusData?>(nil)
|
|
public var status: Signal<PeerInfoStatusData?, NoError> {
|
|
self.statusPromise.get()
|
|
}
|
|
|
|
public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
|
public 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?
|
|
|
|
public private(set) var calendarSource: SparseMessageCalendar?
|
|
private var listSource: PeerStoryListContext
|
|
|
|
public var openCurrentDate: (() -> Void)?
|
|
public var paneDidScroll: (() -> Void)?
|
|
|
|
private weak var currentGestureItem: SparseItemGridDisplayItem?
|
|
|
|
private var presentationData: PresentationData
|
|
private var presentationDataDisposable: Disposable?
|
|
|
|
public init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, contentType: ContentType, captureProtected: Bool) {
|
|
self.context = context
|
|
self.peerId = peerId
|
|
self.chatLocation = chatLocation
|
|
self.chatLocationContextHolder = chatLocationContextHolder
|
|
self.chatControllerInteraction = chatControllerInteraction
|
|
self.contentType = contentType
|
|
self.contentTypePromise = ValuePromise<ContentType>(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)
|
|
|
|
self.itemGridBinding = SparseItemGridBindingImpl(
|
|
context: context,
|
|
chatLocation: .peer(id: peerId),
|
|
chatControllerInteraction: chatControllerInteraction,
|
|
directMediaImageCache: self.directMediaImageCache,
|
|
captureProtected: captureProtected
|
|
)
|
|
|
|
self.listSource = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false)
|
|
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 self else {
|
|
return
|
|
}
|
|
if let selectionState = self.chatControllerInteraction.selectionState {
|
|
let _ = selectionState
|
|
//TODO:selection
|
|
/*var toggledValue = true
|
|
if selectionState.selectedIds.contains(item.message.id) {
|
|
toggledValue = false
|
|
}
|
|
strongSelf.chatControllerInteraction.toggleMessagesSelection([item.message.id], toggledValue)*/
|
|
} else {
|
|
let listContext = PeerStoryListContentContextImpl(
|
|
context: self.context,
|
|
peerId: self.peerId,
|
|
listContext: self.listSource,
|
|
initialId: item.story.id
|
|
)
|
|
let _ = (listContext.state
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
|
guard let self, let navigationController = self.chatControllerInteraction.navigationController() else {
|
|
return
|
|
}
|
|
|
|
var transitionIn: StoryContainerScreen.TransitionIn?
|
|
|
|
let story = item.story
|
|
var foundItemLayer: SparseItemGridLayer?
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemLayer = item.layer as? ItemLayer else {
|
|
return
|
|
}
|
|
if let listItem = itemLayer.item, listItem.story.id == story.id {
|
|
foundItemLayer = itemLayer
|
|
}
|
|
}
|
|
if let foundItemLayer {
|
|
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
|
|
transitionIn = StoryContainerScreen.TransitionIn(
|
|
sourceView: self.view,
|
|
sourceRect: self.itemGrid.view.convert(itemRect, to: self.view),
|
|
sourceCornerRadius: 0.0
|
|
)
|
|
}
|
|
|
|
let storyContainerScreen = StoryContainerScreen(
|
|
context: self.context,
|
|
content: listContext,
|
|
transitionIn: transitionIn,
|
|
transitionOut: { [weak self] _, itemId in
|
|
guard let self else {
|
|
return nil
|
|
}
|
|
|
|
var foundItemLayer: SparseItemGridLayer?
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemLayer = item.layer as? ItemLayer else {
|
|
return
|
|
}
|
|
if let listItem = itemLayer.item, AnyHashable(listItem.story.id) == itemId {
|
|
foundItemLayer = itemLayer
|
|
}
|
|
}
|
|
if let foundItemLayer {
|
|
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
|
|
return StoryContainerScreen.TransitionOut(
|
|
destinationView: self.view,
|
|
destinationRect: self.itemGrid.view.convert(itemRect, to: self.view),
|
|
destinationCornerRadius: 0.0,
|
|
destinationIsAvatar: false,
|
|
completed: {}
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
)
|
|
navigationController.pushViewController(storyContainerScreen)
|
|
})
|
|
|
|
/*let _ = (StoryChatContent.stories(
|
|
context: self.context,
|
|
storyList: self.listSource,
|
|
focusItem: item.story.id
|
|
)
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] initialContent in
|
|
guard let self, let navigationController = self.chatControllerInteraction.navigationController() else {
|
|
return
|
|
}
|
|
|
|
|
|
var transitionIn: StoryContainerScreen.TransitionIn?
|
|
|
|
let story = item.story
|
|
var foundItemLayer: SparseItemGridLayer?
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemLayer = item.layer as? ItemLayer else {
|
|
return
|
|
}
|
|
if let listItem = itemLayer.item, listItem.story.id == story.id {
|
|
foundItemLayer = itemLayer
|
|
}
|
|
}
|
|
if let foundItemLayer {
|
|
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
|
|
transitionIn = StoryContainerScreen.TransitionIn(
|
|
sourceView: self.view,
|
|
sourceRect: self.itemGrid.view.convert(itemRect, to: self.view),
|
|
sourceCornerRadius: 0.0
|
|
)
|
|
}
|
|
|
|
let storyContainerScreen = StoryContainerScreen(
|
|
context: self.context,
|
|
initialFocusedId: AnyHashable(peerId),
|
|
initialContent: initialContent,
|
|
transitionIn: transitionIn,
|
|
transitionOut: { [weak self] _, itemId in
|
|
guard let self else {
|
|
return nil
|
|
}
|
|
|
|
var foundItemLayer: SparseItemGridLayer?
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemLayer = item.layer as? ItemLayer else {
|
|
return
|
|
}
|
|
if let listItem = itemLayer.item, AnyHashable(listItem.story.id) == itemId {
|
|
foundItemLayer = itemLayer
|
|
}
|
|
}
|
|
if let foundItemLayer {
|
|
let itemRect = self.itemGrid.frameForItem(layer: foundItemLayer)
|
|
return StoryContainerScreen.TransitionOut(
|
|
destinationView: self.view,
|
|
destinationRect: self.itemGrid.view.convert(itemRect, to: self.view),
|
|
destinationCornerRadius: 0.0,
|
|
destinationIsAvatar: false,
|
|
completed: {}
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
)
|
|
navigationController.pushViewController(storyContainerScreen)
|
|
})*/
|
|
//TODO:open
|
|
//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: PeerInfoScreenNodeProtocol?
|
|
while true {
|
|
if let currentNode = currentNode as? PeerInfoScreenNodeProtocol {
|
|
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 _ = strongSelf
|
|
//let _ = updateVisualMediaStoredState(engine: strongSelf.context.engine, 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 = true
|
|
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 story = itemLayer.item?.story else {
|
|
return
|
|
}
|
|
let rect = strongSelf.itemGrid.frameForItem(layer: itemLayer)
|
|
|
|
//TODO:context menu
|
|
let _ = story
|
|
let _ = rect
|
|
let _ = gesture
|
|
//strongSelf.chatControllerInteraction.openMessageContextActions(message, strongSelf, rect, gesture)
|
|
|
|
strongSelf.itemGrid.cancelGestures()
|
|
}
|
|
|
|
/*self.storedStateDisposable = (visualMediaStoredState(engine: context.engine, 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)
|
|
})*/
|
|
|
|
//TODO:hidden media
|
|
/*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.engine.data.subscribe(EngineDataMap(
|
|
summaries.map { TelegramEngine.EngineData.Item.Messages.MessageCount(peerId: peerId, threadId: chatLocation.threadId, tag: $0) }
|
|
))
|
|
|> map { summaries -> (ContentType, [MessageTags: Int32]) in
|
|
var result: [MessageTags: Int32] = [:]
|
|
for (key, count) in summaries {
|
|
result[key.tag] = count.flatMap(Int32.init) ?? 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, key: .media)
|
|
} else if photoCount != 0 {
|
|
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_PhotoCount(Int32(photoCount)), isActivity: false, key: .media)
|
|
} else if videoCount != 0 {
|
|
return PeerInfoStatusData(text: presentationData.strings.SharedMedia_VideoCount(Int32(videoCount)), isActivity: false, key: .media)
|
|
} 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, key: .media)
|
|
} 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, key: .media)
|
|
} 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, key: .gifs)
|
|
} 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, key: .files)
|
|
} 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, key: .voice)
|
|
} 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, key: .music)
|
|
} 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)
|
|
})
|
|
|
|
self.requestHistoryAroundVisiblePosition(synchronous: false, reloadAtTop: false)
|
|
}
|
|
|
|
deinit {
|
|
self.listDisposable.dispose()
|
|
self.hiddenMediaDisposable?.dispose()
|
|
self.animationTimer?.invalidate()
|
|
self.presentationDataDisposable?.dispose()
|
|
}
|
|
|
|
public func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError> {
|
|
//TODO:load more
|
|
/*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
|
|
}*/
|
|
|
|
return .never()
|
|
}
|
|
|
|
public func updateContentType(contentType: ContentType) {
|
|
/*if self.contentType == contentType {
|
|
return
|
|
}
|
|
self.contentType = contentType
|
|
self.contentTypePromise.set(contentType)
|
|
|
|
self.itemGrid.hideScrollingArea()
|
|
|
|
var threadId: Int64?
|
|
if case let .replyThread(message) = chatLocation {
|
|
threadId = Int64(message.messageId.id)
|
|
}
|
|
|
|
self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, threadId: threadId, tag: tagMaskForType(self.contentType))
|
|
self.isRequestingView = false
|
|
self.requestHistoryAroundVisiblePosition(synchronous: true, reloadAtTop: true)*/
|
|
}
|
|
|
|
public func updateZoomLevel(level: ZoomLevel) {
|
|
self.itemGrid.setZoomLevel(level: level.value)
|
|
|
|
//let _ = updateVisualMediaStoredState(engine: self.context.engine, peerId: self.peerId, messageTag: self.stateTag, state: VisualMediaStoredState(zoomLevel: level.rawValue)).start()
|
|
}
|
|
|
|
public 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] state in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let timezoneOffset = Int32(TimeZone.current.secondsFromGMT())
|
|
|
|
var mappedItems: [SparseItemGrid.Item] = []
|
|
let mappedHoles: [SparseItemGrid.HoleAnchor] = []
|
|
var totalCount: Int = 0
|
|
if let peerReference = state.peerReference {
|
|
for item in state.items {
|
|
mappedItems.append(VisualMediaItem(
|
|
index: mappedItems.count,
|
|
peer: peerReference,
|
|
story: item,
|
|
localMonthTimestamp: Month(localTimestamp: item.timestamp + timezoneOffset).packedValue
|
|
))
|
|
}
|
|
}
|
|
totalCount = state.totalCount
|
|
totalCount = max(mappedItems.count, totalCount)
|
|
|
|
Queue.mainQueue().async { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
let items = SparseItemGrid.Items(
|
|
items: mappedItems,
|
|
holeAnchors: mappedHoles,
|
|
count: 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))
|
|
}
|
|
}
|
|
|
|
public func scrollToTop() -> Bool {
|
|
return self.itemGrid.scrollToTop()
|
|
}
|
|
|
|
public func hitTestResultForScrolling() -> UIView? {
|
|
return self.itemGrid.hitTestResultForScrolling()
|
|
}
|
|
|
|
public func brieflyDisableTouchActions() {
|
|
self.itemGrid.brieflyDisableTouchActions()
|
|
}
|
|
|
|
public func findLoadedMessage(id: MessageId) -> Message? {
|
|
return nil
|
|
}
|
|
|
|
public func updateHiddenMedia() {
|
|
//TODO: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
|
|
itemLayer.updateHasSpoiler(hasSpoiler: false)
|
|
self.itemGridBinding.revealedSpoilerMessageIds.insert(item.message.id)
|
|
} else {
|
|
itemLayer.isHidden = false
|
|
}
|
|
} else {
|
|
itemLayer.isHidden = false
|
|
}
|
|
}*/
|
|
}
|
|
|
|
public func transferVelocity(_ velocity: CGFloat) {
|
|
self.itemGrid.transferVelocity(velocity)
|
|
}
|
|
|
|
public func cancelPreviewGestures() {
|
|
}
|
|
|
|
public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
return nil
|
|
|
|
/*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
|
|
if let contents = itemLayer.getContents() {
|
|
if let image = contents as? UIImage {
|
|
proxyNode.contents = image.cgImage
|
|
} else {
|
|
proxyNode.contents = 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*/
|
|
}
|
|
|
|
public func addToTransitionSurface(view: UIView) {
|
|
self.itemGrid.addToTransitionSurface(view: view)
|
|
}
|
|
|
|
private var gridSelectionGesture: MediaPickerGridSelectionGesture<EngineMessage.Id>?
|
|
|
|
override public func didLoad() {
|
|
super.didLoad()
|
|
|
|
let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
|
selectionRecognizer.shouldBegin = {
|
|
return true
|
|
}
|
|
self.view.addGestureRecognizer(selectionRecognizer)
|
|
}
|
|
|
|
private var selectionPanState: (selecting: Bool, initialMessageId: EngineMessage.Id, toggledMessageIds: [[EngineMessage.Id]])?
|
|
private var selectionScrollActivationTimer: SwiftSignalKit.Timer?
|
|
private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator?
|
|
private var selectionScrollDelta: CGFloat?
|
|
private var selectionLastLocation: CGPoint?
|
|
|
|
private func storyAtPoint(_ location: CGPoint) -> StoryViewList.Item? {
|
|
return nil
|
|
}
|
|
|
|
@objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void {
|
|
//TODO:selection
|
|
/*let location = recognizer.location(in: self.view)
|
|
switch recognizer.state {
|
|
case .began:
|
|
if let message = self.messageAtPoint(location) {
|
|
let selecting = !(self.chatControllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false)
|
|
self.selectionPanState = (selecting, message.id, [])
|
|
self.chatControllerInteraction.toggleMessagesSelection([message.id], selecting)
|
|
}
|
|
case .changed:
|
|
self.handlePanSelection(location: location)
|
|
self.selectionLastLocation = location
|
|
case .ended, .failed, .cancelled:
|
|
self.selectionPanState = nil
|
|
self.selectionScrollDisplayLink = nil
|
|
self.selectionScrollActivationTimer?.invalidate()
|
|
self.selectionScrollActivationTimer = nil
|
|
self.selectionScrollDelta = nil
|
|
self.selectionLastLocation = nil
|
|
self.selectionScrollSkipUpdate = false
|
|
case .possible:
|
|
break
|
|
@unknown default:
|
|
fatalError()
|
|
}*/
|
|
}
|
|
|
|
private func handlePanSelection(location: CGPoint) {
|
|
/*var location = location
|
|
if location.y < 0.0 {
|
|
location.y = 5.0
|
|
} else if location.y > self.frame.height {
|
|
location.y = self.frame.height - 5.0
|
|
}
|
|
|
|
var hasState = false
|
|
if let state = self.selectionPanState {
|
|
hasState = true
|
|
if let message = self.messageAtPoint(location) {
|
|
if message.id == state.initialMessageId {
|
|
if !state.toggledMessageIds.isEmpty {
|
|
self.chatControllerInteraction.toggleMessagesSelection(state.toggledMessageIds.flatMap { $0.compactMap({ $0 }) }, !state.selecting)
|
|
self.selectionPanState = (state.selecting, state.initialMessageId, [])
|
|
}
|
|
} else if state.toggledMessageIds.last?.first != message.id {
|
|
var updatedToggledMessageIds: [[EngineMessage.Id]] = []
|
|
var previouslyToggled = false
|
|
for i in (0 ..< state.toggledMessageIds.count) {
|
|
if let messageId = state.toggledMessageIds[i].first {
|
|
if messageId == message.id {
|
|
previouslyToggled = true
|
|
updatedToggledMessageIds = Array(state.toggledMessageIds.prefix(i + 1))
|
|
|
|
let messageIdsToToggle = Array(state.toggledMessageIds.suffix(state.toggledMessageIds.count - i - 1)).flatMap { $0 }
|
|
self.chatControllerInteraction.toggleMessagesSelection(messageIdsToToggle, !state.selecting)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !previouslyToggled {
|
|
updatedToggledMessageIds = state.toggledMessageIds
|
|
let isSelected = self.chatControllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false
|
|
if state.selecting != isSelected {
|
|
updatedToggledMessageIds.append([message.id])
|
|
self.chatControllerInteraction.toggleMessagesSelection([message.id], state.selecting)
|
|
}
|
|
}
|
|
|
|
self.selectionPanState = (state.selecting, state.initialMessageId, updatedToggledMessageIds)
|
|
}
|
|
}
|
|
}
|
|
guard hasState else {
|
|
return
|
|
}
|
|
let scrollingAreaHeight: CGFloat = 50.0
|
|
if location.y < scrollingAreaHeight || location.y > self.frame.height - scrollingAreaHeight {
|
|
if location.y < self.frame.height / 2.0 {
|
|
self.selectionScrollDelta = (scrollingAreaHeight - location.y) / scrollingAreaHeight
|
|
} else {
|
|
self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - location.y)))) / scrollingAreaHeight
|
|
}
|
|
if let displayLink = self.selectionScrollDisplayLink {
|
|
displayLink.isPaused = false
|
|
} else {
|
|
if let _ = self.selectionScrollActivationTimer {
|
|
} else {
|
|
let timer = SwiftSignalKit.Timer(timeout: 0.45, repeat: false, completion: { [weak self] in
|
|
self?.setupSelectionScrolling()
|
|
}, queue: .mainQueue())
|
|
timer.start()
|
|
self.selectionScrollActivationTimer = timer
|
|
}
|
|
}
|
|
} else {
|
|
self.selectionScrollDisplayLink?.isPaused = true
|
|
self.selectionScrollActivationTimer?.invalidate()
|
|
self.selectionScrollActivationTimer = nil
|
|
}*/
|
|
}
|
|
|
|
private var selectionScrollSkipUpdate = false
|
|
private func setupSelectionScrolling() {
|
|
self.selectionScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
|
|
self?.selectionScrollActivationTimer = nil
|
|
if let strongSelf = self, let delta = strongSelf.selectionScrollDelta {
|
|
let distance: CGFloat = 15.0 * min(1.0, 0.15 + abs(delta * delta))
|
|
let direction: ListViewScrollDirection = delta > 0.0 ? .up : .down
|
|
let _ = strongSelf.itemGrid.scrollWithDelta(direction == .up ? -distance : distance)
|
|
|
|
if let location = strongSelf.selectionLastLocation {
|
|
if !strongSelf.selectionScrollSkipUpdate {
|
|
strongSelf.handlePanSelection(location: location)
|
|
}
|
|
strongSelf.selectionScrollSkipUpdate = !strongSelf.selectionScrollSkipUpdate
|
|
}
|
|
}
|
|
})
|
|
self.selectionScrollDisplayLink?.isPaused = false
|
|
}
|
|
|
|
override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
let location = gestureRecognizer.location(in: gestureRecognizer.view)
|
|
if location.x < 44.0 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer {
|
|
otherGestureRecognizer.isEnabled = false
|
|
otherGestureRecognizer.isEnabled = true
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
public func updateSelectedMessages(animated: Bool) {
|
|
/*switch self.contentType {
|
|
case .files, .music, .voiceAndVideoMessages:
|
|
self.itemGrid.forEachVisibleItem { item in
|
|
guard let itemView = item.view as? ItemView, let (size, topInset, sideInset, bottomInset, _, _, _, _) = self.currentParams 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: CGSize(width: size.width, height: itemView.bounds.height),
|
|
insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset)
|
|
)
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
|
|
let isSelecting = self.chatControllerInteraction.selectionState != nil
|
|
self.itemGrid.pinchEnabled = !isSelecting
|
|
|
|
if isSelecting {
|
|
if self.gridSelectionGesture == nil {
|
|
let selectionGesture = MediaPickerGridSelectionGesture<EngineMessage.Id>()
|
|
selectionGesture.delegate = self
|
|
selectionGesture.sideInset = 44.0
|
|
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
|
|
self?.itemGrid.isScrollEnabled = isEnabled
|
|
}
|
|
selectionGesture.itemAt = { [weak self] point in
|
|
if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let messageId = itemLayer.item?.message.id {
|
|
return (messageId, strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(messageId) ?? false)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
selectionGesture.updateSelection = { [weak self] messageId, selected in
|
|
if let strongSelf = self {
|
|
strongSelf.chatControllerInteraction.toggleMessagesSelection([messageId], selected)
|
|
}
|
|
}
|
|
self.itemGrid.view.addGestureRecognizer(selectionGesture)
|
|
self.gridSelectionGesture = selectionGesture
|
|
}
|
|
} else if let gridSelectionGesture = self.gridSelectionGesture {
|
|
self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture)
|
|
self.gridSelectionGesture = nil
|
|
}
|
|
}*/
|
|
}
|
|
|
|
public 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?
|
|
let isList = false
|
|
switch self.contentType {
|
|
default:
|
|
fixedItemHeight = nil
|
|
}
|
|
|
|
let fixedItemAspect: CGFloat? = 9.0 / 16.0
|
|
|
|
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none)
|
|
}
|
|
}
|
|
|
|
public 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.story.timestamp)
|
|
} else {
|
|
timestamp = item.story.timestamp
|
|
}
|
|
}
|
|
}
|
|
return timestamp
|
|
}
|
|
|
|
public 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.story.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 = SimpleLayer()
|
|
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()
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func scrollToItem(index: Int) {
|
|
guard let _ = self.items else {
|
|
return
|
|
}
|
|
self.itemGrid.scrollToItem(at: index)
|
|
}
|
|
|
|
override public 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
|
|
}
|
|
|
|
public func availableZoomLevels() -> (decrement: ZoomLevel?, increment: ZoomLevel?) {
|
|
let levels = self.itemGrid.availableZoomLevels()
|
|
return (levels.decrement.flatMap(ZoomLevel.init), levels.increment.flatMap(ZoomLevel.init))
|
|
}
|
|
}
|
|
|
|
private class MediaListSelectionRecognizer: UIPanGestureRecognizer {
|
|
private let selectionGestureActivationThreshold: CGFloat = 5.0
|
|
|
|
var recognized: Bool? = nil
|
|
var initialLocation: CGPoint = CGPoint()
|
|
|
|
public var shouldBegin: (() -> Bool)?
|
|
|
|
public override init(target: Any?, action: Selector?) {
|
|
super.init(target: target, action: action)
|
|
|
|
self.minimumNumberOfTouches = 2
|
|
self.maximumNumberOfTouches = 2
|
|
}
|
|
|
|
public override func reset() {
|
|
super.reset()
|
|
|
|
self.recognized = nil
|
|
}
|
|
|
|
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesBegan(touches, with: event)
|
|
|
|
if let shouldBegin = self.shouldBegin, !shouldBegin() {
|
|
self.state = .failed
|
|
} else {
|
|
let touch = touches.first!
|
|
self.initialLocation = touch.location(in: self.view)
|
|
}
|
|
}
|
|
|
|
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
let location = touches.first!.location(in: self.view)
|
|
let translation = location.offsetBy(dx: -self.initialLocation.x, dy: -self.initialLocation.y)
|
|
|
|
let touchesArray = Array(touches)
|
|
if self.recognized == nil, touchesArray.count == 2 {
|
|
if let firstTouch = touchesArray.first, let secondTouch = touchesArray.last {
|
|
let firstLocation = firstTouch.location(in: self.view)
|
|
let secondLocation = secondTouch.location(in: self.view)
|
|
|
|
func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat {
|
|
let dx = v1.x - v2.x
|
|
let dy = v1.y - v2.y
|
|
return sqrt(dx * dx + dy * dy)
|
|
}
|
|
if distance(firstLocation, secondLocation) > 200.0 {
|
|
self.state = .failed
|
|
}
|
|
}
|
|
if self.state != .failed && (abs(translation.y) >= selectionGestureActivationThreshold) {
|
|
self.recognized = true
|
|
}
|
|
}
|
|
|
|
if let recognized = self.recognized, recognized {
|
|
super.touchesMoved(touches, with: event)
|
|
}
|
|
}
|
|
}
|