mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 01:10:09 +00:00
Shared media improvements
This commit is contained in:
parent
021f3c57b3
commit
fe82f7020e
@ -2,6 +2,9 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public final class ComponentHostView<EnvironmentType>: UIView {
|
public final class ComponentHostView<EnvironmentType>: UIView {
|
||||||
|
private var currentComponent: AnyComponent<EnvironmentType>?
|
||||||
|
private var currentContainerSize: CGSize?
|
||||||
|
private var currentSize: CGSize?
|
||||||
private var componentView: UIView?
|
private var componentView: UIView?
|
||||||
private(set) var isUpdating: Bool = false
|
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 {
|
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 {
|
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 experimentalCompatibility(Bool)
|
||||||
case enableDebugDataDisplay(Bool)
|
case enableDebugDataDisplay(Bool)
|
||||||
case acceleratedStickers(Bool)
|
case acceleratedStickers(Bool)
|
||||||
|
case mockICE(Bool)
|
||||||
case playerEmbedding(Bool)
|
case playerEmbedding(Bool)
|
||||||
case playlistPlayback(Bool)
|
case playlistPlayback(Bool)
|
||||||
case voiceConference
|
case voiceConference
|
||||||
@ -100,7 +101,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||||||
return DebugControllerSection.logging.rawValue
|
return DebugControllerSection.logging.rawValue
|
||||||
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
|
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
|
||||||
return DebugControllerSection.experiments.rawValue
|
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
|
return DebugControllerSection.experiments.rawValue
|
||||||
case .preferredVideoCodec:
|
case .preferredVideoCodec:
|
||||||
return DebugControllerSection.videoExperiments.rawValue
|
return DebugControllerSection.videoExperiments.rawValue
|
||||||
@ -169,14 +170,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||||||
return 27
|
return 27
|
||||||
case .acceleratedStickers:
|
case .acceleratedStickers:
|
||||||
return 29
|
return 29
|
||||||
case .playerEmbedding:
|
case .mockICE:
|
||||||
return 30
|
return 30
|
||||||
case .playlistPlayback:
|
case .playerEmbedding:
|
||||||
return 31
|
return 31
|
||||||
case .voiceConference:
|
case .playlistPlayback:
|
||||||
return 32
|
return 32
|
||||||
|
case .voiceConference:
|
||||||
|
return 33
|
||||||
case let .preferredVideoCodec(index, _, _, _):
|
case let .preferredVideoCodec(index, _, _, _):
|
||||||
return 33 + index
|
return 34 + index
|
||||||
case .disableVideoAspectScaling:
|
case .disableVideoAspectScaling:
|
||||||
return 100
|
return 100
|
||||||
case .enableVoipTcp:
|
case .enableVoipTcp:
|
||||||
@ -749,6 +752,16 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
|||||||
})
|
})
|
||||||
}).start()
|
}).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):
|
case let .playerEmbedding(value):
|
||||||
return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
return ItemListSwitchItem(presentationData: presentationData, title: "Player Embedding", value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||||
let _ = arguments.sharedContext.accountManager.transaction ({ transaction 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(.experimentalCompatibility(experimentalSettings.experimentalCompatibility))
|
||||||
entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay))
|
entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay))
|
||||||
entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers))
|
entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers))
|
||||||
|
entries.append(.mockICE(experimentalSettings.mockICE))
|
||||||
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
|
entries.append(.playerEmbedding(experimentalSettings.playerEmbedding))
|
||||||
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
|
entries.append(.playlistPlayback(experimentalSettings.playlistPlayback))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ swift_library(
|
|||||||
"//submodules/TinyThumbnail:TinyThumbnail",
|
"//submodules/TinyThumbnail:TinyThumbnail",
|
||||||
"//submodules/Display:Display",
|
"//submodules/Display:Display",
|
||||||
"//submodules/FastBlur:FastBlur",
|
"//submodules/FastBlur:FastBlur",
|
||||||
|
"//submodules/MozjpegBinding:MozjpegBinding",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import UIKit
|
|||||||
import TinyThumbnail
|
import TinyThumbnail
|
||||||
import Display
|
import Display
|
||||||
import FastBlur
|
import FastBlur
|
||||||
|
import MozjpegBinding
|
||||||
|
|
||||||
private func generateBlurredThumbnail(image: UIImage) -> UIImage? {
|
private func generateBlurredThumbnail(image: UIImage) -> UIImage? {
|
||||||
let thumbnailContextSize = CGSize(width: 32.0, height: 32.0)
|
let thumbnailContextSize = CGSize(width: 32.0, height: 32.0)
|
||||||
@ -22,6 +23,84 @@ private func generateBlurredThumbnail(image: UIImage) -> UIImage? {
|
|||||||
return thumbnailContext.generateImage()
|
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 DirectMediaImageCache {
|
||||||
public final class GetMediaResult {
|
public final class GetMediaResult {
|
||||||
public let image: UIImage?
|
public let image: UIImage?
|
||||||
@ -65,18 +144,19 @@ public final class DirectMediaImageCache {
|
|||||||
}
|
}
|
||||||
|> take(1)).start(next: { data in
|
|> take(1)).start(next: { data in
|
||||||
if let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let image = UIImage(data: dataValue) {
|
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 scaledSize = CGSize(width: CGFloat(width), height: CGFloat(width))
|
||||||
let filledSize = image.size.aspectFilled(size)
|
let scaledContext = DrawingContext(size: scaledSize, scale: 1.0, opaque: true)
|
||||||
let imageRect = CGRect(origin: CGPoint(x: (size.width - filledSize.width) / 2.0, y: (size.height - filledSize.height) / 2.0), size: filledSize)
|
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)
|
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))
|
if let scaledImage = storeImage(context: scaledContext, to: cachePath) {
|
||||||
subscriber.putNext(scaledImage)
|
subscriber.putNext(scaledImage)
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return ActionDisposable {
|
return ActionDisposable {
|
||||||
@ -113,14 +193,14 @@ public final class DirectMediaImageCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let resource = resource {
|
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)
|
return GetMediaResult(image: image, loadSignal: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var blurredImage: UIImage?
|
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
|
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) {
|
if let blurredImageValue = generateBlurredThumbnail(image: image) {
|
||||||
blurredImage = blurredImageValue
|
blurredImage = blurredImageValue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ private class TimerTargetWrapper: NSObject {
|
|||||||
|
|
||||||
private let beginDelay: Double = 0.12
|
private let beginDelay: Double = 0.12
|
||||||
|
|
||||||
private func cancelParentGestures(view: UIView) {
|
public func cancelParentGestures(view: UIView) {
|
||||||
if let gestureRecognizers = view.gestureRecognizers {
|
if let gestureRecognizers = view.gestureRecognizers {
|
||||||
for recognizer in gestureRecognizers {
|
for recognizer in gestureRecognizers {
|
||||||
recognizer.state = .failed
|
recognizer.state = .failed
|
||||||
@ -31,6 +31,9 @@ private func cancelParentGestures(view: UIView) {
|
|||||||
if let node = (view as? ListViewBackingView)?.target {
|
if let node = (view as? ListViewBackingView)?.target {
|
||||||
node.cancelSelection()
|
node.cancelSelection()
|
||||||
}
|
}
|
||||||
|
if let node = view.asyncdisplaykit_node as? HighlightTrackingButtonNode {
|
||||||
|
node.highligthedChanged(false)
|
||||||
|
}
|
||||||
if let superview = view.superview {
|
if let superview = view.superview {
|
||||||
cancelParentGestures(view: superview)
|
cancelParentGestures(view: superview)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -184,7 +184,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
private let playbackStatusDisposable = MetaDisposable()
|
private let playbackStatusDisposable = MetaDisposable()
|
||||||
private let playbackStatus = Promise<MediaPlayerStatus>()
|
private let playbackStatus = Promise<MediaPlayerStatus>()
|
||||||
|
|
||||||
private var downloadStatusIconNode: DownloadIconNode
|
private var downloadStatusIconNode: DownloadIconNode?
|
||||||
private var linearProgressNode: LinearProgressNode?
|
private var linearProgressNode: LinearProgressNode?
|
||||||
|
|
||||||
private var context: AccountContext?
|
private var context: AccountContext?
|
||||||
@ -216,15 +216,19 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
self.highlightedBackgroundNode.isLayerBacked = true
|
self.highlightedBackgroundNode.isLayerBacked = true
|
||||||
|
|
||||||
self.titleNode = TextNode()
|
self.titleNode = TextNode()
|
||||||
|
self.titleNode.displaysAsynchronously = false
|
||||||
self.titleNode.isUserInteractionEnabled = false
|
self.titleNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.textNode = TextNode()
|
self.textNode = TextNode()
|
||||||
|
self.textNode.displaysAsynchronously = false
|
||||||
self.textNode.isUserInteractionEnabled = false
|
self.textNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.descriptionNode = TextNode()
|
self.descriptionNode = TextNode()
|
||||||
|
self.descriptionNode.displaysAsynchronously = false
|
||||||
self.descriptionNode.isUserInteractionEnabled = false
|
self.descriptionNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.descriptionProgressNode = ImmediateTextNode()
|
self.descriptionProgressNode = ImmediateTextNode()
|
||||||
|
self.descriptionProgressNode.displaysAsynchronously = false
|
||||||
self.descriptionProgressNode.isUserInteractionEnabled = false
|
self.descriptionProgressNode.isUserInteractionEnabled = false
|
||||||
self.descriptionProgressNode.maximumNumberOfLines = 1
|
self.descriptionProgressNode.maximumNumberOfLines = 1
|
||||||
|
|
||||||
@ -237,6 +241,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
self.extensionIconNode.displayWithoutProcessing = true
|
self.extensionIconNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
self.extensionIconText = TextNode()
|
self.extensionIconText = TextNode()
|
||||||
|
self.extensionIconText.displaysAsynchronously = false
|
||||||
self.extensionIconText.isUserInteractionEnabled = false
|
self.extensionIconText.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.iconImageNode = TransformImageNode()
|
self.iconImageNode = TransformImageNode()
|
||||||
@ -246,8 +251,6 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
|
self.iconStatusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white)
|
||||||
self.iconStatusNode.isUserInteractionEnabled = false
|
self.iconStatusNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.downloadStatusIconNode = DownloadIconNode()
|
|
||||||
|
|
||||||
self.restrictionNode = ASDisplayNode()
|
self.restrictionNode = ASDisplayNode()
|
||||||
self.restrictionNode.isHidden = true
|
self.restrictionNode.isHidden = true
|
||||||
|
|
||||||
@ -276,6 +279,8 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelParentGestures(view: strongSelf.view)
|
||||||
|
|
||||||
item.interaction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
|
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 (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()))
|
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
|
return (nodeLayout, { animation in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
|
if strongSelf.downloadStatusIconNode == nil {
|
||||||
|
strongSelf.downloadStatusIconNode = DownloadIconNode(theme: item.presentationData.theme.theme)
|
||||||
|
}
|
||||||
|
|
||||||
let transition: ContainedViewLayoutTransition
|
let transition: ContainedViewLayoutTransition
|
||||||
if animation.isAnimated {
|
if animation.isAnimated {
|
||||||
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||||
@ -743,7 +752,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
|
|
||||||
strongSelf.restrictionNode.backgroundColor = item.presentationData.theme.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.6)
|
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 {
|
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
|
||||||
@ -855,7 +864,9 @@ 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 {
|
if let updatedFetchControls = updatedFetchControls {
|
||||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||||
@ -982,6 +993,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
override public func updateSelectionState(animated: Bool) {
|
override public func updateSelectionState(animated: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func cancelPreviewGesture() {
|
||||||
|
self.containerNode.cancelGesture()
|
||||||
|
}
|
||||||
|
|
||||||
private func updateProgressFrame(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
private func updateProgressFrame(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
guard let item = self.appliedItem else {
|
guard let item = self.appliedItem else {
|
||||||
return
|
return
|
||||||
@ -1025,11 +1040,13 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
linearProgressNode.updateProgress(value: CGFloat(progress), completion: {})
|
linearProgressNode.updateProgress(value: CGFloat(progress), completion: {})
|
||||||
|
|
||||||
var animated = true
|
var animated = true
|
||||||
if self.downloadStatusIconNode.supernode == nil {
|
if let downloadStatusIconNode = self.downloadStatusIconNode {
|
||||||
|
if downloadStatusIconNode.supernode == nil {
|
||||||
animated = false
|
animated = false
|
||||||
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
|
self.offsetContainerNode.addSubnode(downloadStatusIconNode)
|
||||||
|
}
|
||||||
|
downloadStatusIconNode.enqueueState(.pause, animated: animated)
|
||||||
}
|
}
|
||||||
self.downloadStatusIconNode.enqueueState(.pause, animated: animated)
|
|
||||||
case .Local:
|
case .Local:
|
||||||
if let linearProgressNode = self.linearProgressNode {
|
if let linearProgressNode = self.linearProgressNode {
|
||||||
self.linearProgressNode = nil
|
self.linearProgressNode = nil
|
||||||
@ -1039,8 +1056,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if self.downloadStatusIconNode.supernode != nil {
|
if let downloadStatusIconNode = self.downloadStatusIconNode {
|
||||||
self.downloadStatusIconNode.removeFromSupernode()
|
if downloadStatusIconNode.supernode != nil {
|
||||||
|
downloadStatusIconNode.removeFromSupernode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .Remote:
|
case .Remote:
|
||||||
if let linearProgressNode = self.linearProgressNode {
|
if let linearProgressNode = self.linearProgressNode {
|
||||||
@ -1049,12 +1068,14 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
linearProgressNode?.removeFromSupernode()
|
linearProgressNode?.removeFromSupernode()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if let downloadStatusIconNode = self.downloadStatusIconNode {
|
||||||
var animated = true
|
var animated = true
|
||||||
if self.downloadStatusIconNode.supernode == nil {
|
if downloadStatusIconNode.supernode == nil {
|
||||||
animated = false
|
animated = false
|
||||||
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
|
self.offsetContainerNode.addSubnode(downloadStatusIconNode)
|
||||||
|
}
|
||||||
|
downloadStatusIconNode.enqueueState(.download, animated: animated)
|
||||||
}
|
}
|
||||||
self.downloadStatusIconNode.enqueueState(.download, animated: animated)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let linearProgressNode = self.linearProgressNode {
|
if let linearProgressNode = self.linearProgressNode {
|
||||||
@ -1063,8 +1084,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
linearProgressNode?.removeFromSupernode()
|
linearProgressNode?.removeFromSupernode()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if self.downloadStatusIconNode.supernode != nil {
|
if let downloadStatusIconNode = self.downloadStatusIconNode {
|
||||||
self.downloadStatusIconNode.removeFromSupernode()
|
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))
|
transition.updateFrame(node: self.descriptionProgressNode, frame: CGRect(origin: self.descriptionNode.frame.origin, size: descriptionSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
func activateMedia() {
|
public func activateMedia() {
|
||||||
self.progressPressed()
|
self.progressPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1288,14 +1311,36 @@ private enum DownloadIconNodeState: Equatable {
|
|||||||
case pause
|
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 let duration: Double = 0.3
|
||||||
private var iconState: DownloadIconNodeState = .download
|
private var iconState: DownloadIconNodeState = .download
|
||||||
|
private var animationNode: ManagedAnimationNode?
|
||||||
|
|
||||||
init() {
|
init(theme: PresentationTheme) {
|
||||||
super.init(size: CGSize(width: 18.0, height: 18.0))
|
self.customColor = theme.list.itemAccentColor
|
||||||
|
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_shareddownload"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
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) {
|
func enqueueState(_ state: DownloadIconNodeState, animated: Bool) {
|
||||||
@ -1303,6 +1348,19 @@ private final class DownloadIconNode: ManagedAnimationNode {
|
|||||||
return
|
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
|
let previousState = self.iconState
|
||||||
self.iconState = state
|
self.iconState = state
|
||||||
|
|
||||||
@ -1311,9 +1369,9 @@ private final class DownloadIconNode: ManagedAnimationNode {
|
|||||||
switch state {
|
switch state {
|
||||||
case .download:
|
case .download:
|
||||||
if animated {
|
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 {
|
} 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:
|
case .pause:
|
||||||
break
|
break
|
||||||
@ -1322,9 +1380,9 @@ private final class DownloadIconNode: ManagedAnimationNode {
|
|||||||
switch state {
|
switch state {
|
||||||
case .pause:
|
case .pause:
|
||||||
if animated {
|
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 {
|
} 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:
|
case .download:
|
||||||
break
|
break
|
||||||
|
|||||||
@ -48,7 +48,7 @@ public final class ListMessageItem: ListViewItem {
|
|||||||
let chatLocation: ChatLocation
|
let chatLocation: ChatLocation
|
||||||
let interaction: ListMessageItemInteraction
|
let interaction: ListMessageItemInteraction
|
||||||
let message: Message
|
let message: Message
|
||||||
let selection: ChatHistoryMessageSelection
|
public let selection: ChatHistoryMessageSelection
|
||||||
let hintIsLink: Bool
|
let hintIsLink: Bool
|
||||||
let isGlobalSearchResult: Bool
|
let isGlobalSearchResult: Bool
|
||||||
|
|
||||||
|
|||||||
@ -3,3 +3,4 @@
|
|||||||
NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage);
|
NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage);
|
||||||
NSArray<NSNumber *> * _Nonnull extractJPEGDataScans(NSData * _Nonnull data);
|
NSArray<NSNumber *> * _Nonnull extractJPEGDataScans(NSData * _Nonnull data);
|
||||||
NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size);
|
NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size);
|
||||||
|
UIImage * _Nullable decompressImage(NSData * _Nonnull sourceData);
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#import <mozjpeg/turbojpeg.h>
|
#import <mozjpeg/turbojpeg.h>
|
||||||
#import <mozjpeg/jpeglib.h>
|
#import <mozjpeg/jpeglib.h>
|
||||||
|
#import <Accelerate/Accelerate.h>
|
||||||
|
|
||||||
static NSData *getHeaderPattern() {
|
static NSData *getHeaderPattern() {
|
||||||
static NSData *value = nil;
|
static NSData *value = nil;
|
||||||
@ -253,3 +254,80 @@ NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size)
|
|||||||
|
|
||||||
return serializedData;
|
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/AsyncDisplayKit:AsyncDisplayKit",
|
||||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
"//submodules/ComponentFlow:ComponentFlow",
|
"//submodules/ComponentFlow:ComponentFlow",
|
||||||
|
"//submodules/AnimationUI:AnimationUI",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -13,17 +13,32 @@ private let nullAction = NullActionClass()
|
|||||||
|
|
||||||
public protocol SparseItemGridLayer: CALayer {
|
public protocol SparseItemGridLayer: CALayer {
|
||||||
func update(size: CGSize)
|
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 {
|
public protocol SparseItemGridBinding: AnyObject {
|
||||||
func createLayer() -> SparseItemGridLayer
|
func createLayer() -> SparseItemGridLayer?
|
||||||
func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridLayer])
|
func createView() -> SparseItemGridView?
|
||||||
|
func bindLayers(items: [SparseItemGrid.Item], layers: [SparseItemGridDisplayItem])
|
||||||
func unbindLayer(layer: SparseItemGridLayer)
|
func unbindLayer(layer: SparseItemGridLayer)
|
||||||
func scrollerTextForTag(tag: Int32) -> String?
|
func scrollerTextForTag(tag: Int32) -> String?
|
||||||
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError>
|
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError>
|
||||||
func onTap(item: SparseItemGrid.Item)
|
func onTap(item: SparseItemGrid.Item)
|
||||||
func onTagTap()
|
func onTagTap()
|
||||||
func didScroll()
|
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?) {
|
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 {
|
private final class Shimmer {
|
||||||
public final class ShimmerLayer: CAGradientLayer {
|
private var image: UIImage?
|
||||||
override public init() {
|
private var colors: SparseItemGrid.ShimmerColors = SparseItemGrid.ShimmerColors(background: 0, foreground: 0)
|
||||||
super.init()
|
|
||||||
|
|
||||||
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) {
|
if let image = self.image {
|
||||||
fatalError("init(coder:) has not been implemented")
|
layer.contents = image.cgImage
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func action(forKey event: String) -> CAAction? {
|
final class Layer: CALayer {
|
||||||
|
override func action(forKey event: String) -> CAAction? {
|
||||||
return nullAction
|
return nullAction
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func update(size: CGSize) {
|
public final class SparseItemGrid: ASDisplayNode {
|
||||||
self.endPoint = CGPoint(x: 0.0, y: size.height)
|
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,23 +318,65 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class Viewport: ASDisplayNode, UIScrollViewDelegate {
|
private final class Viewport: ASDisplayNode, UIScrollViewDelegate {
|
||||||
final class VisibleItemLayer {
|
final class VisibleItem: SparseItemGridDisplayItem {
|
||||||
let layer: SparseItemGridLayer
|
let layer: SparseItemGridLayer?
|
||||||
|
let view: SparseItemGridView?
|
||||||
|
|
||||||
init(layer: SparseItemGridLayer) {
|
init(layer: SparseItemGridLayer?, view: SparseItemGridView?) {
|
||||||
self.layer = layer
|
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 {
|
final class Layout {
|
||||||
let containerLayout: ContainerLayout
|
let containerLayout: ContainerLayout
|
||||||
let itemSize: CGFloat
|
let itemSize: CGSize
|
||||||
let itemSpacing: CGFloat
|
let itemSpacing: CGFloat
|
||||||
let lastItemSize: CGFloat
|
let lastItemSize: CGFloat
|
||||||
let itemsPerRow: Int
|
let itemsPerRow: Int
|
||||||
|
|
||||||
init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel) {
|
init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel) {
|
||||||
self.containerLayout = containerLayout
|
self.containerLayout = containerLayout
|
||||||
|
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
|
self.itemSpacing = 1.0
|
||||||
|
|
||||||
let width = containerLayout.size.width
|
let width = containerLayout.size.width
|
||||||
@ -282,16 +384,18 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
let unclippedItemWidth = (CGFloat(zoomLevel.rawValue) / 100.0) * baseItemWidth
|
let unclippedItemWidth = (CGFloat(zoomLevel.rawValue) / 100.0) * baseItemWidth
|
||||||
let itemsPerRow = floor(width / unclippedItemWidth)
|
let itemsPerRow = floor(width / unclippedItemWidth)
|
||||||
self.itemsPerRow = Int(itemsPerRow)
|
self.itemsPerRow = Int(itemsPerRow)
|
||||||
self.itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / 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 {
|
func frame(at index: Int) -> CGRect {
|
||||||
let row = index / self.itemsPerRow
|
let row = index / self.itemsPerRow
|
||||||
let column = 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 {
|
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) {
|
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)
|
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 minVisibleIndex = minVisibleRow * self.itemsPerRow
|
||||||
let maxVisibleIndex = min(count - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
|
let maxVisibleIndex = min(count - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
|
||||||
@ -313,16 +418,22 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
let zoomLevel: ZoomLevel
|
let zoomLevel: ZoomLevel
|
||||||
|
|
||||||
private let scrollView: UIScrollView
|
private let scrollView: UIScrollView
|
||||||
|
private let shimmer: Shimmer
|
||||||
|
|
||||||
var layout: Layout?
|
var layout: Layout?
|
||||||
var items: Items?
|
var items: Items?
|
||||||
var visibleItems: [AnyHashable: VisibleItemLayer] = [:]
|
var visibleItems: [AnyHashable: VisibleItem] = [:]
|
||||||
var visiblePlaceholders: [ShimmerLayer] = []
|
var visiblePlaceholders: [Shimmer.Layer] = []
|
||||||
|
|
||||||
private var scrollingArea: SparseItemGridScrollingArea?
|
private var scrollingArea: SparseItemGridScrollingArea?
|
||||||
|
private var currentScrollingTag: Int32?
|
||||||
private let maybeLoadHoleAnchor: (HoleAnchor, HoleLocation) -> Void
|
private let maybeLoadHoleAnchor: (HoleAnchor, HoleLocation) -> Void
|
||||||
|
|
||||||
private var ignoreScrolling: Bool = false
|
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) {
|
init(zoomLevel: ZoomLevel, maybeLoadHoleAnchor: @escaping (HoleAnchor, HoleLocation) -> Void) {
|
||||||
self.zoomLevel = zoomLevel
|
self.zoomLevel = zoomLevel
|
||||||
@ -338,6 +449,8 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
self.scrollView.delaysContentTouches = false
|
self.scrollView.delaysContentTouches = false
|
||||||
self.scrollView.clipsToBounds = false
|
self.scrollView.clipsToBounds = false
|
||||||
|
|
||||||
|
self.shimmer = Shimmer()
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.anchorPoint = CGPoint()
|
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) {
|
@objc func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
if !self.ignoreScrolling {
|
if !self.ignoreScrolling {
|
||||||
self.updateVisibleItems(resetScrolling: false, restoreScrollPosition: nil)
|
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)
|
let localPoint = self.scrollView.convert(point, from: self.view)
|
||||||
|
|
||||||
for (id, visibleItem) in self.visibleItems {
|
for (id, visibleItem) in self.visibleItems {
|
||||||
if visibleItem.layer.frame.contains(localPoint) {
|
if visibleItem.frame.contains(localPoint) {
|
||||||
for item in items.items {
|
for item in items.items {
|
||||||
if item.id == id {
|
if item.id == id {
|
||||||
return item
|
return item
|
||||||
@ -391,7 +596,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
|
|
||||||
var closestItem: (CGFloat, AnyHashable)?
|
var closestItem: (CGFloat, AnyHashable)?
|
||||||
for (id, visibleItem) in self.visibleItems {
|
for (id, visibleItem) in self.visibleItems {
|
||||||
let itemCenter = visibleItem.layer.frame.center
|
let itemCenter = visibleItem.frame.center
|
||||||
let distanceX = itemCenter.x - localPoint.x
|
let distanceX = itemCenter.x - localPoint.x
|
||||||
let distanceY = itemCenter.y - localPoint.y
|
let distanceY = itemCenter.y - localPoint.y
|
||||||
let distance2 = distanceX * distanceX + distanceY * distanceY
|
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)
|
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)?) {
|
private func updateVisibleItems(resetScrolling: Bool, restoreScrollPosition: (y: CGFloat, index: Int)?) {
|
||||||
guard let layout = self.layout, let items = self.items else {
|
guard let layout = self.layout, let items = self.items else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentHeight = layout.contentHeight(count: items.count)
|
let contentHeight = layout.contentHeight(count: items.count)
|
||||||
|
let shimmerColors = items.itemBinding.getShimmerColors()
|
||||||
|
|
||||||
if resetScrolling {
|
if resetScrolling {
|
||||||
if !self.scrollView.bounds.isEmpty {
|
if !self.scrollView.bounds.isEmpty {
|
||||||
@ -494,39 +709,45 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
var usedPlaceholderCount = 0
|
var usedPlaceholderCount = 0
|
||||||
if !items.items.isEmpty {
|
if !items.items.isEmpty {
|
||||||
var bindItems: [Item] = []
|
var bindItems: [Item] = []
|
||||||
var bindLayers: [SparseItemGridLayer] = []
|
var bindLayers: [SparseItemGridDisplayItem] = []
|
||||||
var updateLayers: [SparseItemGridLayer] = []
|
var updateLayers: [SparseItemGridDisplayItem] = []
|
||||||
|
|
||||||
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
|
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
|
||||||
for index in visibleRange.minIndex ... visibleRange.maxIndex {
|
for index in visibleRange.minIndex ... visibleRange.maxIndex {
|
||||||
if let item = items.item(at: index) {
|
if let item = items.item(at: index) {
|
||||||
let itemLayer: VisibleItemLayer
|
let itemLayer: VisibleItem
|
||||||
if let current = self.visibleItems[item.id] {
|
if let current = self.visibleItems[item.id] {
|
||||||
itemLayer = current
|
itemLayer = current
|
||||||
updateLayers.append(itemLayer.layer)
|
updateLayers.append(itemLayer)
|
||||||
} else {
|
} else {
|
||||||
itemLayer = VisibleItemLayer(layer: items.itemBinding.createLayer())
|
itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView())
|
||||||
self.visibleItems[item.id] = itemLayer
|
self.visibleItems[item.id] = itemLayer
|
||||||
|
|
||||||
bindItems.append(item)
|
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)
|
validIds.insert(item.id)
|
||||||
|
|
||||||
itemLayer.layer.frame = layout.frame(at: index)
|
itemLayer.frame = layout.frame(at: index)
|
||||||
} else {
|
} else if layout.containerLayout.fixedItemHeight == nil {
|
||||||
let placeholderLayer: ShimmerLayer
|
let placeholderLayer: Shimmer.Layer
|
||||||
if self.visiblePlaceholders.count > usedPlaceholderCount {
|
if self.visiblePlaceholders.count > usedPlaceholderCount {
|
||||||
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
|
placeholderLayer = self.visiblePlaceholders[usedPlaceholderCount]
|
||||||
} else {
|
} else {
|
||||||
placeholderLayer = ShimmerLayer()
|
placeholderLayer = Shimmer.Layer()
|
||||||
self.scrollView.layer.addSublayer(placeholderLayer)
|
self.scrollView.layer.addSublayer(placeholderLayer)
|
||||||
self.visiblePlaceholders.append(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
|
usedPlaceholderCount += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -535,8 +756,18 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers)
|
items.itemBinding.bindLayers(items: bindItems, layers: bindLayers)
|
||||||
}
|
}
|
||||||
|
|
||||||
for layer in updateLayers {
|
for item in updateLayers {
|
||||||
layer.update(size: layer.bounds.size)
|
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 {
|
for id in removeIds {
|
||||||
if let itemLayer = self.visibleItems.removeValue(forKey: id) {
|
if let item = self.visibleItems.removeValue(forKey: id) {
|
||||||
items.itemBinding.unbindLayer(layer: itemLayer.layer)
|
if let layer = item.layer {
|
||||||
itemLayer.layer.removeFromSuperlayer()
|
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 {
|
guard let strongSelf = self else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
strongSelf.items?.itemBinding.onBeginFastScrolling()
|
||||||
return strongSelf.scrollView
|
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()
|
self.updateScrollingArea()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -624,13 +868,20 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let scrollingArea = self.scrollingArea {
|
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(
|
scrollingArea.update(
|
||||||
containerSize: layout.containerLayout.size,
|
containerSize: layout.containerLayout.size,
|
||||||
containerInsets: layout.containerLayout.insets,
|
containerInsets: layout.containerLayout.insets,
|
||||||
contentHeight: contentHeight,
|
contentHeight: contentHeight,
|
||||||
contentOffset: self.scrollView.bounds.minY,
|
contentOffset: self.scrollView.bounds.minY,
|
||||||
isScrolling: self.scrollView.isDragging || self.scrollView.isDecelerating,
|
isScrolling: self.scrollView.isDragging || self.scrollView.isDecelerating,
|
||||||
dateString: tag.flatMap { items.itemBinding.scrollerTextForTag(tag: $0) } ?? "",
|
dateString: dateString ?? "",
|
||||||
transition: .immediate
|
transition: .immediate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -740,8 +991,10 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
var insets: UIEdgeInsets
|
var insets: UIEdgeInsets
|
||||||
var scrollIndicatorInsets: UIEdgeInsets
|
var scrollIndicatorInsets: UIEdgeInsets
|
||||||
var lockScrollingAtTop: Bool
|
var lockScrollingAtTop: Bool
|
||||||
|
var fixedItemHeight: CGFloat?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var tapRecognizer: UITapGestureRecognizer?
|
||||||
private var pinchRecognizer: UIPinchGestureRecognizer?
|
private var pinchRecognizer: UIPinchGestureRecognizer?
|
||||||
|
|
||||||
private var containerLayout: ContainerLayout?
|
private var containerLayout: ContainerLayout?
|
||||||
@ -754,6 +1007,13 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
private var isLoadingHole: Bool = false
|
private var isLoadingHole: Bool = false
|
||||||
private let loadingHoleDisposable = MetaDisposable()
|
private let loadingHoleDisposable = MetaDisposable()
|
||||||
|
|
||||||
|
public var coveringInsetOffset: CGFloat {
|
||||||
|
if let currentViewport = self.currentViewport {
|
||||||
|
return currentViewport.coveringInsetOffset
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
override public init() {
|
override public init() {
|
||||||
self.scrollingArea = SparseItemGridScrollingArea()
|
self.scrollingArea = SparseItemGridScrollingArea()
|
||||||
|
|
||||||
@ -762,6 +1022,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
self.clipsToBounds = true
|
self.clipsToBounds = true
|
||||||
|
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||||
|
self.tapRecognizer = tapRecognizer
|
||||||
self.view.addGestureRecognizer(tapRecognizer)
|
self.view.addGestureRecognizer(tapRecognizer)
|
||||||
|
|
||||||
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:)))
|
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) {
|
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)
|
let containerLayout = ContainerLayout(size: size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, lockScrollingAtTop: lockScrollingAtTop, fixedItemHeight: fixedItemHeight)
|
||||||
self.containerLayout = containerLayout
|
self.containerLayout = containerLayout
|
||||||
self.items = items
|
self.items = items
|
||||||
self.scrollingArea.isHidden = lockScrollingAtTop
|
self.scrollingArea.isHidden = lockScrollingAtTop
|
||||||
|
|
||||||
|
self.tapRecognizer?.isEnabled = fixedItemHeight == nil
|
||||||
|
self.pinchRecognizer?.isEnabled = fixedItemHeight == nil
|
||||||
|
|
||||||
if self.currentViewport == nil {
|
if self.currentViewport == nil {
|
||||||
let currentViewport = Viewport(zoomLevel: ZoomLevel(rawValue: 100), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in
|
let currentViewport = Viewport(zoomLevel: ZoomLevel(rawValue: 100), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in
|
||||||
guard let strongSelf = self else {
|
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 {
|
guard let currentViewport = self.currentViewport else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (_, itemLayer) in currentViewport.visibleItems {
|
for (_, itemLayer) in currentViewport.visibleItems {
|
||||||
f(itemLayer.layer)
|
f(itemLayer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1086,7 +1350,18 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
currentViewport.scrollToItem(at: index)
|
currentViewport.scrollToItem(at: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func scrollToTop() -> Bool {
|
||||||
|
guard let currentViewport = self.currentViewport else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return currentViewport.scrollToTop()
|
||||||
|
}
|
||||||
|
|
||||||
public func addToTransitionSurface(view: UIView) {
|
public func addToTransitionSurface(view: UIView) {
|
||||||
self.view.insertSubview(view, belowSubview: self.scrollingArea.view)
|
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 AsyncDisplayKit
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import SwiftSignalKit
|
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 {
|
private final class RoundedRectangle: Component {
|
||||||
let color: UIColor
|
let color: UIColor
|
||||||
@ -324,6 +568,11 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
private let dateIndicator: ComponentHostView<Empty>
|
private let dateIndicator: ComponentHostView<Empty>
|
||||||
|
|
||||||
private let lineIndicator: 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 indicatorPosition: CGFloat?
|
||||||
private var scrollIndicatorHeight: CGFloat?
|
private var scrollIndicatorHeight: CGFloat?
|
||||||
|
|
||||||
@ -336,6 +585,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
private var activityTimer: SwiftSignalKit.Timer?
|
private var activityTimer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
public var beginScrolling: (() -> UIScrollView?)?
|
public var beginScrolling: (() -> UIScrollView?)?
|
||||||
|
public var setContentOffset: ((CGPoint) -> Void)?
|
||||||
public var openCurrentDate: (() -> Void)?
|
public var openCurrentDate: (() -> Void)?
|
||||||
|
|
||||||
private var offsetBarTimer: SwiftSignalKit.Timer?
|
private var offsetBarTimer: SwiftSignalKit.Timer?
|
||||||
@ -350,6 +600,20 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
private var projectionData: ProjectionData?
|
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() {
|
override public init() {
|
||||||
self.dateIndicator = ComponentHostView<Empty>()
|
self.dateIndicator = ComponentHostView<Empty>()
|
||||||
self.lineIndicator = ComponentHostView<Empty>()
|
self.lineIndicator = ComponentHostView<Empty>()
|
||||||
@ -399,10 +663,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
if let scrollView = strongSelf.beginScrolling?() {
|
if let scrollView = strongSelf.beginScrolling?() {
|
||||||
strongSelf.draggingScrollView = scrollView
|
strongSelf.draggingScrollView = scrollView
|
||||||
strongSelf.scrollingInitialOffset = scrollView.contentOffset.y
|
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
|
ended: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -424,7 +688,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
|
|
||||||
strongSelf.updateLineIndicator(transition: transition)
|
strongSelf.updateLineIndicator(transition: transition)
|
||||||
|
|
||||||
strongSelf.updateActivityTimer()
|
strongSelf.updateActivityTimer(isScrolling: false)
|
||||||
},
|
},
|
||||||
moved: { [weak self] relativeOffset in
|
moved: { [weak self] relativeOffset in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -454,7 +718,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
offset = scrollView.contentSize.height - scrollView.bounds.height
|
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 _ = scrollView
|
||||||
let _ = projectionData
|
let _ = projectionData
|
||||||
}
|
}
|
||||||
@ -473,6 +737,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
self.updateLineIndicator(transition: transition)
|
self.updateLineIndicator(transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func feedbackTap() {
|
||||||
|
self.hapticFeedback.tap()
|
||||||
|
}
|
||||||
|
|
||||||
public func update(
|
public func update(
|
||||||
containerSize: CGSize,
|
containerSize: CGSize,
|
||||||
containerInsets: UIEdgeInsets,
|
containerInsets: UIEdgeInsets,
|
||||||
@ -482,8 +750,10 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
dateString: String,
|
dateString: String,
|
||||||
transition: ContainedViewLayoutTransition
|
transition: ContainedViewLayoutTransition
|
||||||
) {
|
) {
|
||||||
|
self.containerSize = containerSize
|
||||||
|
|
||||||
if isScrolling {
|
if isScrolling {
|
||||||
self.updateActivityTimer()
|
self.updateActivityTimer(isScrolling: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
let indicatorSize = self.dateIndicator.update(
|
let indicatorSize = self.dateIndicator.update(
|
||||||
@ -508,7 +778,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let indicatorVerticalInset: CGFloat = 3.0
|
let indicatorVerticalInset: CGFloat = 3.0
|
||||||
let topIndicatorInset: CGFloat = indicatorVerticalInset
|
let topIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.top
|
||||||
let bottomIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.bottom
|
let bottomIndicatorInset: CGFloat = indicatorVerticalInset + containerInsets.bottom
|
||||||
|
|
||||||
let scrollIndicatorHeight = max(35.0, ceil(scrollIndicatorHeightFraction * containerSize.height))
|
let scrollIndicatorHeight = max(35.0, ceil(scrollIndicatorHeightFraction * containerSize.height))
|
||||||
@ -539,7 +809,13 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
self.lineIndicator.alpha = 1.0
|
self.lineIndicator.alpha = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.updateLineTooltip(containerSize: containerSize)
|
||||||
|
|
||||||
self.updateLineIndicator(transition: transition)
|
self.updateLineIndicator(transition: transition)
|
||||||
|
|
||||||
|
if isScrolling {
|
||||||
|
self.displayTooltipOnFirstScroll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateLineIndicator(transition: ContainedViewLayoutTransition) {
|
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))
|
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()
|
self.activityTimer?.invalidate()
|
||||||
|
|
||||||
if self.isDragging {
|
if self.isDragging {
|
||||||
@ -582,11 +858,68 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
|||||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
|
||||||
transition.updateAlpha(layer: strongSelf.dateIndicator.layer, alpha: 0.0)
|
transition.updateAlpha(layer: strongSelf.dateIndicator.layer, alpha: 0.0)
|
||||||
transition.updateAlpha(layer: strongSelf.lineIndicator.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())
|
}, queue: .mainQueue())
|
||||||
self.activityTimer?.start()
|
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? {
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
if self.dateIndicator.alpha <= 0.01 {
|
if self.dateIndicator.alpha <= 0.01 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -683,7 +683,9 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
if let _ = audioSessionControl, !wasActive || previousControl == nil {
|
if let _ = audioSessionControl, !wasActive || previousControl == nil {
|
||||||
let logName = "\(id.id)_\(id.accessHash)"
|
let logName = "\(id.id)_\(id.accessHash)"
|
||||||
|
|
||||||
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 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: 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
|
self.ongoingContext = ongoingContext
|
||||||
ongoingContext.setIsMuted(self.isMutedValue)
|
ongoingContext.setIsMuted(self.isMutedValue)
|
||||||
if let requestedVideoAspect = self.requestedVideoAspect {
|
if let requestedVideoAspect = self.requestedVideoAspect {
|
||||||
|
|||||||
@ -232,6 +232,11 @@ private func parseConnection(_ apiConnection: Api.PhoneConnection) -> CallSessio
|
|||||||
public struct CallSessionConnectionSet {
|
public struct CallSessionConnectionSet {
|
||||||
public let primary: CallSessionConnection
|
public let primary: CallSessionConnection
|
||||||
public let alternatives: [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 {
|
private func parseConnectionSet(primary: Api.PhoneConnection, alternative: [Api.PhoneConnection]) -> CallSessionConnectionSet {
|
||||||
|
|||||||
@ -160,6 +160,8 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
|
|||||||
case chatSpecificThemeLightPreviewTip = 26
|
case chatSpecificThemeLightPreviewTip = 26
|
||||||
case chatSpecificThemeDarkPreviewTip = 27
|
case chatSpecificThemeDarkPreviewTip = 27
|
||||||
case interactiveEmojiSyncTip = 28
|
case interactiveEmojiSyncTip = 28
|
||||||
|
case sharedMediaScrollingTooltip = 29
|
||||||
|
case sharedMediaFastScrollingTooltip = 30
|
||||||
|
|
||||||
var key: ValueBoxKey {
|
var key: ValueBoxKey {
|
||||||
let v = ValueBoxKey(length: 4)
|
let v = ValueBoxKey(length: 4)
|
||||||
@ -324,6 +326,14 @@ private struct ApplicationSpecificNoticeKeys {
|
|||||||
static func dismissedInvitationRequestsNotice(peerId: PeerId) -> NoticeEntryKey {
|
static func dismissedInvitationRequestsNotice(peerId: PeerId) -> NoticeEntryKey {
|
||||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: peerInviteRequestsNamespace), key: noticeKey(peerId: peerId, key: 0))
|
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 {
|
public struct ApplicationSpecificNotice {
|
||||||
@ -894,6 +904,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> {
|
public static func dismissedTrendingStickerPacks(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<[Int64]?, NoError> {
|
||||||
return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedTrendingStickerPacks())
|
return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedTrendingStickerPacks())
|
||||||
|> map { view -> [Int64]? in
|
|> map { view -> [Int64]? in
|
||||||
|
|||||||
@ -679,27 +679,9 @@ public struct PresentationResourcesChat {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme) -> UIImage? {
|
public static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme, generate: () -> UIImage?) -> UIImage? {
|
||||||
return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { theme in
|
return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { _ in
|
||||||
return generateImage(CGSize(width: 12.0, height: 12.0), rotatedContext: { size, context in
|
return generate()
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import Intents
|
|||||||
import Postbox
|
import Postbox
|
||||||
import PushKit
|
import PushKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import CloudKit
|
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramCallsUI
|
import TelegramCallsUI
|
||||||
|
|||||||
@ -82,6 +82,11 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||||
|
var tabBarOffset: CGFloat {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
private var disposable: Disposable?
|
private var disposable: Disposable?
|
||||||
|
|
||||||
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
|
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
|
||||||
@ -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
|
let isFirstLayout = self.currentParams == nil
|
||||||
self.currentParams = (size, isScrollingLockedAtTop, presentationData)
|
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
|
self.listNode.scrollEnabled = !isScrollingLockedAtTop
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
|
|
||||||
private let listNode: ChatHistoryListNode
|
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 let ready = Promise<Bool>()
|
||||||
private var didSetReady: Bool = false
|
private var didSetReady: Bool = false
|
||||||
@ -55,6 +55,11 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
self.statusPromise.get()
|
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) {
|
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, tagMask: MessageTags) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
@ -129,8 +134,8 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
strongSelf.playlistStateAndType = nil
|
strongSelf.playlistStateAndType = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) = strongSelf.currentParams {
|
if let (size, topInset, 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))
|
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) {
|
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, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
self.currentParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
||||||
|
|
||||||
var topPanelHeight: CGFloat = 0.0
|
var topPanelHeight: CGFloat = 0.0
|
||||||
if let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType {
|
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))
|
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
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 {
|
if isScrollingLockedAtTop {
|
||||||
switch self.listNode.visibleContentOffset() {
|
switch self.listNode.visibleContentOffset() {
|
||||||
case let .known(value) where value <= CGFloat.ulpOfOne:
|
case let .known(value) where value <= CGFloat.ulpOfOne:
|
||||||
|
|||||||
@ -125,6 +125,11 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||||
|
var tabBarOffset: CGFloat {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
private var disposable: Disposable?
|
private var disposable: Disposable?
|
||||||
|
|
||||||
init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) {
|
init(context: AccountContext, peerId: PeerId, membersContext: PeerInfoMembersContext, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void) {
|
||||||
@ -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
|
let isFirstLayout = self.currentParams == nil
|
||||||
self.currentParams = (size, isScrollingLockedAtTop)
|
self.currentParams = (size, isScrollingLockedAtTop)
|
||||||
self.presentationDataPromise.set(.single(presentationData))
|
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)
|
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
|
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 {
|
final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode {
|
||||||
private var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
|
private(set) var buttonNodes: [PeerInfoHeaderNavigationButtonKey: PeerInfoHeaderNavigationButton] = [:]
|
||||||
|
|
||||||
private var currentButtons: [PeerInfoHeaderNavigationButtonSpec] = []
|
private var currentButtons: [PeerInfoHeaderNavigationButtonSpec] = []
|
||||||
|
|
||||||
|
|||||||
@ -15,8 +15,10 @@ protocol PeerInfoPaneNode: ASDisplayNode {
|
|||||||
var parentController: ViewController? { get set }
|
var parentController: ViewController? { get set }
|
||||||
|
|
||||||
var status: Signal<PeerInfoStatusData?, NoError> { get }
|
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 scrollToTop() -> Bool
|
||||||
func transferVelocity(_ velocity: CGFloat)
|
func transferVelocity(_ velocity: CGFloat)
|
||||||
func cancelPreviewGestures()
|
func cancelPreviewGestures()
|
||||||
@ -32,21 +34,21 @@ final class PeerInfoPaneWrapper {
|
|||||||
let key: PeerInfoPaneKey
|
let key: PeerInfoPaneKey
|
||||||
let node: PeerInfoPaneNode
|
let node: PeerInfoPaneNode
|
||||||
var isAnimatingOut: Bool = false
|
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) {
|
init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) {
|
||||||
self.key = key
|
self.key = key
|
||||||
self.node = node
|
self.node = node
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
if let (currentSize, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams {
|
if let (currentSize, currentTopInset, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams {
|
||||||
if currentSize == size && currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData {
|
if currentSize == size && currentTopInset == topInset, currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.appliedParams = (size, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData)
|
self.appliedParams = (size, topInset, 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.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()
|
paneDidScroll()
|
||||||
}
|
}
|
||||||
case .files:
|
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:
|
case .links:
|
||||||
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .webPage)
|
paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .webPage)
|
||||||
case .voice:
|
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:
|
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:
|
case .gifs:
|
||||||
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .gifs)
|
let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .gifs)
|
||||||
paneNode = visualPaneNode
|
paneNode = visualPaneNode
|
||||||
@ -715,15 +723,11 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
|||||||
self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
||||||
self.tabsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
self.tabsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
||||||
|
|
||||||
|
let isScrollingLockedAtTop = expansionFraction < 1.0 - CGFloat.ulpOfOne
|
||||||
|
|
||||||
let tabsHeight: CGFloat = 48.0
|
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)))
|
let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||||
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))
|
|
||||||
|
|
||||||
var visiblePaneIndices: [Int] = []
|
var visiblePaneIndices: [Int] = []
|
||||||
var requiredPendingKeys: [PeerInfoPaneKey] = []
|
var requiredPendingKeys: [PeerInfoPaneKey] = []
|
||||||
@ -794,14 +798,23 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
|||||||
)
|
)
|
||||||
self.pendingPanes[key] = pane
|
self.pendingPanes[key] = pane
|
||||||
pane.pane.node.frame = paneFrame
|
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
|
leftScope = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (key, pane) in self.pendingPanes {
|
for (key, pane) in self.pendingPanes {
|
||||||
pane.pane.node.frame = paneFrame
|
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 {
|
if pane.isReady {
|
||||||
self.pendingPanes.removeValue(forKey: key)
|
self.pendingPanes.removeValue(forKey: key)
|
||||||
@ -834,7 +847,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
|||||||
if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex {
|
if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex {
|
||||||
var paneWasAdded = false
|
var paneWasAdded = false
|
||||||
if pane.node.supernode == nil {
|
if pane.node.supernode == nil {
|
||||||
self.addSubnode(pane.node)
|
self.insertSubnode(pane.node, belowSubnode: self.coveringBackgroundNode)
|
||||||
paneWasAdded = true
|
paneWasAdded = true
|
||||||
}
|
}
|
||||||
let indexOffset = CGFloat(index - updatedCurrentIndex)
|
let indexOffset = CGFloat(index - updatedCurrentIndex)
|
||||||
@ -878,13 +891,29 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
|||||||
paneCompletion()
|
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 })")
|
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)))
|
||||||
|
|
||||||
transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: tabsHeight)))
|
|
||||||
self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
|
self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
|
||||||
let title: String
|
let title: String
|
||||||
switch key {
|
switch key {
|
||||||
@ -911,7 +940,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
|||||||
for (_, pane) in self.pendingPanes {
|
for (_, pane) in self.pendingPanes {
|
||||||
let paneTransition: ContainedViewLayoutTransition = .immediate
|
let paneTransition: ContainedViewLayoutTransition = .immediate
|
||||||
paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame)
|
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 !self.didSetIsReady && data != nil {
|
||||||
if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {
|
if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] {
|
||||||
|
|||||||
@ -61,6 +61,7 @@ import TelegramCallsUI
|
|||||||
import PeerInfoAvatarListNode
|
import PeerInfoAvatarListNode
|
||||||
import PasswordSetupUI
|
import PasswordSetupUI
|
||||||
import CalendarMessageScreen
|
import CalendarMessageScreen
|
||||||
|
import TooltipUI
|
||||||
|
|
||||||
protocol PeerInfoScreenItem: AnyObject {
|
protocol PeerInfoScreenItem: AnyObject {
|
||||||
var id: AnyHashable { get }
|
var id: AnyHashable { get }
|
||||||
@ -5996,6 +5997,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
|
|
||||||
private weak var mediaGalleryContextMenu: ContextController?
|
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) {
|
private func displayMediaGalleryContextMenu(source: ContextReferenceContentNode) {
|
||||||
guard let controller = self.controller else {
|
guard let controller = self.controller else {
|
||||||
return
|
return
|
||||||
@ -6080,8 +6095,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
updatedContentType = .photo
|
updatedContentType = .photo
|
||||||
case .video:
|
case .video:
|
||||||
updatedContentType = .photoOrVideo
|
updatedContentType = .photoOrVideo
|
||||||
case .gifs:
|
default:
|
||||||
updatedContentType = .gifs
|
updatedContentType = pane.contentType
|
||||||
}
|
}
|
||||||
pane.updateContentType(contentType: updatedContentType)
|
pane.updateContentType(contentType: updatedContentType)
|
||||||
})))
|
})))
|
||||||
@ -6104,8 +6119,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
updatedContentType = .photoOrVideo
|
updatedContentType = .photoOrVideo
|
||||||
case .video:
|
case .video:
|
||||||
updatedContentType = .video
|
updatedContentType = .video
|
||||||
case .gifs:
|
default:
|
||||||
updatedContentType = .gifs
|
updatedContentType = pane.contentType
|
||||||
}
|
}
|
||||||
pane.updateContentType(contentType: updatedContentType)
|
pane.updateContentType(contentType: updatedContentType)
|
||||||
})))
|
})))
|
||||||
|
|||||||
@ -18,6 +18,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
public var experimentalCompatibility: Bool
|
public var experimentalCompatibility: Bool
|
||||||
public var enableDebugDataDisplay: Bool
|
public var enableDebugDataDisplay: Bool
|
||||||
public var acceleratedStickers: Bool
|
public var acceleratedStickers: Bool
|
||||||
|
public var mockICE: Bool
|
||||||
|
|
||||||
public static var defaultSettings: ExperimentalUISettings {
|
public static var defaultSettings: ExperimentalUISettings {
|
||||||
return ExperimentalUISettings(
|
return ExperimentalUISettings(
|
||||||
@ -34,7 +35,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
enableVoipTcp: false,
|
enableVoipTcp: false,
|
||||||
experimentalCompatibility: false,
|
experimentalCompatibility: false,
|
||||||
enableDebugDataDisplay: false,
|
enableDebugDataDisplay: false,
|
||||||
acceleratedStickers: false
|
acceleratedStickers: false,
|
||||||
|
mockICE: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +54,8 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
enableVoipTcp: Bool,
|
enableVoipTcp: Bool,
|
||||||
experimentalCompatibility: Bool,
|
experimentalCompatibility: Bool,
|
||||||
enableDebugDataDisplay: Bool,
|
enableDebugDataDisplay: Bool,
|
||||||
acceleratedStickers: Bool
|
acceleratedStickers: Bool,
|
||||||
|
mockICE: Bool
|
||||||
) {
|
) {
|
||||||
self.keepChatNavigationStack = keepChatNavigationStack
|
self.keepChatNavigationStack = keepChatNavigationStack
|
||||||
self.skipReadHistory = skipReadHistory
|
self.skipReadHistory = skipReadHistory
|
||||||
@ -68,6 +71,7 @@ public struct ExperimentalUISettings: Codable, Equatable {
|
|||||||
self.experimentalCompatibility = experimentalCompatibility
|
self.experimentalCompatibility = experimentalCompatibility
|
||||||
self.enableDebugDataDisplay = enableDebugDataDisplay
|
self.enableDebugDataDisplay = enableDebugDataDisplay
|
||||||
self.acceleratedStickers = acceleratedStickers
|
self.acceleratedStickers = acceleratedStickers
|
||||||
|
self.mockICE = mockICE
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
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.experimentalCompatibility = (try container.decodeIfPresent(Int32.self, forKey: "experimentalCompatibility") ?? 0) != 0
|
||||||
self.enableDebugDataDisplay = (try container.decodeIfPresent(Int32.self, forKey: "enableDebugDataDisplay") ?? 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.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 {
|
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.experimentalCompatibility ? 1 : 0) as Int32, forKey: "experimentalCompatibility")
|
||||||
try container.encode((self.enableDebugDataDisplay ? 1 : 0) as Int32, forKey: "enableDebugDataDisplay")
|
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.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