mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Shared media improvements
This commit is contained in:
parent
021f3c57b3
commit
fe82f7020e
@ -2,6 +2,9 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class ComponentHostView<EnvironmentType>: UIView {
|
||||
private var currentComponent: AnyComponent<EnvironmentType>?
|
||||
private var currentContainerSize: CGSize?
|
||||
private var currentSize: CGSize?
|
||||
private var componentView: UIView?
|
||||
private(set) var isUpdating: Bool = false
|
||||
|
||||
@ -14,7 +17,16 @@ public final class ComponentHostView<EnvironmentType>: UIView {
|
||||
}
|
||||
|
||||
public func update(transition: Transition, component: AnyComponent<EnvironmentType>, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>, containerSize: CGSize) -> CGSize {
|
||||
self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, containerSize: containerSize)
|
||||
if let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize {
|
||||
if currentContainerSize == containerSize && currentComponent == component {
|
||||
return currentSize
|
||||
}
|
||||
}
|
||||
self.currentComponent = component
|
||||
self.currentContainerSize = containerSize
|
||||
let size = self._update(transition: transition, component: component, maybeEnvironment: environment, updateEnvironment: true, containerSize: containerSize)
|
||||
self.currentSize = size
|
||||
return size
|
||||
}
|
||||
|
||||
private func _update(transition: Transition, component: AnyComponent<EnvironmentType>, maybeEnvironment: () -> Environment<EnvironmentType>, updateEnvironment: Bool, containerSize: CGSize) -> CGSize {
|
||||
|
||||
@ -79,6 +79,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
case experimentalCompatibility(Bool)
|
||||
case enableDebugDataDisplay(Bool)
|
||||
case acceleratedStickers(Bool)
|
||||
case mockICE(Bool)
|
||||
case playerEmbedding(Bool)
|
||||
case playlistPlayback(Bool)
|
||||
case voiceConference
|
||||
@ -100,7 +101,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return DebugControllerSection.logging.rawValue
|
||||
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers:
|
||||
case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .mockICE:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .preferredVideoCodec:
|
||||
return DebugControllerSection.videoExperiments.rawValue
|
||||
@ -169,14 +170,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return 27
|
||||
case .acceleratedStickers:
|
||||
return 29
|
||||
case .playerEmbedding:
|
||||
case .mockICE:
|
||||
return 30
|
||||
case .playlistPlayback:
|
||||
case .playerEmbedding:
|
||||
return 31
|
||||
case .voiceConference:
|
||||
case .playlistPlayback:
|
||||
return 32
|
||||
case .voiceConference:
|
||||
return 33
|
||||
case let .preferredVideoCodec(index, _, _, _):
|
||||
return 33 + index
|
||||
return 34 + index
|
||||
case .disableVideoAspectScaling:
|
||||
return 100
|
||||
case .enableVoipTcp:
|
||||
@ -749,6 +752,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .mockICE(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "mockICE", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in
|
||||
var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
|
||||
settings.mockICE = value
|
||||
return PreferencesEntry(settings)
|
||||
})
|
||||
}).start()
|
||||
})
|
||||
case let .playerEmbedding(value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction in
|
||||
@ -861,6 +874,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
||||
entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility))
|
||||
entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay))
|
||||
entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers))
|
||||
entries.append(.mockICE(experimentalSettings.mockICE))
|
||||
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
|
||||
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ swift_library(
|
||||
"//submodules/TinyThumbnail:TinyThumbnail",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/FastBlur:FastBlur",
|
||||
"//submodules/MozjpegBinding:MozjpegBinding",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -6,6 +6,7 @@ import UIKit
|
||||
import TinyThumbnail
|
||||
import Display
|
||||
import FastBlur
|
||||
import MozjpegBinding
|
||||
|
||||
private func generateBlurredThumbnail(image: UIImage) -> UIImage? {
|
||||
let thumbnailContextSize = CGSize(width: 32.0, height: 32.0)
|
||||
@ -22,6 +23,84 @@ private func generateBlurredThumbnail(image: UIImage) -> UIImage? {
|
||||
return thumbnailContext.generateImage()
|
||||
}
|
||||
|
||||
private func storeImage(context: DrawingContext, to path: String) -> UIImage? {
|
||||
if context.size.width <= 70.0 && context.size.height <= 70.0 {
|
||||
guard let file = ManagedFile(queue: nil, path: path, mode: .readwrite) else {
|
||||
return nil
|
||||
}
|
||||
var header: UInt32 = 0xcaf1
|
||||
let _ = file.write(&header, count: 4)
|
||||
var width: UInt16 = UInt16(context.size.width)
|
||||
let _ = file.write(&width, count: 2)
|
||||
var height: UInt16 = UInt16(context.size.height)
|
||||
let _ = file.write(&height, count: 2)
|
||||
var bytesPerRow: UInt16 = UInt16(context.bytesPerRow)
|
||||
let _ = file.write(&bytesPerRow, count: 2)
|
||||
|
||||
let _ = file.write(context.bytes, count: context.length)
|
||||
|
||||
return context.generateImage()
|
||||
} else {
|
||||
guard let image = context.generateImage(), let resultData = image.jpegData(compressionQuality: 0.7) else {
|
||||
return nil
|
||||
}
|
||||
let _ = try? resultData.write(to: URL(fileURLWithPath: path))
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImage(data: Data) -> UIImage? {
|
||||
if data.count > 4 + 2 + 2 + 2 {
|
||||
var header: UInt32 = 0
|
||||
withUnsafeMutableBytes(of: &header, { header in
|
||||
data.copyBytes(to: header.baseAddress!.assumingMemoryBound(to: UInt8.self), from: 0 ..< 4)
|
||||
})
|
||||
if header == 0xcaf1 {
|
||||
var width: UInt16 = 0
|
||||
withUnsafeMutableBytes(of: &width, { width in
|
||||
data.copyBytes(to: width.baseAddress!.assumingMemoryBound(to: UInt8.self), from: 4 ..< (4 + 2))
|
||||
})
|
||||
var height: UInt16 = 0
|
||||
withUnsafeMutableBytes(of: &height, { height in
|
||||
data.copyBytes(to: height.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (4 + 2) ..< (4 + 2 + 2))
|
||||
})
|
||||
var bytesPerRow: UInt16 = 0
|
||||
withUnsafeMutableBytes(of: &bytesPerRow, { bytesPerRow in
|
||||
data.copyBytes(to: bytesPerRow.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (4 + 2 + 2) ..< (4 + 2 + 2 + 2))
|
||||
})
|
||||
|
||||
let imageData = data.subdata(in: (4 + 2 + 2 + 2) ..< data.count)
|
||||
guard let dataProvider = CGDataProvider(data: imageData as CFData) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let image = CGImage(
|
||||
width: Int(width),
|
||||
height: Int(height),
|
||||
bitsPerComponent: DeviceGraphicsContextSettings.shared.bitsPerComponent,
|
||||
bitsPerPixel: DeviceGraphicsContextSettings.shared.bitsPerPixel,
|
||||
bytesPerRow: Int(bytesPerRow),
|
||||
space: DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
bitmapInfo: DeviceGraphicsContextSettings.shared.opaqueBitmapInfo,
|
||||
provider: dataProvider,
|
||||
decode: nil,
|
||||
shouldInterpolate: true,
|
||||
intent: .defaultIntent
|
||||
) {
|
||||
return UIImage(cgImage: image, scale: 1.0, orientation: .up)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let decompressedImage = decompressImage(data) {
|
||||
return decompressedImage
|
||||
}
|
||||
|
||||
return UIImage(data: data)
|
||||
}
|
||||
|
||||
public final class DirectMediaImageCache {
|
||||
public final class GetMediaResult {
|
||||
public let image: UIImage?
|
||||
@ -65,16 +144,17 @@ public final class DirectMediaImageCache {
|
||||
}
|
||||
|> take(1)).start(next: { data in
|
||||
if let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: dataValue) {
|
||||
if let scaledImage = generateImage(CGSize(width: CGFloat(width), height: CGFloat(width)), contextGenerator: { size, context in
|
||||
let filledSize = image.size.aspectFilled(size)
|
||||
let imageRect = CGRect(origin: CGPoint(x: (size.width - filledSize.width) / 2.0, y: (size.height - filledSize.height) / 2.0), size: filledSize)
|
||||
let scaledSize = CGSize(width: CGFloat(width), height: CGFloat(width))
|
||||
let scaledContext = DrawingContext(size: scaledSize, scale: 1.0, opaque: true)
|
||||
scaledContext.withFlippedContext { context in
|
||||
let filledSize = image.size.aspectFilled(scaledSize)
|
||||
let imageRect = CGRect(origin: CGPoint(x: (scaledSize.width - filledSize.width) / 2.0, y: (scaledSize.height - filledSize.height) / 2.0), size: filledSize)
|
||||
context.draw(image.cgImage!, in: imageRect)
|
||||
}, scale: 1.0) {
|
||||
if let resultData = scaledImage.jpegData(compressionQuality: 0.7) {
|
||||
let _ = try? resultData.write(to: URL(fileURLWithPath: cachePath))
|
||||
subscriber.putNext(scaledImage)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
if let scaledImage = storeImage(context: scaledContext, to: cachePath) {
|
||||
subscriber.putNext(scaledImage)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -113,14 +193,14 @@ public final class DirectMediaImageCache {
|
||||
}
|
||||
|
||||
if let resource = resource {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: width)))), let image = UIImage(data: data) {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .square(width: width)))), let image = loadImage(data: data) {
|
||||
return GetMediaResult(image: image, loadSignal: nil)
|
||||
}
|
||||
|
||||
var blurredImage: UIImage?
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .blurredThumbnail))), let image = UIImage(data: data) {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.id, imageType: .blurredThumbnail))), let image = loadImage(data: data) {
|
||||
blurredImage = image
|
||||
} else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = UIImage(data: data) {
|
||||
} else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data) {
|
||||
if let blurredImageValue = generateBlurredThumbnail(image: image) {
|
||||
blurredImage = blurredImageValue
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ private class TimerTargetWrapper: NSObject {
|
||||
|
||||
private let beginDelay: Double = 0.12
|
||||
|
||||
private func cancelParentGestures(view: UIView) {
|
||||
public func cancelParentGestures(view: UIView) {
|
||||
if let gestureRecognizers = view.gestureRecognizers {
|
||||
for recognizer in gestureRecognizers {
|
||||
recognizer.state = .failed
|
||||
@ -31,6 +31,9 @@ private func cancelParentGestures(view: UIView) {
|
||||
if let node = (view as? ListViewBackingView)?.target {
|
||||
node.cancelSelection()
|
||||
}
|
||||
if let node = view.asyncdisplaykit_node as? HighlightTrackingButtonNode {
|
||||
node.highligthedChanged(false)
|
||||
}
|
||||
if let superview = view.superview {
|
||||
cancelParentGestures(view: superview)
|
||||
}
|
||||
|
||||
@ -184,7 +184,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
private let playbackStatusDisposable = MetaDisposable()
|
||||
private let playbackStatus = Promise<MediaPlayerStatus>()
|
||||
|
||||
private var downloadStatusIconNode: DownloadIconNode
|
||||
private var downloadStatusIconNode: DownloadIconNode?
|
||||
private var linearProgressNode: LinearProgressNode?
|
||||
|
||||
private var context: AccountContext?
|
||||
@ -216,15 +216,19 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
|
||||
self.textNode = TextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.descriptionNode = TextNode()
|
||||
self.descriptionNode.displaysAsynchronously = false
|
||||
self.descriptionNode.isUserInteractionEnabled = false
|
||||
|
||||
self.descriptionProgressNode = ImmediateTextNode()
|
||||
self.descriptionProgressNode.displaysAsynchronously = false
|
||||
self.descriptionProgressNode.isUserInteractionEnabled = false
|
||||
self.descriptionProgressNode.maximumNumberOfLines = 1
|
||||
|
||||
@ -237,6 +241,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
self.extensionIconNode.displayWithoutProcessing = true
|
||||
|
||||
self.extensionIconText = TextNode()
|
||||
self.extensionIconText.displaysAsynchronously = false
|
||||
self.extensionIconText.isUserInteractionEnabled = false
|
||||
|
||||
self.iconImageNode = TransformImageNode()
|
||||
@ -246,8 +251,6 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
|
||||
self.iconStatusNode.isUserInteractionEnabled = false
|
||||
|
||||
self.downloadStatusIconNode = DownloadIconNode()
|
||||
|
||||
self.restrictionNode = ASDisplayNode()
|
||||
self.restrictionNode.isHidden = true
|
||||
|
||||
@ -275,6 +278,8 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
|
||||
cancelParentGestures(view: strongSelf.view)
|
||||
|
||||
item.interaction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
|
||||
}
|
||||
@ -647,9 +652,9 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - rightInset - dateNodeLayout.size.width - 4.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - rightInset - dateNodeLayout.size.width - 4.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (textNodeLayout, textNodeApply) = textNodeMakeLayout(TextNodeLayoutArguments(attributedString: captionText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (textNodeLayout, textNodeApply) = textNodeMakeLayout(TextNodeLayoutArguments(attributedString: captionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
@ -700,6 +705,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
return (nodeLayout, { animation in
|
||||
if let strongSelf = self {
|
||||
if strongSelf.downloadStatusIconNode == nil {
|
||||
strongSelf.downloadStatusIconNode = DownloadIconNode(theme: item.presentationData.theme.theme)
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
@ -742,8 +751,8 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
strongSelf.linearProgressNode?.updateTheme(theme: item.presentationData.theme.theme)
|
||||
|
||||
strongSelf.restrictionNode.backgroundColor = item.presentationData.theme.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6)
|
||||
|
||||
strongSelf.downloadStatusIconNode.customColor = item.presentationData.theme.theme.list.itemAccentColor
|
||||
|
||||
strongSelf.downloadStatusIconNode?.updateTheme(theme: item.presentationData.theme.theme)
|
||||
}
|
||||
|
||||
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
|
||||
@ -854,8 +863,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
transition.updateFrame(node: strongSelf.downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 3.0, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 18.0) / 2.0)), size: CGSize(width: 18.0, height: 18.0)))
|
||||
|
||||
if let downloadStatusIconNode = strongSelf.downloadStatusIconNode {
|
||||
transition.updateFrame(node: downloadStatusIconNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset - 3.0, y: strongSelf.descriptionNode.frame.minY + floor((strongSelf.descriptionNode.frame.height - 18.0) / 2.0)), size: CGSize(width: 18.0, height: 18.0)))
|
||||
}
|
||||
|
||||
if let updatedFetchControls = updatedFetchControls {
|
||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||
@ -981,6 +992,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
override public func updateSelectionState(animated: Bool) {
|
||||
}
|
||||
|
||||
public func cancelPreviewGesture() {
|
||||
self.containerNode.cancelGesture()
|
||||
}
|
||||
|
||||
private func updateProgressFrame(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let item = self.appliedItem else {
|
||||
@ -1025,11 +1040,13 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
linearProgressNode.updateProgress(value: CGFloat(progress), completion: {})
|
||||
|
||||
var animated = true
|
||||
if self.downloadStatusIconNode.supernode == nil {
|
||||
animated = false
|
||||
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
|
||||
if let downloadStatusIconNode = self.downloadStatusIconNode {
|
||||
if downloadStatusIconNode.supernode == nil {
|
||||
animated = false
|
||||
self.offsetContainerNode.addSubnode(downloadStatusIconNode)
|
||||
}
|
||||
downloadStatusIconNode.enqueueState(.pause, animated: animated)
|
||||
}
|
||||
self.downloadStatusIconNode.enqueueState(.pause, animated: animated)
|
||||
case .Local:
|
||||
if let linearProgressNode = self.linearProgressNode {
|
||||
self.linearProgressNode = nil
|
||||
@ -1039,8 +1056,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
})
|
||||
})
|
||||
}
|
||||
if self.downloadStatusIconNode.supernode != nil {
|
||||
self.downloadStatusIconNode.removeFromSupernode()
|
||||
if let downloadStatusIconNode = self.downloadStatusIconNode {
|
||||
if downloadStatusIconNode.supernode != nil {
|
||||
downloadStatusIconNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
case .Remote:
|
||||
if let linearProgressNode = self.linearProgressNode {
|
||||
@ -1049,12 +1068,14 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
linearProgressNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
var animated = true
|
||||
if self.downloadStatusIconNode.supernode == nil {
|
||||
animated = false
|
||||
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
|
||||
if let downloadStatusIconNode = self.downloadStatusIconNode {
|
||||
var animated = true
|
||||
if downloadStatusIconNode.supernode == nil {
|
||||
animated = false
|
||||
self.offsetContainerNode.addSubnode(downloadStatusIconNode)
|
||||
}
|
||||
downloadStatusIconNode.enqueueState(.download, animated: animated)
|
||||
}
|
||||
self.downloadStatusIconNode.enqueueState(.download, animated: animated)
|
||||
}
|
||||
} else {
|
||||
if let linearProgressNode = self.linearProgressNode {
|
||||
@ -1063,8 +1084,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
linearProgressNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
if self.downloadStatusIconNode.supernode != nil {
|
||||
self.downloadStatusIconNode.removeFromSupernode()
|
||||
if let downloadStatusIconNode = self.downloadStatusIconNode {
|
||||
if downloadStatusIconNode.supernode != nil {
|
||||
downloadStatusIconNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1090,7 +1113,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
transition.updateFrame(node: self.descriptionProgressNode, frame: CGRect(origin: self.descriptionNode.frame.origin, size: descriptionSize))
|
||||
}
|
||||
|
||||
func activateMedia() {
|
||||
public func activateMedia() {
|
||||
self.progressPressed()
|
||||
}
|
||||
|
||||
@ -1288,20 +1311,55 @@ private enum DownloadIconNodeState: Equatable {
|
||||
case pause
|
||||
}
|
||||
|
||||
private final class DownloadIconNode: ManagedAnimationNode {
|
||||
private func generateDownloadIcon(color: UIColor) -> UIImage? {
|
||||
let animation = ManagedAnimationNode(size: CGSize(width: 18.0, height: 18.0))
|
||||
animation.customColor = color
|
||||
animation.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||
return animation.image
|
||||
}
|
||||
|
||||
private final class DownloadIconNode: ASImageNode {
|
||||
private var customColor: UIColor
|
||||
private let duration: Double = 0.3
|
||||
private var iconState: DownloadIconNodeState = .download
|
||||
private var animationNode: ManagedAnimationNode?
|
||||
|
||||
init() {
|
||||
super.init(size: CGSize(width: 18.0, height: 18.0))
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||
init(theme: PresentationTheme) {
|
||||
self.customColor = theme.list.itemAccentColor
|
||||
|
||||
super.init()
|
||||
|
||||
self.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(theme, generate: {
|
||||
return generateDownloadIcon(color: theme.list.itemAccentColor)
|
||||
})
|
||||
self.contentMode = .center
|
||||
}
|
||||
|
||||
func updateTheme(theme: PresentationTheme) {
|
||||
self.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(theme, generate: {
|
||||
return generateDownloadIcon(color: theme.list.itemAccentColor)
|
||||
})
|
||||
self.customColor = theme.list.itemAccentColor
|
||||
self.animationNode?.customColor = self.customColor
|
||||
}
|
||||
|
||||
func enqueueState(_ state: DownloadIconNodeState, animated: Bool) {
|
||||
guard self.iconState != state else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.animationNode == nil {
|
||||
let animationNode = ManagedAnimationNode(size: CGSize(width: 18.0, height: 18.0))
|
||||
self.animationNode = animationNode
|
||||
animationNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 18.0, height: 18.0))
|
||||
animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||
self.addSubnode(animationNode)
|
||||
self.image = nil
|
||||
}
|
||||
|
||||
guard let animationNode = self.animationNode else {
|
||||
return
|
||||
}
|
||||
|
||||
let previousState = self.iconState
|
||||
self.iconState = state
|
||||
@ -1311,9 +1369,9 @@ private final class DownloadIconNode: ManagedAnimationNode {
|
||||
switch state {
|
||||
case .download:
|
||||
if animated {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 100, endFrame: 120), duration: self.duration))
|
||||
animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 100, endFrame: 120), duration: self.duration))
|
||||
} else {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||
animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||
}
|
||||
case .pause:
|
||||
break
|
||||
@ -1322,9 +1380,9 @@ private final class DownloadIconNode: ManagedAnimationNode {
|
||||
switch state {
|
||||
case .pause:
|
||||
if animated {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
|
||||
animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 20), duration: self.duration))
|
||||
} else {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 60, endFrame: 60), duration: 0.01))
|
||||
animationNode.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 60, endFrame: 60), duration: 0.01))
|
||||
}
|
||||
case .download:
|
||||
break
|
||||
|
||||
@ -48,7 +48,7 @@ public final class ListMessageItem: ListViewItem {
|
||||
let chatLocation: ChatLocation
|
||||
let interaction: ListMessageItemInteraction
|
||||
let message: Message
|
||||
let selection: ChatHistoryMessageSelection
|
||||
public let selection: ChatHistoryMessageSelection
|
||||
let hintIsLink: Bool
|
||||
let isGlobalSearchResult: Bool
|
||||
|
||||
|
||||
@ -3,3 +3,4 @@
|
||||
NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage);
|
||||
NSArray<NSNumber *> * _Nonnull extractJPEGDataScans(NSData * _Nonnull data);
|
||||
NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size);
|
||||
UIImage * _Nullable decompressImage(NSData * _Nonnull sourceData);
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#import <mozjpeg/turbojpeg.h>
|
||||
#import <mozjpeg/jpeglib.h>
|
||||
#import <Accelerate/Accelerate.h>
|
||||
|
||||
static NSData *getHeaderPattern() {
|
||||
static NSData *value = nil;
|
||||
@ -253,3 +254,80 @@ NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size)
|
||||
|
||||
return serializedData;
|
||||
}
|
||||
|
||||
UIImage * _Nullable decompressImage(NSData * _Nonnull sourceData) {
|
||||
long unsigned int jpegSize = sourceData.length;
|
||||
unsigned char *_compressedImage = (unsigned char *)sourceData.bytes;
|
||||
|
||||
int jpegSubsamp, width, height;
|
||||
|
||||
tjhandle _jpegDecompressor = tjInitDecompress();
|
||||
|
||||
tjDecompressHeader2(_jpegDecompressor, _compressedImage, jpegSize, &width, &height, &jpegSubsamp);
|
||||
|
||||
int sourceBytesPerRow = (3 * width + 31) & ~0x1F;
|
||||
int targetBytesPerRow = (4 * width + 31) & ~0x1F;
|
||||
|
||||
unsigned char *buffer = malloc(sourceBytesPerRow * height);
|
||||
|
||||
tjDecompress2(_jpegDecompressor, _compressedImage, jpegSize, buffer, width, sourceBytesPerRow, height, TJPF_RGB, TJFLAG_FASTDCT | TJFLAG_FASTUPSAMPLE);
|
||||
|
||||
tjDestroy(_jpegDecompressor);
|
||||
|
||||
vImage_Buffer source;
|
||||
source.width = width;
|
||||
source.height = height;
|
||||
source.rowBytes = sourceBytesPerRow;
|
||||
source.data = buffer;
|
||||
|
||||
vImage_Buffer target;
|
||||
target.width = width;
|
||||
target.height = height;
|
||||
target.rowBytes = targetBytesPerRow;
|
||||
|
||||
unsigned char *targetBuffer = malloc(targetBytesPerRow * height);
|
||||
target.data = targetBuffer;
|
||||
|
||||
vImageConvert_RGB888toARGB8888(&source, nil, 0xff, &target, false, kvImageDoNotTile);
|
||||
|
||||
free(buffer);
|
||||
|
||||
vImage_Buffer permuteTarget;
|
||||
permuteTarget.width = width;
|
||||
permuteTarget.height = height;
|
||||
permuteTarget.rowBytes = targetBytesPerRow;
|
||||
|
||||
unsigned char *permuteTargetBuffer = malloc(targetBytesPerRow * height);
|
||||
permuteTarget.data = permuteTargetBuffer;
|
||||
|
||||
const uint8_t permuteMap[4] = {3,2,1,0};
|
||||
vImagePermuteChannels_ARGB8888(&target, &permuteTarget, permuteMap, kvImageDoNotTile);
|
||||
|
||||
free(targetBuffer);
|
||||
|
||||
NSData *resultData = [[NSData alloc] initWithBytesNoCopy:permuteTargetBuffer length:targetBytesPerRow * height deallocator:^(void * _Nonnull bytes, __unused NSUInteger length) {
|
||||
free(bytes);
|
||||
}];
|
||||
|
||||
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)resultData);
|
||||
|
||||
static CGColorSpaceRef imageColorSpace;
|
||||
static CGBitmapInfo bitmapInfo;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0);
|
||||
UIImage *refImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
imageColorSpace = CGColorSpaceRetain(CGImageGetColorSpace(refImage.CGImage));
|
||||
bitmapInfo = CGImageGetBitmapInfo(refImage.CGImage);
|
||||
UIGraphicsEndImageContext();
|
||||
});
|
||||
|
||||
CGImageRef cgImg = CGImageCreate(width, height, 8, 32, targetBytesPerRow, imageColorSpace, bitmapInfo, dataProvider, NULL, true, kCGRenderingIntentDefault);
|
||||
|
||||
CGDataProviderRelease(dataProvider);
|
||||
|
||||
UIImage *resultImage = [[UIImage alloc] initWithCGImage:cgImg];
|
||||
CGImageRelease(cgImg);
|
||||
|
||||
return resultImage;
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ swift_library(
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/AnimationUI:AnimationUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -13,17 +13,32 @@ private let nullAction = NullActionClass()
|
||||
|
||||
public protocol SparseItemGridLayer: CALayer {
|
||||
func update(size: CGSize)
|
||||
func needsShimmer() -> Bool
|
||||
}
|
||||
|
||||
public protocol SparseItemGridView: UIView {
|
||||
func update(size: CGSize)
|
||||
func needsShimmer() -> Bool
|
||||
}
|
||||
|
||||
public protocol SparseItemGridDisplayItem: AnyObject {
|
||||
var layer: SparseItemGridLayer? { get }
|
||||
var view: SparseItemGridView? { get }
|
||||
}
|
||||
|
||||
public protocol SparseItemGridBinding: AnyObject {
|
||||
func createLayer() -> SparseItemGridLayer
|
||||
func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridLayer])
|
||||
func createLayer() -> SparseItemGridLayer?
|
||||
func createView() -> SparseItemGridView?
|
||||
func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridDisplayItem])
|
||||
func unbindLayer(layer: SparseItemGridLayer)
|
||||
func scrollerTextForTag(tag: Int32) -> String?
|
||||
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError>
|
||||
func onTap(item: SparseItemGrid.Item)
|
||||
func onTagTap()
|
||||
func didScroll()
|
||||
func coveringInsetOffsetUpdated(transition: ContainedViewLayoutTransition)
|
||||
func onBeginFastScrolling()
|
||||
func getShimmerColors() -> SparseItemGrid.ShimmerColors
|
||||
}
|
||||
|
||||
private func binarySearch(_ inputArr: [SparseItemGrid.Item], searchItem: Int) -> (index: Int?, lowerBound: Int?, upperBound: Int?) {
|
||||
@ -78,24 +93,69 @@ private func binarySearch(_ inputArr: [SparseItemGrid.HoleAnchor], searchItem: I
|
||||
}
|
||||
}
|
||||
|
||||
public final class SparseItemGrid: ASDisplayNode {
|
||||
public final class ShimmerLayer: CAGradientLayer {
|
||||
override public init() {
|
||||
super.init()
|
||||
private final class Shimmer {
|
||||
private var image: UIImage?
|
||||
private var colors: SparseItemGrid.ShimmerColors = SparseItemGrid.ShimmerColors(background: 0, foreground: 0)
|
||||
|
||||
self.backgroundColor = UIColor(white: 0.9, alpha: 1.0).cgColor
|
||||
func update(colors: SparseItemGrid.ShimmerColors, layer: CALayer, containerSize: CGSize, frame: CGRect) {
|
||||
if self.colors != colors {
|
||||
self.colors = colors
|
||||
|
||||
self.image = generateImage(CGSize(width: 1.0, height: 320.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor(rgb: colors.background).cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.clip(to: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let transparentColor = UIColor(argb: colors.foreground).withAlphaComponent(0.0).cgColor
|
||||
let peakColor = UIColor(argb: colors.foreground).cgColor
|
||||
|
||||
var locations: [CGFloat] = [0.0, 0.5, 1.0]
|
||||
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
if let image = self.image {
|
||||
layer.contents = image.cgImage
|
||||
|
||||
override public func action(forKey event: String) -> CAAction? {
|
||||
let shiftedContentsRect = CGRect(origin: CGPoint(x: frame.minX / containerSize.width, y: frame.minY / containerSize.height), size: CGSize(width: frame.width / containerSize.width, height: frame.height / containerSize.height))
|
||||
let _ = shiftedContentsRect
|
||||
layer.contentsRect = shiftedContentsRect
|
||||
|
||||
if layer.animation(forKey: "shimmer") == nil {
|
||||
let animation = CABasicAnimation(keyPath: "contentsRect.origin.y")
|
||||
animation.fromValue = 1.0 as NSNumber
|
||||
animation.toValue = -1.0 as NSNumber
|
||||
animation.isAdditive = true
|
||||
animation.repeatCount = .infinity
|
||||
animation.duration = 0.8
|
||||
animation.beginTime = 1.0
|
||||
layer.add(animation, forKey: "shimmer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class Layer: CALayer {
|
||||
override func action(forKey event: String) -> CAAction? {
|
||||
return nullAction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
self.endPoint = CGPoint(x: 0.0, y: size.height)
|
||||
public final class SparseItemGrid: ASDisplayNode {
|
||||
public struct ShimmerColors: Equatable {
|
||||
public var background: UInt32
|
||||
public var foreground: UInt32
|
||||
|
||||
public init(background: UInt32, foreground: UInt32) {
|
||||
self.background = background
|
||||
self.foreground = foreground
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,40 +318,84 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
|
||||
private final class Viewport: ASDisplayNode, UIScrollViewDelegate {
|
||||
final class VisibleItemLayer {
|
||||
let layer: SparseItemGridLayer
|
||||
final class VisibleItem: SparseItemGridDisplayItem {
|
||||
let layer: SparseItemGridLayer?
|
||||
let view: SparseItemGridView?
|
||||
|
||||
init(layer: SparseItemGridLayer) {
|
||||
init(layer: SparseItemGridLayer?, view: SparseItemGridView?) {
|
||||
self.layer = layer
|
||||
self.view = view
|
||||
}
|
||||
|
||||
var displayLayer: CALayer {
|
||||
if let layer = self.layer {
|
||||
return layer
|
||||
} else if let view = self.view {
|
||||
return view.layer
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
var frame: CGRect {
|
||||
get {
|
||||
return self.displayLayer.frame
|
||||
} set(value) {
|
||||
if let layer = self.layer {
|
||||
layer.frame = value
|
||||
} else if let view = self.view {
|
||||
view.frame = value
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var needsShimmer: Bool {
|
||||
if let layer = self.layer {
|
||||
return layer.needsShimmer()
|
||||
} else if let view = self.view {
|
||||
return view.needsShimmer()
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class Layout {
|
||||
let containerLayout: ContainerLayout
|
||||
let itemSize: CGFloat
|
||||
let itemSize: CGSize
|
||||
let itemSpacing: CGFloat
|
||||
let lastItemSize: CGFloat
|
||||
let itemsPerRow: Int
|
||||
|
||||
init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel) {
|
||||
self.containerLayout = containerLayout
|
||||
self.itemSpacing = 1.0
|
||||
if let fixedItemHeight = containerLayout.fixedItemHeight {
|
||||
self.itemsPerRow = 1
|
||||
self.itemSize = CGSize(width: containerLayout.size.width, height: fixedItemHeight)
|
||||
self.lastItemSize = containerLayout.size.width
|
||||
self.itemSpacing = 0.0
|
||||
} else {
|
||||
self.itemSpacing = 1.0
|
||||
|
||||
let width = containerLayout.size.width
|
||||
let baseItemWidth = floor(min(150.0, width / 3.0))
|
||||
let unclippedItemWidth = (CGFloat(zoomLevel.rawValue) / 100.0) * baseItemWidth
|
||||
let itemsPerRow = floor(width / unclippedItemWidth)
|
||||
self.itemsPerRow = Int(itemsPerRow)
|
||||
self.itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow)
|
||||
let width = containerLayout.size.width
|
||||
let baseItemWidth = floor(min(150.0, width / 3.0))
|
||||
let unclippedItemWidth = (CGFloat(zoomLevel.rawValue) / 100.0) * baseItemWidth
|
||||
let itemsPerRow = floor(width / unclippedItemWidth)
|
||||
self.itemsPerRow = Int(itemsPerRow)
|
||||
let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow)
|
||||
self.itemSize = CGSize(width: itemSize, height: itemSize)
|
||||
|
||||
self.lastItemSize = width - (self.itemSize + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
|
||||
self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
|
||||
}
|
||||
}
|
||||
|
||||
func frame(at index: Int) -> CGRect {
|
||||
let row = index / self.itemsPerRow
|
||||
let column = index % self.itemsPerRow
|
||||
|
||||
return CGRect(origin: CGPoint(x: CGFloat(column) * (self.itemSize + self.itemSpacing), y: CGFloat(row) * (self.itemSize + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize, height: itemSize))
|
||||
return CGRect(origin: CGPoint(x: CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: self.containerLayout.insets.top + CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height))
|
||||
}
|
||||
|
||||
func contentHeight(count: Int) -> CGFloat {
|
||||
@ -299,9 +403,10 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
|
||||
func visibleItemRange(for rect: CGRect, count: Int) -> (minIndex: Int, maxIndex: Int) {
|
||||
var minVisibleRow = Int(floor((rect.minY - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
|
||||
let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.containerLayout.insets.top)
|
||||
var minVisibleRow = Int(floor((offsetRect.minY - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
let maxVisibleRow = Int(ceil((rect.maxY - self.itemSpacing) / (self.itemSize + itemSpacing)))
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.itemSpacing) / (self.itemSize.height + itemSpacing)))
|
||||
|
||||
let minVisibleIndex = minVisibleRow * self.itemsPerRow
|
||||
let maxVisibleIndex = min(count - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
|
||||
@ -313,16 +418,22 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
let zoomLevel: ZoomLevel
|
||||
|
||||
private let scrollView: UIScrollView
|
||||
private let shimmer: Shimmer
|
||||
|
||||
var layout: Layout?
|
||||
var items: Items?
|
||||
var visibleItems: [AnyHashable: VisibleItemLayer] = [:]
|
||||
var visiblePlaceholders: [ShimmerLayer] = []
|
||||
var visibleItems: [AnyHashable: VisibleItem] = [:]
|
||||
var visiblePlaceholders: [Shimmer.Layer] = []
|
||||
|
||||
private var scrollingArea: SparseItemGridScrollingArea?
|
||||
private var currentScrollingTag: Int32?
|
||||
private let maybeLoadHoleAnchor: (HoleAnchor, HoleLocation) -> Void
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var isFastScrolling: Bool = false
|
||||
|
||||
private var previousScrollOffset: CGFloat = 0.0
|
||||
var coveringInsetOffset: CGFloat = 0.0
|
||||
|
||||
init(zoomLevel: ZoomLevel, maybeLoadHoleAnchor: @escaping (HoleAnchor, HoleLocation) -> Void) {
|
||||
self.zoomLevel = zoomLevel
|
||||
@ -338,6 +449,8 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.clipsToBounds = false
|
||||
|
||||
self.shimmer = Shimmer()
|
||||
|
||||
super.init()
|
||||
|
||||
self.anchorPoint = CGPoint()
|
||||
@ -355,9 +468,101 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
self.items?.itemBinding.didScroll()
|
||||
}
|
||||
|
||||
@objc func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateVisibleItems(resetScrolling: false, restoreScrollPosition: nil)
|
||||
|
||||
if let layout = self.layout, let items = self.items {
|
||||
let offset = scrollView.contentOffset.y
|
||||
let delta = offset - self.previousScrollOffset
|
||||
self.previousScrollOffset = offset
|
||||
|
||||
if self.isFastScrolling {
|
||||
if offset <= layout.containerLayout.insets.top {
|
||||
var coveringInsetOffset = self.coveringInsetOffset + delta
|
||||
if coveringInsetOffset < 0.0 {
|
||||
coveringInsetOffset = 0.0
|
||||
}
|
||||
if coveringInsetOffset > layout.containerLayout.insets.top {
|
||||
coveringInsetOffset = layout.containerLayout.insets.top
|
||||
}
|
||||
if offset <= 0.0 {
|
||||
coveringInsetOffset = 0.0
|
||||
}
|
||||
if coveringInsetOffset < self.coveringInsetOffset {
|
||||
self.coveringInsetOffset = coveringInsetOffset
|
||||
items.itemBinding.coveringInsetOffsetUpdated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var coveringInsetOffset = self.coveringInsetOffset + delta
|
||||
if coveringInsetOffset < 0.0 {
|
||||
coveringInsetOffset = 0.0
|
||||
}
|
||||
if coveringInsetOffset > layout.containerLayout.insets.top {
|
||||
coveringInsetOffset = layout.containerLayout.insets.top
|
||||
}
|
||||
if offset <= 0.0 {
|
||||
coveringInsetOffset = 0.0
|
||||
}
|
||||
if coveringInsetOffset != self.coveringInsetOffset {
|
||||
self.coveringInsetOffset = coveringInsetOffset
|
||||
items.itemBinding.coveringInsetOffsetUpdated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.snapCoveringInsetOffset()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !self.ignoreScrolling {
|
||||
if !decelerate {
|
||||
self.snapCoveringInsetOffset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.snapCoveringInsetOffset()
|
||||
}
|
||||
}
|
||||
|
||||
private func snapCoveringInsetOffset() {
|
||||
if let layout = self.layout, let items = self.items {
|
||||
let offset = self.scrollView.contentOffset.y
|
||||
if offset < layout.containerLayout.insets.top {
|
||||
if offset <= layout.containerLayout.insets.top / 2.0 {
|
||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||
} else {
|
||||
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: layout.containerLayout.insets.top), animated: true)
|
||||
}
|
||||
} else {
|
||||
var coveringInsetOffset = self.coveringInsetOffset
|
||||
if coveringInsetOffset > layout.containerLayout.insets.top / 2.0 {
|
||||
coveringInsetOffset = layout.containerLayout.insets.top
|
||||
} else {
|
||||
coveringInsetOffset = 0.0
|
||||
}
|
||||
if offset <= 0.0 {
|
||||
coveringInsetOffset = 0.0
|
||||
}
|
||||
|
||||
if coveringInsetOffset != self.coveringInsetOffset {
|
||||
self.coveringInsetOffset = coveringInsetOffset
|
||||
items.itemBinding.coveringInsetOffsetUpdated(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -369,7 +574,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
let localPoint = self.scrollView.convert(point, from: self.view)
|
||||
|
||||
for (id, visibleItem) in self.visibleItems {
|
||||
if visibleItem.layer.frame.contains(localPoint) {
|
||||
if visibleItem.frame.contains(localPoint) {
|
||||
for item in items.items {
|
||||
if item.id == id {
|
||||
return item
|
||||
@ -391,7 +596,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
|
||||
var closestItem: (CGFloat, AnyHashable)?
|
||||
for (id, visibleItem) in self.visibleItems {
|
||||
let itemCenter = visibleItem.layer.frame.center
|
||||
let itemCenter = visibleItem.frame.center
|
||||
let distanceX = itemCenter.x - localPoint.x
|
||||
let distanceY = itemCenter.y - localPoint.y
|
||||
let distance2 = distanceX * distanceX + distanceY * distanceY
|
||||
@ -446,12 +651,22 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
|
||||
}
|
||||
|
||||
func scrollToTop() -> Bool {
|
||||
if self.scrollView.contentOffset.y > 0.0 {
|
||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVisibleItems(resetScrolling: Bool, restoreScrollPosition: (y: CGFloat, index: Int)?) {
|
||||
guard let layout = self.layout, let items = self.items else {
|
||||
return
|
||||
}
|
||||
|
||||
let contentHeight = layout.contentHeight(count: items.count)
|
||||
let shimmerColors = items.itemBinding.getShimmerColors()
|
||||
|
||||
if resetScrolling {
|
||||
if !self.scrollView.bounds.isEmpty {
|
||||
@ -494,39 +709,45 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
var usedPlaceholderCount = 0
|
||||
if !items.items.isEmpty {
|
||||
var bindItems: [Item] = []
|
||||
var bindLayers: [SparseItemGridLayer] = []
|
||||
var updateLayers: [SparseItemGridLayer] = []
|
||||
var bindLayers: [SparseItemGridDisplayItem] = []
|
||||
var updateLayers: [SparseItemGridDisplayItem] = []
|
||||
|
||||
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
|
||||
for index in visibleRange.minIndex ... visibleRange.maxIndex {
|
||||
if let item = items.item(at: index) {
|
||||
let itemLayer: VisibleItemLayer
|
||||
let itemLayer: VisibleItem
|
||||
if let current = self.visibleItems[item.id] {
|
||||
itemLayer = current
|
||||
updateLayers.append(itemLayer.layer)
|
||||
updateLayers.append(itemLayer)
|
||||
} else {
|
||||
itemLayer = VisibleItemLayer(layer: items.itemBinding.createLayer())
|
||||
itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView())
|
||||
self.visibleItems[item.id] = itemLayer
|
||||
|
||||
bindItems.append(item)
|
||||
bindLayers.append(itemLayer.layer)
|
||||
bindLayers.append(itemLayer)
|
||||
|
||||
self.scrollView.layer.addSublayer(itemLayer.layer)
|
||||
if let layer = itemLayer.layer {
|
||||
self.scrollView.layer.addSublayer(layer)
|
||||
} else if let view = itemLayer.view {
|
||||
self.scrollView.addSubview(view)
|
||||
}
|
||||
}
|
||||
|
||||
validIds.insert(item.id)
|
||||
|
||||
itemLayer.layer.frame = layout.frame(at: index)
|
||||
} else {
|
||||
let placeholderLayer: ShimmerLayer
|
||||
itemLayer.frame = layout.frame(at: index)
|
||||
} else if layout.containerLayout.fixedItemHeight == nil {
|
||||
let placeholderLayer: Shimmer.Layer
|
||||
if self.visiblePlaceholders.count > usedPlaceholderCount {
|
||||
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
|
||||
} else {
|
||||
placeholderLayer = ShimmerLayer()
|
||||
placeholderLayer = Shimmer.Layer()
|
||||
self.scrollView.layer.addSublayer(placeholderLayer)
|
||||
self.visiblePlaceholders.append(placeholderLayer)
|
||||
}
|
||||
placeholderLayer.frame = layout.frame(at: index)
|
||||
let itemFrame = layout.frame(at: index)
|
||||
placeholderLayer.frame = itemFrame
|
||||
self.shimmer.update(colors: shimmerColors, layer: placeholderLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
|
||||
usedPlaceholderCount += 1
|
||||
}
|
||||
}
|
||||
@ -535,8 +756,18 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers)
|
||||
}
|
||||
|
||||
for layer in updateLayers {
|
||||
layer.update(size: layer.bounds.size)
|
||||
for item in updateLayers {
|
||||
let item = item as! VisibleItem
|
||||
if let layer = item.layer {
|
||||
layer.update(size: layer.frame.size)
|
||||
} else if let view = item.view {
|
||||
view.update(size: layer.frame.size)
|
||||
}
|
||||
|
||||
if item.needsShimmer {
|
||||
let itemFrame = layer.frame
|
||||
self.shimmer.update(colors: shimmerColors, layer: item.displayLayer, containerSize: layout.containerLayout.size, frame: itemFrame.offsetBy(dx: 0.0, dy: -visibleBounds.minY))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -547,9 +778,13 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
if let itemLayer = self.visibleItems.removeValue(forKey: id) {
|
||||
items.itemBinding.unbindLayer(layer: itemLayer.layer)
|
||||
itemLayer.layer.removeFromSuperlayer()
|
||||
if let item = self.visibleItems.removeValue(forKey: id) {
|
||||
if let layer = item.layer {
|
||||
items.itemBinding.unbindLayer(layer: layer)
|
||||
layer.removeFromSuperlayer()
|
||||
} else if let view = item.view {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -600,8 +835,17 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
guard let strongSelf = self else {
|
||||
return nil
|
||||
}
|
||||
strongSelf.items?.itemBinding.onBeginFastScrolling()
|
||||
return strongSelf.scrollView
|
||||
}
|
||||
scrollingArea.setContentOffset = { [weak self] offset in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isFastScrolling = true
|
||||
strongSelf.scrollView.setContentOffset(offset, animated: false)
|
||||
strongSelf.isFastScrolling = false
|
||||
}
|
||||
self.updateScrollingArea()
|
||||
}
|
||||
}
|
||||
@ -624,13 +868,20 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
|
||||
if let scrollingArea = self.scrollingArea {
|
||||
let dateString = tag.flatMap { items.itemBinding.scrollerTextForTag(tag: $0) }
|
||||
if self.currentScrollingTag != tag {
|
||||
self.currentScrollingTag = tag
|
||||
if scrollingArea.isDragging {
|
||||
scrollingArea.feedbackTap()
|
||||
}
|
||||
}
|
||||
scrollingArea.update(
|
||||
containerSize: layout.containerLayout.size,
|
||||
containerInsets: layout.containerLayout.insets,
|
||||
contentHeight: contentHeight,
|
||||
contentOffset: self.scrollView.bounds.minY,
|
||||
isScrolling: self.scrollView.isDragging || self.scrollView.isDecelerating,
|
||||
dateString: tag.flatMap { items.itemBinding.scrollerTextForTag(tag: $0) } ?? "",
|
||||
dateString: dateString ?? "",
|
||||
transition: .immediate
|
||||
)
|
||||
}
|
||||
@ -740,8 +991,10 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
var insets: UIEdgeInsets
|
||||
var scrollIndicatorInsets: UIEdgeInsets
|
||||
var lockScrollingAtTop: Bool
|
||||
var fixedItemHeight: CGFloat?
|
||||
}
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
private var pinchRecognizer: UIPinchGestureRecognizer?
|
||||
|
||||
private var containerLayout: ContainerLayout?
|
||||
@ -754,6 +1007,13 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
private var isLoadingHole: Bool = false
|
||||
private let loadingHoleDisposable = MetaDisposable()
|
||||
|
||||
public var coveringInsetOffset: CGFloat {
|
||||
if let currentViewport = self.currentViewport {
|
||||
return currentViewport.coveringInsetOffset
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
override public init() {
|
||||
self.scrollingArea = SparseItemGridScrollingArea()
|
||||
|
||||
@ -762,6 +1022,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
self.clipsToBounds = true
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
self.tapRecognizer = tapRecognizer
|
||||
self.view.addGestureRecognizer(tapRecognizer)
|
||||
|
||||
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:)))
|
||||
@ -936,12 +1197,15 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public func update(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, items: Items) {
|
||||
let containerLayout = ContainerLayout(size: size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop)
|
||||
public func update(size: CGSize, insets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, items: Items) {
|
||||
let containerLayout = ContainerLayout(size: size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight)
|
||||
self.containerLayout = containerLayout
|
||||
self.items = items
|
||||
self.scrollingArea.isHidden = lockScrollingAtTop
|
||||
|
||||
self.tapRecognizer?.isEnabled = fixedItemHeight == nil
|
||||
self.pinchRecognizer?.isEnabled = fixedItemHeight == nil
|
||||
|
||||
if self.currentViewport == nil {
|
||||
let currentViewport = Viewport(zoomLevel: ZoomLevel(rawValue: 100), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in
|
||||
guard let strongSelf = self else {
|
||||
@ -1063,12 +1327,12 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public func forEachVisibleItem(_ f: (SparseItemGridLayer) -> Void) {
|
||||
public func forEachVisibleItem(_ f: (SparseItemGridDisplayItem) -> Void) {
|
||||
guard let currentViewport = self.currentViewport else {
|
||||
return
|
||||
}
|
||||
for (_, itemLayer) in currentViewport.visibleItems {
|
||||
f(itemLayer.layer)
|
||||
f(itemLayer)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1086,7 +1350,18 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
currentViewport.scrollToItem(at: index)
|
||||
}
|
||||
|
||||
public func scrollToTop() -> Bool {
|
||||
guard let currentViewport = self.currentViewport else {
|
||||
return false
|
||||
}
|
||||
return currentViewport.scrollToTop()
|
||||
}
|
||||
|
||||
public func addToTransitionSurface(view: UIView) {
|
||||
self.view.insertSubview(view, belowSubview: self.scrollingArea.view)
|
||||
}
|
||||
|
||||
public func updateScrollingAreaTooltip(tooltip: SparseItemGridScrollingArea.DisplayTooltip) {
|
||||
self.scrollingArea.displayTooltip = tooltip
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,250 @@ import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import AnimationUI
|
||||
|
||||
public final class MultilineText: Component {
|
||||
public let text: String
|
||||
public let font: UIFont
|
||||
public let color: UIColor
|
||||
|
||||
public init(
|
||||
text: String,
|
||||
font: UIFont,
|
||||
color: UIColor
|
||||
) {
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.color = color
|
||||
}
|
||||
|
||||
public static func ==(lhs: MultilineText, rhs: MultilineText) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.font != rhs.font {
|
||||
return false
|
||||
}
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let text: ImmediateTextNode
|
||||
|
||||
init() {
|
||||
self.text = ImmediateTextNode()
|
||||
self.text.maximumNumberOfLines = 0
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubnode(self.text)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: MultilineText, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.text.attributedText = NSAttributedString(string: component.text, font: component.font, textColor: component.color, paragraphAlignment: nil)
|
||||
let textSize = self.text.updateLayout(availableSize)
|
||||
transition.setFrame(view: self.text.view, frame: CGRect(origin: CGPoint(), size: textSize))
|
||||
|
||||
return textSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class LottieAnimationComponent: Component {
|
||||
public let name: String
|
||||
|
||||
public init(
|
||||
name: String
|
||||
) {
|
||||
self.name = name
|
||||
}
|
||||
|
||||
public static func ==(lhs: LottieAnimationComponent, rhs: LottieAnimationComponent) -> Bool {
|
||||
if lhs.name != rhs.name {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var animationNode: AnimationNode?
|
||||
private var currentName: String?
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: LottieAnimationComponent, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
if self.currentName != component.name {
|
||||
self.currentName = component.name
|
||||
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.removeFromSupernode()
|
||||
self.animationNode = nil
|
||||
}
|
||||
|
||||
let animationNode = AnimationNode(animation: component.name, colors: [:], scale: 1.0)
|
||||
self.animationNode = animationNode
|
||||
self.addSubnode(animationNode)
|
||||
|
||||
animationNode.play()
|
||||
}
|
||||
|
||||
if let animationNode = self.animationNode {
|
||||
let preferredSize = animationNode.preferredSize()
|
||||
return preferredSize ?? CGSize(width: 32.0, height: 32.0)
|
||||
} else {
|
||||
return CGSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class TooltipComponent: Component {
|
||||
public let icon: AnyComponent<Empty>?
|
||||
public let content: AnyComponent<Empty>
|
||||
public let arrowLocation: CGRect
|
||||
|
||||
public init(
|
||||
icon: AnyComponent<Empty>?,
|
||||
content: AnyComponent<Empty>,
|
||||
arrowLocation: CGRect
|
||||
) {
|
||||
self.icon = icon
|
||||
self.content = content
|
||||
self.arrowLocation = arrowLocation
|
||||
}
|
||||
|
||||
public static func ==(lhs: TooltipComponent, rhs: TooltipComponent) -> Bool {
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.arrowLocation != rhs.arrowLocation {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private var icon: ComponentHostView<Empty>?
|
||||
private let content: ComponentHostView<Empty>
|
||||
|
||||
init() {
|
||||
self.backgroundNode = NavigationBackgroundNode(color: UIColor(white: 0.2, alpha: 0.7))
|
||||
self.content = ComponentHostView<Empty>()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubview(self.content)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(component: TooltipComponent, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let insets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
|
||||
let spacing: CGFloat = 8.0
|
||||
|
||||
var iconSize: CGSize?
|
||||
if let icon = component.icon {
|
||||
let iconView: ComponentHostView<Empty>
|
||||
if let current = self.icon {
|
||||
iconView = current
|
||||
} else {
|
||||
iconView = ComponentHostView<Empty>()
|
||||
self.icon = iconView
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
iconSize = iconView.update(
|
||||
transition: transition,
|
||||
component: icon,
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
} else if let icon = self.icon {
|
||||
self.icon = nil
|
||||
icon.removeFromSuperview()
|
||||
}
|
||||
|
||||
var contentLeftInset: CGFloat = 0.0
|
||||
if let iconSize = iconSize {
|
||||
contentLeftInset += iconSize.width + spacing
|
||||
}
|
||||
|
||||
let contentSize = self.content.update(
|
||||
transition: transition,
|
||||
component: component.content,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: min(200.0, availableSize.width - contentLeftInset), height: availableSize.height)
|
||||
)
|
||||
|
||||
var innerContentHeight = contentSize.height
|
||||
if let iconSize = iconSize, iconSize.height > innerContentHeight {
|
||||
innerContentHeight = iconSize.height
|
||||
}
|
||||
|
||||
let combinedContentSize = CGSize(width: insets.left + insets.right + contentLeftInset + contentSize.width, height: insets.top + insets.bottom + innerContentHeight)
|
||||
var contentRect = CGRect(origin: CGPoint(x: component.arrowLocation.minX - combinedContentSize.width, y: component.arrowLocation.maxY), size: combinedContentSize)
|
||||
if contentRect.minX < 0.0 {
|
||||
contentRect.origin.x = component.arrowLocation.maxX
|
||||
}
|
||||
if contentRect.minY < 0.0 {
|
||||
contentRect.origin.y = component.arrowLocation.minY - contentRect.height
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundNode.view, frame: contentRect)
|
||||
self.backgroundNode.update(size: contentRect.size, cornerRadius: 8.0, transition: .immediate)
|
||||
|
||||
if let iconSize = iconSize, let icon = self.icon {
|
||||
transition.setFrame(view: icon, frame: CGRect(origin: CGPoint(x: contentRect.minX + insets.left, y: contentRect.minY + insets.top + floor((contentRect.height - insets.top - insets.bottom - iconSize.height) / 2.0)), size: iconSize))
|
||||
}
|
||||
transition.setFrame(view: self.content, frame: CGRect(origin: CGPoint(x: contentRect.minX + insets.left + contentLeftInset, y: contentRect.minY + insets.top + floor((contentRect.height - insets.top - insets.bottom - contentSize.height) / 2.0)), size: contentSize))
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class RoundedRectangle: Component {
|
||||
let color: UIColor
|
||||
@ -324,6 +568,11 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
private let dateIndicator: ComponentHostView<Empty>
|
||||
|
||||
private let lineIndicator: ComponentHostView<Empty>
|
||||
|
||||
private var displayedTooltip: Bool = false
|
||||
private var lineTooltip: ComponentHostView<Empty>?
|
||||
|
||||
private var containerSize: CGSize?
|
||||
private var indicatorPosition: CGFloat?
|
||||
private var scrollIndicatorHeight: CGFloat?
|
||||
|
||||
@ -336,6 +585,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
private var activityTimer: SwiftSignalKit.Timer?
|
||||
|
||||
public var beginScrolling: (() -> UIScrollView?)?
|
||||
public var setContentOffset: ((CGPoint) -> Void)?
|
||||
public var openCurrentDate: (() -> Void)?
|
||||
|
||||
private var offsetBarTimer: SwiftSignalKit.Timer?
|
||||
@ -350,6 +600,20 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
}
|
||||
private var projectionData: ProjectionData?
|
||||
|
||||
public struct DisplayTooltip {
|
||||
public var animation: String?
|
||||
public var text: String
|
||||
public var completed: () -> Void
|
||||
|
||||
public init(animation: String?, text: String, completed: @escaping () -> Void) {
|
||||
self.animation = animation
|
||||
self.text = text
|
||||
self.completed = completed
|
||||
}
|
||||
}
|
||||
|
||||
public var displayTooltip: DisplayTooltip?
|
||||
|
||||
override public init() {
|
||||
self.dateIndicator = ComponentHostView<Empty>()
|
||||
self.lineIndicator = ComponentHostView<Empty>()
|
||||
@ -399,10 +663,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
if let scrollView = strongSelf.beginScrolling?() {
|
||||
strongSelf.draggingScrollView = scrollView
|
||||
strongSelf.scrollingInitialOffset = scrollView.contentOffset.y
|
||||
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
|
||||
strongSelf.setContentOffset?(scrollView.contentOffset)
|
||||
}
|
||||
|
||||
strongSelf.updateActivityTimer()
|
||||
strongSelf.updateActivityTimer(isScrolling: false)
|
||||
},
|
||||
ended: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
@ -424,7 +688,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
|
||||
strongSelf.updateLineIndicator(transition: transition)
|
||||
|
||||
strongSelf.updateActivityTimer()
|
||||
strongSelf.updateActivityTimer(isScrolling: false)
|
||||
},
|
||||
moved: { [weak self] relativeOffset in
|
||||
guard let strongSelf = self else {
|
||||
@ -454,7 +718,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
offset = scrollView.contentSize.height - scrollView.bounds.height
|
||||
}
|
||||
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: offset), animated: false)
|
||||
strongSelf.setContentOffset?(CGPoint(x: 0.0, y: offset))
|
||||
let _ = scrollView
|
||||
let _ = projectionData
|
||||
}
|
||||
@ -473,6 +737,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
self.updateLineIndicator(transition: transition)
|
||||
}
|
||||
|
||||
func feedbackTap() {
|
||||
self.hapticFeedback.tap()
|
||||
}
|
||||
|
||||
public func update(
|
||||
containerSize: CGSize,
|
||||
containerInsets: UIEdgeInsets,
|
||||
@ -482,8 +750,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
dateString: String,
|
||||
transition: ContainedViewLayoutTransition
|
||||
) {
|
||||
self.containerSize = containerSize
|
||||
|
||||
if isScrolling {
|
||||
self.updateActivityTimer()
|
||||
self.updateActivityTimer(isScrolling: true)
|
||||
}
|
||||
|
||||
let indicatorSize = self.dateIndicator.update(
|
||||
@ -508,7 +778,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
}
|
||||
|
||||
let indicatorVerticalInset: CGFloat = 3.0
|
||||
let topIndicatorInset: CGFloat = indicatorVerticalInset
|
||||
let topIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.top
|
||||
let bottomIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.bottom
|
||||
|
||||
let scrollIndicatorHeight = max(35.0, ceil(scrollIndicatorHeightFraction * containerSize.height))
|
||||
@ -539,7 +809,13 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
self.lineIndicator.alpha = 1.0
|
||||
}
|
||||
|
||||
self.updateLineTooltip(containerSize: containerSize)
|
||||
|
||||
self.updateLineIndicator(transition: transition)
|
||||
|
||||
if isScrolling {
|
||||
self.displayTooltipOnFirstScroll()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLineIndicator(transition: ContainedViewLayoutTransition) {
|
||||
@ -567,7 +843,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
transition.updateFrame(view: self.lineIndicator, frame: CGRect(origin: CGPoint(x: self.bounds.size.width - 3.0 - lineIndicatorSize.width, y: indicatorPosition), size: lineIndicatorSize))
|
||||
}
|
||||
|
||||
private func updateActivityTimer() {
|
||||
private func updateActivityTimer(isScrolling: Bool) {
|
||||
self.activityTimer?.invalidate()
|
||||
|
||||
if self.isDragging {
|
||||
@ -582,11 +858,68 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
transition.updateAlpha(layer: strongSelf.dateIndicator.layer, alpha: 0.0)
|
||||
transition.updateAlpha(layer: strongSelf.lineIndicator.layer, alpha: 0.0)
|
||||
|
||||
if let lineTooltip = strongSelf.lineTooltip {
|
||||
strongSelf.lineTooltip = nil
|
||||
lineTooltip.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak lineTooltip] _ in
|
||||
lineTooltip?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}, queue: .mainQueue())
|
||||
self.activityTimer?.start()
|
||||
}
|
||||
}
|
||||
|
||||
private func displayTooltipOnFirstScroll() {
|
||||
guard let displayTooltip = self.displayTooltip else {
|
||||
return
|
||||
}
|
||||
if self.displayedTooltip {
|
||||
return
|
||||
}
|
||||
self.displayedTooltip = true
|
||||
|
||||
let lineTooltip = ComponentHostView<Empty>()
|
||||
self.lineTooltip = lineTooltip
|
||||
self.view.addSubview(lineTooltip)
|
||||
|
||||
if let containerSize = self.containerSize {
|
||||
self.updateLineTooltip(containerSize: containerSize)
|
||||
}
|
||||
|
||||
lineTooltip.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
displayTooltip.completed()
|
||||
}
|
||||
|
||||
private func updateLineTooltip(containerSize: CGSize) {
|
||||
guard let displayTooltip = self.displayTooltip else {
|
||||
return
|
||||
}
|
||||
guard let lineTooltip = self.lineTooltip else {
|
||||
return
|
||||
}
|
||||
let lineTooltipSize = lineTooltip.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(TooltipComponent(
|
||||
icon: displayTooltip.animation.flatMap { animation in
|
||||
AnyComponent(LottieAnimationComponent(
|
||||
name: animation
|
||||
))
|
||||
},
|
||||
content: AnyComponent(MultilineText(
|
||||
text: displayTooltip.text,
|
||||
font: Font.regular(13.0),
|
||||
color: .white
|
||||
)),
|
||||
arrowLocation: self.lineIndicator.frame.insetBy(dx: -4.0, dy: -4.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: containerSize
|
||||
)
|
||||
lineTooltip.frame = CGRect(origin: CGPoint(), size: lineTooltipSize)
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.dateIndicator.alpha <= 0.01 {
|
||||
return nil
|
||||
|
||||
@ -682,8 +682,10 @@ public final class PresentationCallImpl: PresentationCall {
|
||||
self.audioSessionShouldBeActive.set(true)
|
||||
if let _ = audioSessionControl, !wasActive || previousControl == nil {
|
||||
let logName = "\(id.id)_\(id.accessHash)"
|
||||
|
||||
let updatedConnections = connections
|
||||
|
||||
let ongoingContext = OngoingCallContext(account: self.context.account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, derivedState: self.derivedState, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: connections, maxLayer: maxLayer, version: version, allowP2P: allowsP2P, enableTCP: self.enableTCP, enableStunMarking: self.enableStunMarking, audioSessionActive: self.audioSessionActive.get(), logName: logName, preferredVideoCodec: self.preferredVideoCodec)
|
||||
let ongoingContext = OngoingCallContext(account: self.context.account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, derivedState: self.derivedState, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: updatedConnections, maxLayer: maxLayer, version: version, allowP2P: allowsP2P, enableTCP: self.enableTCP, enableStunMarking: self.enableStunMarking, audioSessionActive: self.audioSessionActive.get(), logName: logName, preferredVideoCodec: self.preferredVideoCodec)
|
||||
self.ongoingContext = ongoingContext
|
||||
ongoingContext.setIsMuted(self.isMutedValue)
|
||||
if let requestedVideoAspect = self.requestedVideoAspect {
|
||||
|
||||
@ -232,6 +232,11 @@ private func parseConnection(_ apiConnection: Api.PhoneConnection) -> CallSessio
|
||||
public struct CallSessionConnectionSet {
|
||||
public let primary: CallSessionConnection
|
||||
public let alternatives: [CallSessionConnection]
|
||||
|
||||
public init(primary: CallSessionConnection, alternatives: [CallSessionConnection]) {
|
||||
self.primary = primary
|
||||
self.alternatives = alternatives
|
||||
}
|
||||
}
|
||||
|
||||
private func parseConnectionSet(primary: Api.PhoneConnection, alternative: [Api.PhoneConnection]) -> CallSessionConnectionSet {
|
||||
|
||||
@ -160,6 +160,8 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
|
||||
case chatSpecificThemeLightPreviewTip = 26
|
||||
case chatSpecificThemeDarkPreviewTip = 27
|
||||
case interactiveEmojiSyncTip = 28
|
||||
case sharedMediaScrollingTooltip = 29
|
||||
case sharedMediaFastScrollingTooltip = 30
|
||||
|
||||
var key: ValueBoxKey {
|
||||
let v = ValueBoxKey(length: 4)
|
||||
@ -324,6 +326,14 @@ private struct ApplicationSpecificNoticeKeys {
|
||||
static func dismissedInvitationRequestsNotice(peerId: PeerId) -> NoticeEntryKey {
|
||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: peerInviteRequestsNamespace), key: noticeKey(peerId: peerId, key: 0))
|
||||
}
|
||||
|
||||
static func sharedMediaScrollingTooltip() -> NoticeEntryKey {
|
||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.sharedMediaScrollingTooltip.key)
|
||||
}
|
||||
|
||||
static func sharedMediaFastScrollingTooltip() -> NoticeEntryKey {
|
||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.sharedMediaFastScrollingTooltip.key)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificNotice {
|
||||
@ -893,6 +903,54 @@ public struct ApplicationSpecificNotice {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func getSharedMediaScrollingTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
|
||||
return accountManager.transaction { transaction -> Int32 in
|
||||
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.sharedMediaScrollingTooltip())?.get(ApplicationSpecificCounterNotice.self) {
|
||||
return value.value
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func incrementSharedMediaScrollingTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int32 = 1) -> Signal<Void, NoError> {
|
||||
return accountManager.transaction { transaction -> Void in
|
||||
var currentValue: Int32 = 0
|
||||
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.sharedMediaScrollingTooltip())?.get(ApplicationSpecificCounterNotice.self) {
|
||||
currentValue = value.value
|
||||
}
|
||||
currentValue += count
|
||||
|
||||
if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) {
|
||||
transaction.setNotice(ApplicationSpecificNoticeKeys.sharedMediaScrollingTooltip(), entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func getSharedMediaFastScrollingTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
|
||||
return accountManager.transaction { transaction -> Int32 in
|
||||
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.sharedMediaFastScrollingTooltip())?.get(ApplicationSpecificCounterNotice.self) {
|
||||
return value.value
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func incrementSharedMediaFastScrollingTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int32 = 1) -> Signal<Void, NoError> {
|
||||
return accountManager.transaction { transaction -> Void in
|
||||
var currentValue: Int32 = 0
|
||||
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.sharedMediaFastScrollingTooltip())?.get(ApplicationSpecificCounterNotice.self) {
|
||||
currentValue = value.value
|
||||
}
|
||||
currentValue += count
|
||||
|
||||
if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) {
|
||||
transaction.setNotice(ApplicationSpecificNoticeKeys.sharedMediaFastScrollingTooltip(), entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func dismissedTrendingStickerPacks(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<[Int64]?, NoError> {
|
||||
return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedTrendingStickerPacks())
|
||||
|
||||
@ -679,27 +679,9 @@ public struct PresentationResourcesChat {
|
||||
})
|
||||
}
|
||||
|
||||
public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setStrokeColor(theme.list.itemAccentColor.cgColor)
|
||||
context.setLineWidth(1.67)
|
||||
context.setLineCap(.round)
|
||||
context.setLineJoin(.round)
|
||||
|
||||
context.translateBy(x: 2.0, y: 1.0)
|
||||
|
||||
context.move(to: CGPoint(x: 4.0, y: 0.0))
|
||||
context.addLine(to: CGPoint(x: 4.0, y: 10.0))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 0.0, y: 6.0))
|
||||
context.addLine(to: CGPoint(x: 4.0, y: 10.0))
|
||||
context.addLine(to: CGPoint(x: 8.0, y: 6.0))
|
||||
context.strokePath()
|
||||
})
|
||||
public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme, generate: () -> UIImage?) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { _ in
|
||||
return generate()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,6 @@ import Intents
|
||||
import Postbox
|
||||
import PushKit
|
||||
import AsyncDisplayKit
|
||||
import CloudKit
|
||||
import TelegramUIPreferences
|
||||
import TelegramPresentationData
|
||||
import TelegramCallsUI
|
||||
|
||||
@ -81,6 +81,11 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
var status: Signal<PeerInfoStatusData?, NoError> {
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
var tabBarOffset: CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
private var disposable: Disposable?
|
||||
|
||||
@ -139,7 +144,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
let isFirstLayout = self.currentParams == nil
|
||||
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
|
||||
|
||||
@ -156,7 +161,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
}
|
||||
}
|
||||
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
self.listNode.scrollEnabled = !isScrollingLockedAtTop
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
|
||||
private let listNode: ChatHistoryListNode
|
||||
|
||||
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)?
|
||||
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
|
||||
@ -54,6 +54,11 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
var status: Signal<PeerInfoStatusData?, NoError> {
|
||||
self.statusPromise.get()
|
||||
}
|
||||
|
||||
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
var tabBarOffset: CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) {
|
||||
self.context = context
|
||||
@ -129,8 +134,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
strongSelf.playlistStateAndType = nil
|
||||
}
|
||||
|
||||
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = strongSelf.currentParams {
|
||||
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
if let (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = strongSelf.currentParams {
|
||||
strongSelf.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -180,8 +185,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
self.currentParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
||||
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)
|
||||
|
||||
var topPanelHeight: CGFloat = 0.0
|
||||
if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType {
|
||||
@ -416,11 +421,11 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
})
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.mediaAccessoryPanelContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: MediaNavigationAccessoryHeaderNode.minimizedHeight)))
|
||||
transition.updateFrame(node: self.mediaAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: size.width, height: MediaNavigationAccessoryHeaderNode.minimizedHeight)))
|
||||
|
||||
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topPanelHeight, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve))
|
||||
self.listNode.updateLayout(transition: transition, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topPanelHeight + topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve))
|
||||
if isScrollingLockedAtTop {
|
||||
switch self.listNode.visibleContentOffset() {
|
||||
case let .known(value) where value <= CGFloat.ulpOfOne:
|
||||
|
||||
@ -124,6 +124,11 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
var status: Signal<PeerInfoStatusData?, NoError> {
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
var tabBarOffset: CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
private var disposable: Disposable?
|
||||
|
||||
@ -183,7 +188,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
let isFirstLayout = self.currentParams == nil
|
||||
self.currentParams = (size, isScrollingLockedAtTop)
|
||||
self.presentationDataPromise.set(.single(presentationData))
|
||||
@ -200,7 +205,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: duration), directionHint: .Up)
|
||||
}
|
||||
}
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
self.listNode.scrollEnabled = !isScrollingLockedAtTop
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1044,7 +1044,7 @@ struct PeerInfoHeaderNavigationButtonSpec: Equatable {
|
||||
}
|
||||
|
||||
final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode {
|
||||
private var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
|
||||
private(set) var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
|
||||
|
||||
private var currentButtons: [PeerInfoHeaderNavigationButtonSpec] = []
|
||||
|
||||
|
||||
@ -15,8 +15,10 @@ protocol PeerInfoPaneNode: ASDisplayNode {
|
||||
var parentController: ViewController? { get set }
|
||||
|
||||
var status: Signal<PeerInfoStatusData?, NoError> { get }
|
||||
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)? { get set }
|
||||
var tabBarOffset: CGFloat { get }
|
||||
|
||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
|
||||
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition)
|
||||
func scrollToTop() -> Bool
|
||||
func transferVelocity(_ velocity: CGFloat)
|
||||
func cancelPreviewGestures()
|
||||
@ -32,21 +34,21 @@ final class PeerInfoPaneWrapper {
|
||||
let key: PeerInfoPaneKey
|
||||
let node: PeerInfoPaneNode
|
||||
var isAnimatingOut: Bool = false
|
||||
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, Bool, CGFloat, PresentationData)?
|
||||
private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, CGFloat, Bool, CGFloat, PresentationData)?
|
||||
|
||||
init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) {
|
||||
self.key = key
|
||||
self.node = node
|
||||
}
|
||||
|
||||
func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
if let (currentSize, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams {
|
||||
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData {
|
||||
func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
if let (currentSize, currentTopInset, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams {
|
||||
if currentSize == size && currentTopInset == topInset, currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData {
|
||||
return
|
||||
}
|
||||
}
|
||||
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
||||
self.node.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition)
|
||||
self.appliedParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
||||
self.node.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -401,13 +403,19 @@ private final class PeerInfoPendingPane {
|
||||
paneDidScroll()
|
||||
}
|
||||
case .files:
|
||||
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file)
|
||||
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .files)
|
||||
paneNode = visualPaneNode
|
||||
//paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file)
|
||||
case .links:
|
||||
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .webPage)
|
||||
case .voice:
|
||||
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo)
|
||||
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .voiceAndVideoMessages)
|
||||
paneNode = visualPaneNode
|
||||
//paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo)
|
||||
case .music:
|
||||
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .music)
|
||||
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .music)
|
||||
paneNode = visualPaneNode
|
||||
//paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .music)
|
||||
case .gifs:
|
||||
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .gifs)
|
||||
paneNode = visualPaneNode
|
||||
@ -714,16 +722,12 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
||||
self.coveringBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, transition: .immediate)
|
||||
self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
||||
self.tabsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
|
||||
let isScrollingLockedAtTop = expansionFraction < 1.0 - CGFloat.ulpOfOne
|
||||
|
||||
let tabsHeight: CGFloat = 48.0
|
||||
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel)))
|
||||
transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel)))
|
||||
self.coveringBackgroundNode.update(size: self.coveringBackgroundNode.bounds.size, transition: transition)
|
||||
|
||||
transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel)))
|
||||
|
||||
let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: tabsHeight), size: CGSize(width: size.width, height: size.height - tabsHeight))
|
||||
let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||
|
||||
var visiblePaneIndices: [Int] = []
|
||||
var requiredPendingKeys: [PeerInfoPaneKey] = []
|
||||
@ -794,14 +798,23 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
||||
)
|
||||
self.pendingPanes[key] = pane
|
||||
pane.pane.node.frame = paneFrame
|
||||
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate)
|
||||
pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate)
|
||||
let paneNode = pane.pane.node
|
||||
pane.pane.node.tabBarOffsetUpdated = { [weak self, weak paneNode] transition in
|
||||
guard let strongSelf = self, let paneNode = paneNode, let currentPane = strongSelf.currentPane, paneNode === currentPane.node else {
|
||||
return
|
||||
}
|
||||
if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams {
|
||||
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition)
|
||||
}
|
||||
}
|
||||
leftScope = true
|
||||
}
|
||||
}
|
||||
|
||||
for (key, pane) in self.pendingPanes {
|
||||
pane.pane.node.frame = paneFrame
|
||||
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate)
|
||||
pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate)
|
||||
|
||||
if pane.isReady {
|
||||
self.pendingPanes.removeValue(forKey: key)
|
||||
@ -834,7 +847,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
||||
if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex {
|
||||
var paneWasAdded = false
|
||||
if pane.node.supernode == nil {
|
||||
self.addSubnode(pane.node)
|
||||
self.insertSubnode(pane.node, belowSubnode: self.coveringBackgroundNode)
|
||||
paneWasAdded = true
|
||||
}
|
||||
let indexOffset = CGFloat(index - updatedCurrentIndex)
|
||||
@ -878,13 +891,29 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
||||
paneCompletion()
|
||||
})
|
||||
}
|
||||
pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
|
||||
pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
|
||||
}
|
||||
}
|
||||
|
||||
//print("currentPanes: \(self.currentPanes.map { $0.0 })")
|
||||
|
||||
transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: tabsHeight)))
|
||||
|
||||
var tabsOffset: CGFloat = 0.0
|
||||
if let currentPane = self.currentPane {
|
||||
tabsOffset = currentPane.node.tabBarOffset
|
||||
}
|
||||
tabsOffset = max(0.0, min(tabsHeight, tabsOffset))
|
||||
if isScrollingLockedAtTop {
|
||||
tabsOffset = 0.0
|
||||
}
|
||||
var tabsAlpha = 1.0 - tabsOffset / tabsHeight
|
||||
tabsAlpha *= tabsAlpha
|
||||
transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -tabsOffset), size: CGSize(width: size.width, height: tabsHeight)))
|
||||
transition.updateAlpha(node: self.tabsContainerNode, alpha: tabsAlpha)
|
||||
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel)))
|
||||
transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel)))
|
||||
self.coveringBackgroundNode.update(size: self.coveringBackgroundNode.bounds.size, transition: transition)
|
||||
|
||||
transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel)))
|
||||
|
||||
self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
|
||||
let title: String
|
||||
switch key {
|
||||
@ -911,7 +940,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
||||
for (_, pane) in self.pendingPanes {
|
||||
let paneTransition: ContainedViewLayoutTransition = .immediate
|
||||
paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame)
|
||||
pane.pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: expansionFraction < 1.0 - CGFloat.ulpOfOne, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition)
|
||||
pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition)
|
||||
}
|
||||
if !self.didSetIsReady && data != nil {
|
||||
if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {
|
||||
|
||||
@ -61,6 +61,7 @@ import TelegramCallsUI
|
||||
import PeerInfoAvatarListNode
|
||||
import PasswordSetupUI
|
||||
import CalendarMessageScreen
|
||||
import TooltipUI
|
||||
|
||||
protocol PeerInfoScreenItem: AnyObject {
|
||||
var id: AnyHashable { get }
|
||||
@ -5996,6 +5997,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
|
||||
private weak var mediaGalleryContextMenu: ContextController?
|
||||
|
||||
func displaySharedMediaFastScrollingTooltip() {
|
||||
guard let buttonNode = self.headerNode.navigationButtonContainer.buttonNodes[.more] else {
|
||||
return
|
||||
}
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let buttonFrame = buttonNode.view.convert(buttonNode.bounds, to: self.view)
|
||||
//TODO:localize
|
||||
controller.present(TooltipScreen(account: self.context.account, text: "Tap on this icon for calendar view", style: .default, icon: .none, location: .point(buttonFrame.insetBy(dx: 0.0, dy: 5.0), .top), shouldDismissOnTouch: { point in
|
||||
return .dismiss(consume: false)
|
||||
}), in: .current)
|
||||
}
|
||||
|
||||
private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
@ -6080,8 +6095,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
updatedContentType = .photo
|
||||
case .video:
|
||||
updatedContentType = .photoOrVideo
|
||||
case .gifs:
|
||||
updatedContentType = .gifs
|
||||
default:
|
||||
updatedContentType = pane.contentType
|
||||
}
|
||||
pane.updateContentType(contentType: updatedContentType)
|
||||
})))
|
||||
@ -6104,8 +6119,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
updatedContentType = .photoOrVideo
|
||||
case .video:
|
||||
updatedContentType = .video
|
||||
case .gifs:
|
||||
updatedContentType = .gifs
|
||||
default:
|
||||
updatedContentType = pane.contentType
|
||||
}
|
||||
pane.updateContentType(contentType: updatedContentType)
|
||||
})))
|
||||
|
||||
@ -18,6 +18,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
public var experimentalCompatibility: Bool
|
||||
public var enableDebugDataDisplay: Bool
|
||||
public var acceleratedStickers: Bool
|
||||
public var mockICE: Bool
|
||||
|
||||
public static var defaultSettings: ExperimentalUISettings {
|
||||
return ExperimentalUISettings(
|
||||
@ -34,7 +35,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
enableVoipTcp: false,
|
||||
experimentalCompatibility: false,
|
||||
enableDebugDataDisplay: false,
|
||||
acceleratedStickers: false
|
||||
acceleratedStickers: false,
|
||||
mockICE: false
|
||||
)
|
||||
}
|
||||
|
||||
@ -52,7 +54,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
enableVoipTcp: Bool,
|
||||
experimentalCompatibility: Bool,
|
||||
enableDebugDataDisplay: Bool,
|
||||
acceleratedStickers: Bool
|
||||
acceleratedStickers: Bool,
|
||||
mockICE: Bool
|
||||
) {
|
||||
self.keepChatNavigationStack = keepChatNavigationStack
|
||||
self.skipReadHistory = skipReadHistory
|
||||
@ -68,6 +71,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
self.experimentalCompatibility = experimentalCompatibility
|
||||
self.enableDebugDataDisplay = enableDebugDataDisplay
|
||||
self.acceleratedStickers = acceleratedStickers
|
||||
self.mockICE = mockICE
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@ -87,6 +91,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
self.experimentalCompatibility = (try container.decodeIfPresent(Int32.self, forKey: "experimentalCompatibility") ?? 0) != 0
|
||||
self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 0) != 0
|
||||
self.acceleratedStickers = (try container.decodeIfPresent(Int32.self, forKey: "acceleratedStickers") ?? 0) != 0
|
||||
self.mockICE = (try container.decodeIfPresent(Int32.self, forKey: "mockICE") ?? 0) != 0
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@ -106,6 +111,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
||||
try container.encode((self.experimentalCompatibility ? 1 : 0) as Int32, forKey: "experimentalCompatibility")
|
||||
try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay")
|
||||
try container.encode((self.acceleratedStickers ? 1 : 0) as Int32, forKey: "acceleratedStickers")
|
||||
try container.encode((self.mockICE ? 1 : 0) as Int32, forKey: "mockICE")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user