mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
2dd23729b5
@ -501,6 +501,7 @@ public final class ContactSelectionControllerParams {
|
||||
public enum ChatListSearchFilter: Equatable {
|
||||
case chats
|
||||
case media
|
||||
case downloads
|
||||
case links
|
||||
case files
|
||||
case music
|
||||
@ -514,14 +515,16 @@ public enum ChatListSearchFilter: Equatable {
|
||||
return 0
|
||||
case .media:
|
||||
return 1
|
||||
case .links:
|
||||
case .downloads:
|
||||
return 2
|
||||
case .files:
|
||||
case .links:
|
||||
return 3
|
||||
case .music:
|
||||
case .files:
|
||||
return 4
|
||||
case .voice:
|
||||
case .music:
|
||||
return 5
|
||||
case .voice:
|
||||
return 6
|
||||
case let .peer(peerId, _, _, _):
|
||||
return peerId.id._internalGetInt64Value()
|
||||
case let .date(_, date, _):
|
||||
@ -616,6 +619,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?) -> ViewController
|
||||
func makePrivacyAndSecurityController(context: AccountContext) -> ViewController
|
||||
func navigateToChatController(_ params: NavigateToChatControllerParams)
|
||||
func openStorageUsage(context: AccountContext)
|
||||
func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController)
|
||||
func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void)
|
||||
func chatAvailableMessageActions(postbox: Postbox, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>) -> Signal<ChatAvailableMessageActions, NoError>
|
||||
|
@ -155,6 +155,9 @@ public protocol FetchManager {
|
||||
|
||||
func interactivelyFetched(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, mediaReference: AnyMediaReference?, resourceReference: MediaResourceReference, ranges: IndexSet, statsCategory: MediaResourceStatsCategory, elevatedPriority: Bool, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError>
|
||||
func cancelInteractiveFetches(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource)
|
||||
func cancelInteractiveFetches(resourceId: String)
|
||||
func toggleInteractiveFetchPaused(resourceId: String, isPaused: Bool)
|
||||
func raisePriority(resourceId: String)
|
||||
func fetchStatus(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource) -> Signal<MediaResourceStatus, NoError>
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ public func messageMediaFileStatus(context: AccountContext, messageId: MessageId
|
||||
|
||||
public func messageMediaImageStatus(context: AccountContext, messageId: MessageId, image: TelegramMediaImage) -> Signal<MediaResourceStatus, NoError> {
|
||||
guard let representation = image.representations.last else {
|
||||
return .single(.Remote)
|
||||
return .single(.Remote(progress: 0.0))
|
||||
}
|
||||
return context.fetchManager.fetchStatus(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: representation.resource)
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ swift_library(
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AnimationCompression:AnimationCompression",
|
||||
"//submodules/Components/MetalImageView:MetalImageView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -245,6 +245,12 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
|
||||
}
|
||||
}
|
||||
|
||||
private func alignUp(size: Int, align: Int) -> Int {
|
||||
precondition(((align - 1) & align) == 0, "Align must be a power of two")
|
||||
|
||||
let alignmentMask = align - 1
|
||||
return (size + alignmentMask) & ~alignmentMask
|
||||
}
|
||||
|
||||
private final class AnimatedStickerDirectFrameSourceCache {
|
||||
private enum FrameRangeResult {
|
||||
@ -274,8 +280,8 @@ private final class AnimatedStickerDirectFrameSourceCache {
|
||||
self.storeQueue = sharedStoreQueue
|
||||
|
||||
self.frameCount = frameCount
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.width = alignUp(size: width, align: 8)
|
||||
self.height = alignUp(size: height, align: 8)
|
||||
self.useHardware = useHardware
|
||||
|
||||
let suffix : String
|
||||
|
@ -248,7 +248,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if #available(iOS 10.0, *), self.useMetalCache {
|
||||
if #available(iOS 10.0, *), (self.useMetalCache/* || "".isEmpty*/) {
|
||||
self.renderer = AnimatedStickerNode.hardwareRendererPool.take()
|
||||
} else {
|
||||
self.renderer = AnimatedStickerNode.softwareRendererPool.take()
|
||||
|
@ -42,9 +42,9 @@ final class AnimationRendererPool {
|
||||
}
|
||||
|
||||
private func putBack(renderer: AnimationRenderer) {
|
||||
#if DEBUG
|
||||
/*#if DEBUG
|
||||
self.items.append(renderer)
|
||||
#endif
|
||||
#endif*/
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,44 +8,19 @@ import Accelerate
|
||||
import AnimationCompression
|
||||
import Metal
|
||||
import MetalKit
|
||||
import MetalImageView
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
final class CompressedAnimationRenderer: ASDisplayNode, AnimationRenderer {
|
||||
private final class View: UIView {
|
||||
static override var layerClass: AnyClass {
|
||||
#if targetEnvironment(simulator)
|
||||
if #available(iOS 13.0, *) {
|
||||
return CAMetalLayer.self
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
#else
|
||||
return CAMetalLayer.self
|
||||
#endif
|
||||
return MetalImageLayer.self
|
||||
}
|
||||
|
||||
init(device: MTLDevice) {
|
||||
super.init(frame: CGRect())
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
if #available(iOS 13.0, *) {
|
||||
let metalLayer = self.layer as! CAMetalLayer
|
||||
|
||||
metalLayer.device = MTLCreateSystemDefaultDevice()
|
||||
metalLayer.pixelFormat = .bgra8Unorm
|
||||
metalLayer.framebufferOnly = true
|
||||
metalLayer.allowsNextDrawableTimeout = true
|
||||
}
|
||||
#else
|
||||
let metalLayer = self.layer as! CAMetalLayer
|
||||
|
||||
metalLayer.device = MTLCreateSystemDefaultDevice()
|
||||
metalLayer.pixelFormat = .bgra8Unorm
|
||||
metalLayer.framebufferOnly = true
|
||||
if #available(iOS 11.0, *) {
|
||||
metalLayer.allowsNextDrawableTimeout = true
|
||||
}
|
||||
#endif
|
||||
(self.layer as! MetalImageLayer).renderer.device = device
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -82,16 +57,25 @@ final class CompressedAnimationRenderer: ASDisplayNode, AnimationRenderer {
|
||||
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void) {
|
||||
switch type {
|
||||
case .dct:
|
||||
self.renderer.renderIdct(metalLayer: self.layer, compressedImage: AnimationCompressor.CompressedImageData(data: data), completion: completion)
|
||||
self.renderer.renderIdct(layer: self.layer as! MetalImageLayer, compressedImage: AnimationCompressor.CompressedImageData(data: data), completion: { [weak self] in
|
||||
self?.updateHighlightedContentNode()
|
||||
completion()
|
||||
})
|
||||
case .argb:
|
||||
self.renderer.renderRgb(metalLayer: self.layer, width: width, height: height, bytesPerRow: bytesPerRow, data: data, completion: completion)
|
||||
self.renderer.renderRgb(layer: self.layer as! MetalImageLayer, width: width, height: height, bytesPerRow: bytesPerRow, data: data, completion: { [weak self] in
|
||||
self?.updateHighlightedContentNode()
|
||||
completion()
|
||||
})
|
||||
case .yuva:
|
||||
self.renderer.renderYuva(metalLayer: self.layer, width: width, height: height, data: data, completion: completion)
|
||||
self.renderer.renderYuva(layer: self.layer as! MetalImageLayer, width: width, height: height, data: data, completion: { [weak self] in
|
||||
self?.updateHighlightedContentNode()
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func updateHighlightedContentNode() {
|
||||
/*guard let highlightedContentNode = self.highlightedContentNode, let highlightedColor = self.highlightedColor else {
|
||||
guard let highlightedContentNode = self.highlightedContentNode, let highlightedColor = self.highlightedColor else {
|
||||
return
|
||||
}
|
||||
if let contents = self.contents, CFGetTypeID(contents as CFTypeRef) == CGImage.typeID {
|
||||
@ -100,11 +84,11 @@ final class CompressedAnimationRenderer: ASDisplayNode, AnimationRenderer {
|
||||
highlightedContentNode.tintColor = highlightedColor
|
||||
if self.highlightReplacesContent {
|
||||
self.contents = nil
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {
|
||||
/*self.highlightReplacesContent = replace
|
||||
self.highlightReplacesContent = replace
|
||||
var updated = false
|
||||
if let current = self.highlightedColor, let color = color {
|
||||
updated = !current.isEqual(color)
|
||||
@ -141,6 +125,6 @@ final class CompressedAnimationRenderer: ASDisplayNode, AnimationRenderer {
|
||||
strongSelf.highlightedContentNode?.removeFromSupernode()
|
||||
strongSelf.highlightedContentNode = nil
|
||||
})
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ swift_library(
|
||||
],
|
||||
deps = [
|
||||
":DctHuffman",
|
||||
"//submodules/Components/MetalImageView:MetalImageView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -4,6 +4,7 @@ import Metal
|
||||
import MetalKit
|
||||
import simd
|
||||
import DctHuffman
|
||||
import MetalImageView
|
||||
|
||||
private struct Vertex {
|
||||
var position: vector_float2
|
||||
@ -94,6 +95,8 @@ public final class CompressedImageRenderer {
|
||||
private var yuvaTextures: TextureSet?
|
||||
|
||||
private let commandQueue: MTLCommandQueue
|
||||
|
||||
private var isRendering: Bool = false
|
||||
|
||||
public init?(sharedContext: AnimationCompressor.SharedContext) {
|
||||
self.sharedContext = sharedContext
|
||||
@ -107,46 +110,9 @@ public final class CompressedImageRenderer {
|
||||
|
||||
private var drawableRequestTimestamp: Double?
|
||||
|
||||
private func getNextDrawable(metalLayer: CALayer, drawableSize: CGSize) -> CAMetalDrawable? {
|
||||
#if targetEnvironment(simulator)
|
||||
if #available(iOS 13.0, *) {
|
||||
if let metalLayer = metalLayer as? CAMetalLayer {
|
||||
if metalLayer.drawableSize != drawableSize {
|
||||
metalLayer.drawableSize = drawableSize
|
||||
}
|
||||
return metalLayer.nextDrawable()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
#else
|
||||
if let metalLayer = metalLayer as? CAMetalLayer {
|
||||
if metalLayer.drawableSize != drawableSize {
|
||||
metalLayer.drawableSize = drawableSize
|
||||
}
|
||||
let beginTime = CFAbsoluteTimeGetCurrent()
|
||||
let drawableRequestDuration: Double
|
||||
if let drawableRequestTimestamp = self.drawableRequestTimestamp {
|
||||
drawableRequestDuration = beginTime - drawableRequestTimestamp
|
||||
if drawableRequestDuration < 1.0 / 60.0 {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
drawableRequestDuration = 0.0
|
||||
}
|
||||
self.drawableRequestTimestamp = beginTime
|
||||
let result = metalLayer.nextDrawable()
|
||||
let duration = CFAbsoluteTimeGetCurrent() - beginTime
|
||||
if duration > 1.0 / 200.0 {
|
||||
print("lag \(duration * 1000.0) ms (\(drawableRequestDuration * 1000.0) ms)")
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
private func getNextDrawable(layer: MetalImageLayer, drawableSize: CGSize) -> MetalImageLayer.Drawable? {
|
||||
layer.renderer.drawableSize = drawableSize
|
||||
return layer.renderer.nextDrawable()
|
||||
}
|
||||
|
||||
private func updateIdctTextures(compressedImage: AnimationCompressor.CompressedImageData) {
|
||||
@ -223,7 +189,7 @@ public final class CompressedImageRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
public func renderIdct(metalLayer: CALayer, compressedImage: AnimationCompressor.CompressedImageData, completion: @escaping () -> Void) {
|
||||
public func renderIdct(layer: MetalImageLayer, compressedImage: AnimationCompressor.CompressedImageData, completion: @escaping () -> Void) {
|
||||
DispatchQueue.global().async {
|
||||
self.updateIdctTextures(compressedImage: compressedImage)
|
||||
|
||||
@ -295,7 +261,7 @@ public final class CompressedImageRenderer {
|
||||
|
||||
let drawableSize = CGSize(width: CGFloat(outputTextures.textures[0].width), height: CGFloat(outputTextures.textures[0].height))
|
||||
|
||||
guard let drawable = self.getNextDrawable(metalLayer: metalLayer, drawableSize: drawableSize) else {
|
||||
guard let drawable = self.getNextDrawable(layer: layer, drawableSize: drawableSize) else {
|
||||
commandBuffer.commit()
|
||||
completion()
|
||||
return
|
||||
@ -321,34 +287,15 @@ public final class CompressedImageRenderer {
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
|
||||
var storedDrawable: MTLDrawable? = drawable
|
||||
commandBuffer.addScheduledHandler { _ in
|
||||
storedDrawable?.present()
|
||||
storedDrawable = nil
|
||||
}
|
||||
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
var storedDrawable: MetalImageLayer.Drawable? = drawable
|
||||
commandBuffer.addCompletedHandler { _ in
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
#else
|
||||
if #available(iOS 10.3, *) {
|
||||
drawable.addPresentedHandler { _ in
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
commandBuffer.addCompletedHandler { _ in
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
autoreleasepool {
|
||||
storedDrawable?.present(completion: completion)
|
||||
storedDrawable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
commandBuffer.commit()
|
||||
}
|
||||
@ -383,7 +330,7 @@ public final class CompressedImageRenderer {
|
||||
})
|
||||
}
|
||||
|
||||
public func renderRgb(metalLayer: CALayer, width: Int, height: Int, bytesPerRow: Int, data: Data, completion: @escaping () -> Void) {
|
||||
public func renderRgb(layer: MetalImageLayer, width: Int, height: Int, bytesPerRow: Int, data: Data, completion: @escaping () -> Void) {
|
||||
self.updateRgbTexture(width: width, height: height, bytesPerRow: bytesPerRow, data: data)
|
||||
|
||||
guard let rgbTexture = self.rgbTexture else {
|
||||
@ -397,7 +344,7 @@ public final class CompressedImageRenderer {
|
||||
|
||||
let drawableSize = CGSize(width: CGFloat(rgbTexture.width), height: CGFloat(rgbTexture.height))
|
||||
|
||||
guard let drawable = self.getNextDrawable(metalLayer: metalLayer, drawableSize: drawableSize) else {
|
||||
guard let drawable = self.getNextDrawable(layer: layer, drawableSize: drawableSize) else {
|
||||
commandBuffer.commit()
|
||||
completion()
|
||||
return
|
||||
@ -420,11 +367,13 @@ public final class CompressedImageRenderer {
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
|
||||
commandBuffer.present(drawable)
|
||||
|
||||
var storedDrawable: MetalImageLayer.Drawable? = drawable
|
||||
commandBuffer.addCompletedHandler { _ in
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
autoreleasepool {
|
||||
storedDrawable?.present(completion: completion)
|
||||
storedDrawable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -432,6 +381,10 @@ public final class CompressedImageRenderer {
|
||||
}
|
||||
|
||||
private func updateYuvaTextures(width: Int, height: Int, data: Data) {
|
||||
if width % 2 != 0 || height % 2 != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
self.compressedTextures = nil
|
||||
self.outputTextures = nil
|
||||
self.rgbTexture = nil
|
||||
@ -501,56 +454,92 @@ public final class CompressedImageRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
public func renderYuva(metalLayer: CALayer, width: Int, height: Int, data: Data, completion: @escaping () -> Void) {
|
||||
self.updateYuvaTextures(width: width, height: height, data: data)
|
||||
|
||||
guard let yuvaTextures = self.yuvaTextures else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
|
||||
return
|
||||
}
|
||||
commandBuffer.label = "MyCommand"
|
||||
|
||||
let drawableSize = CGSize(width: CGFloat(yuvaTextures.width), height: CGFloat(yuvaTextures.height))
|
||||
|
||||
guard let drawable = self.getNextDrawable(metalLayer: metalLayer, drawableSize: drawableSize) else {
|
||||
commandBuffer.commit()
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .clear
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
|
||||
|
||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return
|
||||
}
|
||||
renderEncoder.label = "MyRenderEncoder"
|
||||
|
||||
renderEncoder.setRenderPipelineState(self.shared.renderYuvaPipelineState)
|
||||
renderEncoder.setFragmentTexture(yuvaTextures.textures[0].texture, index: 0)
|
||||
renderEncoder.setFragmentTexture(yuvaTextures.textures[1].texture, index: 1)
|
||||
renderEncoder.setFragmentTexture(yuvaTextures.textures[2].texture, index: 2)
|
||||
|
||||
var alphaSize = simd_uint2(UInt32(yuvaTextures.textures[0].texture.width), UInt32(yuvaTextures.textures[0].texture.height))
|
||||
renderEncoder.setFragmentBytes(&alphaSize, length: 8, index: 3)
|
||||
|
||||
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
|
||||
commandBuffer.present(drawable)
|
||||
|
||||
commandBuffer.addCompletedHandler { _ in
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
public func renderYuva(layer: MetalImageLayer, width: Int, height: Int, data: Data, completion: @escaping () -> Void) {
|
||||
DispatchQueue.global().async {
|
||||
autoreleasepool {
|
||||
//let renderStartTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
var beginTime: Double = 0.0
|
||||
var duration: Double = 0.0
|
||||
beginTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
self.updateYuvaTextures(width: width, height: height, data: data)
|
||||
|
||||
duration = CFAbsoluteTimeGetCurrent() - beginTime
|
||||
if duration > 1.0 / 60.0 {
|
||||
print("update textures lag \(duration * 1000.0)")
|
||||
}
|
||||
|
||||
guard let yuvaTextures = self.yuvaTextures else {
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
beginTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
commandBuffer.label = "MyCommand"
|
||||
|
||||
let drawableSize = CGSize(width: CGFloat(yuvaTextures.width), height: CGFloat(yuvaTextures.height))
|
||||
|
||||
guard let drawable = self.getNextDrawable(layer: layer, drawableSize: drawableSize) else {
|
||||
commandBuffer.commit()
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .clear
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
|
||||
|
||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
return
|
||||
}
|
||||
renderEncoder.label = "MyRenderEncoder"
|
||||
|
||||
renderEncoder.setRenderPipelineState(self.shared.renderYuvaPipelineState)
|
||||
renderEncoder.setFragmentTexture(yuvaTextures.textures[0].texture, index: 0)
|
||||
renderEncoder.setFragmentTexture(yuvaTextures.textures[1].texture, index: 1)
|
||||
renderEncoder.setFragmentTexture(yuvaTextures.textures[2].texture, index: 2)
|
||||
|
||||
var alphaSize = simd_uint2(UInt32(yuvaTextures.textures[0].texture.width), UInt32(yuvaTextures.textures[0].texture.height))
|
||||
renderEncoder.setFragmentBytes(&alphaSize, length: 8, index: 3)
|
||||
|
||||
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
|
||||
var storedDrawable: MetalImageLayer.Drawable? = drawable
|
||||
commandBuffer.addCompletedHandler { _ in
|
||||
DispatchQueue.main.async {
|
||||
autoreleasepool {
|
||||
storedDrawable?.present(completion: completion)
|
||||
storedDrawable = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandBuffer.commit()
|
||||
|
||||
duration = CFAbsoluteTimeGetCurrent() - beginTime
|
||||
if duration > 1.0 / 60.0 {
|
||||
print("commit lag \(duration * 1000.0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandBuffer.commit()
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ public enum ChatListSearchItemHeaderType {
|
||||
case recentCalls
|
||||
case orImportIntoAnExistingGroup
|
||||
case subscribers
|
||||
case downloading
|
||||
case recentDownloads
|
||||
|
||||
fileprivate func title(strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
@ -74,6 +76,12 @@ public enum ChatListSearchItemHeaderType {
|
||||
return strings.ChatList_HeaderImportIntoAnExistingGroup
|
||||
case .subscribers:
|
||||
return strings.Channel_ChannelSubscribersHeader
|
||||
case .downloading:
|
||||
//TODO:localize
|
||||
return "Downloading"
|
||||
case .recentDownloads:
|
||||
//TODO:localize
|
||||
return "Recently Downloaded"
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,6 +131,10 @@ public enum ChatListSearchItemHeaderType {
|
||||
return .orImportIntoAnExistingGroup
|
||||
case .subscribers:
|
||||
return .subscribers
|
||||
case .downloading:
|
||||
return .downloading
|
||||
case .recentDownloads:
|
||||
return .recentDownloads
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,6 +166,8 @@ private enum ChatListSearchItemHeaderId: Int32 {
|
||||
case recentCalls
|
||||
case orImportIntoAnExistingGroup
|
||||
case subscribers
|
||||
case downloading
|
||||
case recentDownloads
|
||||
}
|
||||
|
||||
public final class ChatListSearchItemHeader: ListViewItemHeader {
|
||||
|
@ -110,7 +110,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
|
||||
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
|
||||
let placeholder = self.placeholder
|
||||
|
||||
return { item, params, nextIsPinned, isEnabled in
|
||||
return { [weak self] item, params, nextIsPinned, isEnabled in
|
||||
let baseWidth = params.width - params.leftInset - params.rightInset
|
||||
|
||||
let backgroundColor = nextIsPinned ? item.theme.chatList.pinnedItemBackgroundColor : item.theme.chatList.itemBackgroundColor
|
||||
@ -120,7 +120,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 54.0), insets: UIEdgeInsets())
|
||||
|
||||
return (layout, { [weak self] animated in
|
||||
return (layout, { animated in
|
||||
if let strongSelf = self {
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
|
@ -62,6 +62,10 @@ swift_library(
|
||||
"//submodules/TelegramCallsUI:TelegramCallsUI",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/FetchManagerImpl:FetchManagerImpl",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/Components/ProgressIndicatorComponent:ProgressIndicatorComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -25,6 +25,10 @@ import TooltipUI
|
||||
import TelegramCallsUI
|
||||
import StickerResources
|
||||
import PasswordSetupUI
|
||||
import FetchManagerImpl
|
||||
import ComponentFlow
|
||||
import LottieAnimationComponent
|
||||
import ProgressIndicatorComponent
|
||||
|
||||
private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool {
|
||||
if listNode.scroller.isDragging {
|
||||
@ -151,6 +155,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
private let tabContainerNode: ChatListFilterTabContainerNode
|
||||
private var tabContainerData: ([ChatListFilterTabEntry], Bool)?
|
||||
|
||||
private var activeDownloadsDisposable: Disposable?
|
||||
private var clearUnseenDownloadsTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var didSetupTabs = false
|
||||
|
||||
public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
@ -467,6 +474,263 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
})
|
||||
self.searchContentNode?.updateExpansionProgress(0.0)
|
||||
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
|
||||
|
||||
enum State: Equatable {
|
||||
case empty
|
||||
case downloading(Double)
|
||||
case hasUnseen
|
||||
}
|
||||
|
||||
let entriesWithFetchStatuses = Signal<[(entry: FetchManagerEntrySummary, progress: Double)], NoError> { subscriber in
|
||||
let queue = Queue()
|
||||
final class StateHolder {
|
||||
final class EntryContext {
|
||||
var entry: FetchManagerEntrySummary
|
||||
var isRemoved: Bool = false
|
||||
var statusDisposable: Disposable?
|
||||
var status: MediaResourceStatus?
|
||||
|
||||
init(entry: FetchManagerEntrySummary) {
|
||||
self.entry = entry
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.statusDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
let queue: Queue
|
||||
|
||||
var entryContexts: [FetchManagerLocationEntryId: EntryContext] = [:]
|
||||
|
||||
let state = Promise<[(entry: FetchManagerEntrySummary, progress: Double)]>()
|
||||
|
||||
init(queue: Queue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
func update(engine: TelegramEngine, entries: [FetchManagerEntrySummary]) {
|
||||
if entries.isEmpty {
|
||||
self.entryContexts.removeAll()
|
||||
} else {
|
||||
for entry in entries {
|
||||
let context: EntryContext
|
||||
if let current = self.entryContexts[entry.id] {
|
||||
context = current
|
||||
} else {
|
||||
context = EntryContext(entry: entry)
|
||||
self.entryContexts[entry.id] = context
|
||||
}
|
||||
|
||||
context.entry = entry
|
||||
|
||||
if context.isRemoved {
|
||||
context.isRemoved = false
|
||||
context.status = nil
|
||||
context.statusDisposable?.dispose()
|
||||
context.statusDisposable = nil
|
||||
}
|
||||
}
|
||||
|
||||
for (_, context) in self.entryContexts {
|
||||
if !entries.contains(where: { $0.id == context.entry.id }) {
|
||||
context.isRemoved = true
|
||||
}
|
||||
|
||||
if context.statusDisposable == nil {
|
||||
context.statusDisposable = (engine.account.postbox.mediaBox.resourceStatus(context.entry.resourceReference.resource)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self, weak context] status in
|
||||
guard let strongSelf = self, let context = context else {
|
||||
return
|
||||
}
|
||||
if context.status != status {
|
||||
context.status = status
|
||||
strongSelf.notifyUpdatedIfReady()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.notifyUpdatedIfReady()
|
||||
}
|
||||
|
||||
func notifyUpdatedIfReady() {
|
||||
var result: [(entry: FetchManagerEntrySummary, progress: Double)] = []
|
||||
loop: for (_, context) in self.entryContexts {
|
||||
guard let status = context.status else {
|
||||
return
|
||||
}
|
||||
let progress: Double
|
||||
switch status {
|
||||
case .Local:
|
||||
progress = 1.0
|
||||
case .Remote:
|
||||
if context.isRemoved {
|
||||
continue loop
|
||||
}
|
||||
progress = 0.0
|
||||
case let .Paused(value):
|
||||
progress = Double(value)
|
||||
case let .Fetching(_, value):
|
||||
progress = Double(value)
|
||||
}
|
||||
result.append((context.entry, progress))
|
||||
}
|
||||
self.state.set(.single(result))
|
||||
}
|
||||
}
|
||||
let holder = QueueLocalObject<StateHolder>(queue: queue, generate: {
|
||||
return StateHolder(queue: queue)
|
||||
})
|
||||
let entriesDisposable = ((context.fetchManager as! FetchManagerImpl).entriesSummary).start(next: { entries in
|
||||
holder.with { holder in
|
||||
holder.update(engine: context.engine, entries: entries)
|
||||
}
|
||||
})
|
||||
let holderStateDisposable = MetaDisposable()
|
||||
holder.with { holder in
|
||||
holderStateDisposable.set(holder.state.get().start(next: { state in
|
||||
subscriber.putNext(state)
|
||||
}))
|
||||
}
|
||||
|
||||
return ActionDisposable {
|
||||
entriesDisposable.dispose()
|
||||
holderStateDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
let stateSignal: Signal<State, NoError> = (combineLatest(queue: .mainQueue(), entriesWithFetchStatuses, recentDownloadItems(postbox: context.account.postbox))
|
||||
|> map { entries, recentDownloadItems -> State in
|
||||
if !entries.isEmpty {
|
||||
var totalBytes = 0.0
|
||||
var totalProgressInBytes = 0.0
|
||||
for (entry, progress) in entries {
|
||||
var size = 1024 * 1024 * 1024
|
||||
if let sizeValue = entry.resourceReference.resource.size {
|
||||
size = sizeValue
|
||||
}
|
||||
totalBytes += Double(size)
|
||||
totalProgressInBytes += Double(size) * progress
|
||||
}
|
||||
let totalProgress: Double
|
||||
if totalBytes.isZero {
|
||||
totalProgress = 0.0
|
||||
} else {
|
||||
totalProgress = totalProgressInBytes / totalBytes
|
||||
}
|
||||
return .downloading(totalProgress)
|
||||
} else {
|
||||
for item in recentDownloadItems {
|
||||
if !item.isSeen {
|
||||
return .hasUnseen
|
||||
}
|
||||
}
|
||||
return .empty
|
||||
}
|
||||
}
|
||||
|> mapToSignal { value -> Signal<State, NoError> in
|
||||
return .single(value) |> delay(0.1, queue: .mainQueue())
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> deliverOnMainQueue)
|
||||
|
||||
/*if !"".isEmpty {
|
||||
stateSignal = Signal<State, NoError>.single(.downloading)
|
||||
|> then(Signal<State, NoError>.single(.hasUnseen) |> delay(3.0, queue: .mainQueue()))
|
||||
}*/
|
||||
|
||||
self.activeDownloadsDisposable = stateSignal.start(next: { [weak self] state in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let animation: LottieAnimationComponent.Animation?
|
||||
let progressValue: Double?
|
||||
switch state {
|
||||
case let .downloading(progress):
|
||||
animation = LottieAnimationComponent.Animation(
|
||||
name: "anim_search_downloading",
|
||||
colors: [
|
||||
"Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
],
|
||||
loop: true
|
||||
)
|
||||
progressValue = progress
|
||||
|
||||
strongSelf.clearUnseenDownloadsTimer?.invalidate()
|
||||
strongSelf.clearUnseenDownloadsTimer = nil
|
||||
case .hasUnseen:
|
||||
animation = LottieAnimationComponent.Animation(
|
||||
name: "anim_search_downloaded",
|
||||
colors: [
|
||||
"Fill 2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Mask1.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Mask2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Arrow3.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Fill.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
"Arrow2.Union.Fill 1": strongSelf.presentationData.theme.rootController.navigationSearchBar.inputFillColor.blitOver(strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, alpha: 1.0),
|
||||
],
|
||||
loop: false
|
||||
)
|
||||
progressValue = 1.0
|
||||
|
||||
if strongSelf.clearUnseenDownloadsTimer == nil {
|
||||
let timeout: Double
|
||||
#if DEBUG
|
||||
timeout = 10.0
|
||||
#else
|
||||
timeout = 1.0 * 60.0
|
||||
#endif
|
||||
strongSelf.clearUnseenDownloadsTimer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.clearUnseenDownloadsTimer = nil
|
||||
let _ = markAllRecentDownloadItemsAsSeen(postbox: strongSelf.context.account.postbox).start()
|
||||
}, queue: .mainQueue())
|
||||
strongSelf.clearUnseenDownloadsTimer?.start()
|
||||
}
|
||||
case .empty:
|
||||
animation = nil
|
||||
progressValue = nil
|
||||
|
||||
strongSelf.clearUnseenDownloadsTimer?.invalidate()
|
||||
strongSelf.clearUnseenDownloadsTimer = nil
|
||||
}
|
||||
|
||||
if let animation = animation, let progressValue = progressValue {
|
||||
let contentComponent = AnyComponent(ZStack<Empty>([
|
||||
AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieAnimationComponent(
|
||||
animation: animation,
|
||||
size: CGSize(width: 24.0, height: 24.0)
|
||||
))),
|
||||
AnyComponentWithIdentity(id: 1, component: AnyComponent(ProgressIndicatorComponent(
|
||||
diameter: 16.0,
|
||||
backgroundColor: .clear,
|
||||
foregroundColor: strongSelf.presentationData.theme.list.itemAccentColor,
|
||||
value: progressValue
|
||||
)))
|
||||
]))
|
||||
|
||||
strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button(
|
||||
content: contentComponent,
|
||||
insets: UIEdgeInsets(),
|
||||
action: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.activateSearch(filter: .downloads, query: nil)
|
||||
}
|
||||
)))
|
||||
} else {
|
||||
strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if enableDebugActions {
|
||||
@ -514,6 +778,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.stateDisposable.dispose()
|
||||
self.filterDisposable.dispose()
|
||||
self.featuredFiltersDisposable.dispose()
|
||||
self.activeDownloadsDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings() {
|
||||
|
@ -29,6 +29,7 @@ import ChatInterfaceState
|
||||
import ShareController
|
||||
import UndoUI
|
||||
import TextFormat
|
||||
import Postbox
|
||||
|
||||
private enum ChatListTokenId: Int32 {
|
||||
case archive
|
||||
@ -45,14 +46,14 @@ final class ChatListSearchInteraction {
|
||||
let clearRecentSearch: () -> Void
|
||||
let addContact: (String) -> Void
|
||||
let toggleMessageSelection: (EngineMessage.Id, Bool) -> Void
|
||||
let messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)
|
||||
let messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int, isFirstInList: Bool)?) -> Void)
|
||||
let mediaMessageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)
|
||||
let peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?
|
||||
let present: (ViewController, Any?) -> Void
|
||||
let dismissInput: () -> Void
|
||||
let getSelectedMessageIds: () -> Set<EngineMessage.Id>?
|
||||
|
||||
init(openPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set<EngineMessage.Id>?) {
|
||||
init(openPeer: @escaping (EnginePeer, EnginePeer?, Bool) -> Void, openDisabledPeer: @escaping (EnginePeer) -> Void, openMessage: @escaping (EnginePeer, EngineMessage.Id, Bool) -> Void, openUrl: @escaping (String) -> Void, clearRecentSearch: @escaping () -> Void, addContact: @escaping (String) -> Void, toggleMessageSelection: @escaping (EngineMessage.Id, Bool) -> Void, messageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int, isFirstInList: Bool)?) -> Void), mediaMessageContextAction: @escaping ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void), peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, getSelectedMessageIds: @escaping () -> Set<EngineMessage.Id>?) {
|
||||
self.openPeer = openPeer
|
||||
self.openDisabledPeer = openDisabledPeer
|
||||
self.openMessage = openMessage
|
||||
@ -112,8 +113,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
private var stateValue = ChatListSearchContainerNodeSearchState()
|
||||
private let statePromise = ValuePromise<ChatListSearchContainerNodeSearchState>()
|
||||
|
||||
private var selectedFilterKey: ChatListSearchFilterEntryId?
|
||||
private var selectedFilterKeyPromise = Promise<ChatListSearchFilterEntryId?>()
|
||||
private var selectedFilter: ChatListSearchFilterEntry?
|
||||
private var selectedFilterPromise = Promise<ChatListSearchFilterEntry?>()
|
||||
private var transitionFraction: CGFloat = 0.0
|
||||
|
||||
private weak var copyProtectionTooltipController: TooltipController?
|
||||
@ -134,8 +135,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
self.navigationController = navigationController
|
||||
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.selectedFilterKey = .filter(initialFilter.id)
|
||||
self.selectedFilterKeyPromise.set(.single(self.selectedFilterKey))
|
||||
self.selectedFilter = .filter(initialFilter)
|
||||
self.selectedFilterPromise.set(.single(self.selectedFilter))
|
||||
|
||||
self.openMessage = originalOpenMessage
|
||||
self.present = present
|
||||
@ -217,8 +218,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
return state.withUpdatedSelectedMessageIds(selectedMessageIds)
|
||||
}
|
||||
}
|
||||
}, messageContextAction: { [weak self] message, node, rect, gesture in
|
||||
self?.messageContextAction(message, node: node, rect: rect, gesture: gesture)
|
||||
}, messageContextAction: { [weak self] message, node, rect, gesture, paneKey, downloadResource in
|
||||
self?.messageContextAction(message, node: node, rect: rect, gesture: gesture, paneKey: paneKey, downloadResource: downloadResource)
|
||||
}, mediaMessageContextAction: { [weak self] message, node, rect, gesture in
|
||||
self?.mediaMessageContextAction(message, node: node, rect: rect, gesture: gesture)
|
||||
}, peerContextAction: { peer, source, node, gesture in
|
||||
@ -244,6 +245,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
filterKey = .chats
|
||||
case .media:
|
||||
filterKey = .media
|
||||
case .downloads:
|
||||
filterKey = .downloads
|
||||
case .links:
|
||||
filterKey = .links
|
||||
case .files:
|
||||
@ -253,8 +256,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
case .voice:
|
||||
filterKey = .voice
|
||||
}
|
||||
strongSelf.selectedFilterKey = .filter(filterKey.id)
|
||||
strongSelf.selectedFilterKeyPromise.set(.single(strongSelf.selectedFilterKey))
|
||||
strongSelf.selectedFilter = .filter(filterKey)
|
||||
strongSelf.selectedFilterPromise.set(.single(strongSelf.selectedFilter))
|
||||
strongSelf.transitionFraction = transitionFraction
|
||||
|
||||
if let (layout, _) = strongSelf.validLayout {
|
||||
@ -262,9 +265,9 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
if let suggestedFilters = strongSelf.suggestedFilters, !suggestedFilters.isEmpty {
|
||||
filters = suggestedFilters
|
||||
} else {
|
||||
filters = [.chats, .media, .links, .files, .music, .voice]
|
||||
filters = [.chats, .media, .downloads, .links, .files, .music, .voice]
|
||||
}
|
||||
strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilterKey, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition)
|
||||
strongSelf.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, selectedFilter: strongSelf.selectedFilter?.id, transitionFraction: strongSelf.transitionFraction, presentationData: strongSelf.presentationData, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -283,6 +286,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
key = .chats
|
||||
case .media:
|
||||
key = .media
|
||||
case .downloads:
|
||||
key = .downloads
|
||||
case .links:
|
||||
key = .links
|
||||
case .files:
|
||||
@ -306,7 +311,31 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
|
||||
self.filterContainerNode.filterPressed?(initialFilter)
|
||||
|
||||
let suggestedPeers = self.searchQuery.get()
|
||||
let searchQuerySignal = self.searchQuery.get()
|
||||
|
||||
let suggestedPeers = self.selectedFilterPromise.get()
|
||||
|> map { filter -> Bool in
|
||||
guard let filter = filter else {
|
||||
return false
|
||||
}
|
||||
switch filter {
|
||||
case let .filter(filter):
|
||||
switch filter {
|
||||
case .downloads:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { value -> Signal<String?, NoError> in
|
||||
if value {
|
||||
return searchQuerySignal
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|> mapToSignal { query -> Signal<[EnginePeer], NoError> in
|
||||
if let query = query {
|
||||
return context.account.postbox.searchPeers(query: query.lowercased())
|
||||
@ -321,13 +350,13 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
let accountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|
||||
|> take(1)
|
||||
|
||||
self.suggestedFiltersDisposable.set((combineLatest(suggestedPeers, self.suggestedDates.get(), self.selectedFilterKeyPromise.get(), self.searchQuery.get(), accountPeer)
|
||||
self.suggestedFiltersDisposable.set((combineLatest(suggestedPeers, self.suggestedDates.get(), self.selectedFilterPromise.get(), self.searchQuery.get(), accountPeer)
|
||||
|> mapToSignal { peers, dates, selectedFilter, searchQuery, accountPeer -> Signal<([EnginePeer], [(Date?, Date, String?)], ChatListSearchFilterEntryId?, String?, EnginePeer?), NoError> in
|
||||
if searchQuery?.isEmpty ?? true {
|
||||
return .single((peers, dates, selectedFilter, searchQuery, EnginePeer(accountPeer)))
|
||||
return .single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer)))
|
||||
} else {
|
||||
return (.complete() |> delay(0.25, queue: Queue.mainQueue()))
|
||||
|> then(.single((peers, dates, selectedFilter, searchQuery, EnginePeer(accountPeer))))
|
||||
|> then(.single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer))))
|
||||
}
|
||||
} |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> [ChatListSearchFilter] in
|
||||
var suggestedFilters: [ChatListSearchFilter] = []
|
||||
@ -523,6 +552,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
key = .music
|
||||
case .voice:
|
||||
key = .voice
|
||||
case .downloads:
|
||||
key = .downloads
|
||||
default:
|
||||
key = .chats
|
||||
}
|
||||
@ -549,11 +580,11 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
if let suggestedFilters = self.suggestedFilters, !suggestedFilters.isEmpty {
|
||||
filters = suggestedFilters
|
||||
} else {
|
||||
filters = [.chats, .media, .links, .files, .music, .voice]
|
||||
filters = [.chats, .media, .downloads, .links, .files, .music, .voice]
|
||||
}
|
||||
|
||||
let overflowInset: CGFloat = 20.0
|
||||
self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilterKey, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
|
||||
self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
|
||||
|
||||
var bottomIntrinsicInset = layout.intrinsicInsets.bottom
|
||||
if case .root = self.groupId {
|
||||
@ -737,10 +768,154 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
let _ = self.paneContainerNode.scrollToTop()
|
||||
}
|
||||
|
||||
private func messageContextAction(_ message: EngineMessage, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?) {
|
||||
private func messageContextAction(_ message: EngineMessage, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?, paneKey: ChatListSearchPaneKey, downloadResource: (id: String, size: Int, isFirstInList: Bool)?) {
|
||||
guard let node = node as? ContextExtractedContentContainingNode else {
|
||||
return
|
||||
}
|
||||
|
||||
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
|
||||
|
||||
if paneKey == .downloads {
|
||||
let isCachedValue: Signal<Bool, NoError>
|
||||
if let downloadResource = downloadResource {
|
||||
isCachedValue = self.context.account.postbox.mediaBox.resourceStatus(MediaResourceId(downloadResource.id), resourceSize: downloadResource.size)
|
||||
|> map { status -> Bool in
|
||||
switch status {
|
||||
case .Local:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
} else {
|
||||
isCachedValue = .single(false)
|
||||
}
|
||||
|
||||
let shouldBeDismissed: Signal<Bool, NoError> = Signal { subscriber in
|
||||
subscriber.putNext(false)
|
||||
let previous = Atomic<Bool?>(value: nil)
|
||||
return isCachedValue.start(next: { value in
|
||||
let previousSwapped = previous.swap(value)
|
||||
if let previousSwapped = previousSwapped, previousSwapped != value {
|
||||
subscriber.putNext(true)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let items = combineLatest(queue: .mainQueue(),
|
||||
context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: [message.id: message], peers: [:]),
|
||||
isCachedValue |> take(1)
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { [weak self] actions, isCachedValue -> [ContextMenuItem] in
|
||||
guard let strongSelf = self else {
|
||||
return []
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if isCachedValue {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Delete from Cache", textColor: .primary, icon: { _ in
|
||||
return nil
|
||||
}, action: { _, f in
|
||||
guard let strongSelf = self, let downloadResource = downloadResource else {
|
||||
f(.default)
|
||||
return
|
||||
}
|
||||
let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources([MediaResourceId(downloadResource.id)], notify: true)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
f(.dismissWithoutContent)
|
||||
})
|
||||
})))
|
||||
} else {
|
||||
if let downloadResource = downloadResource, !downloadResource.isFirstInList {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Raise Priority", textColor: .primary, icon: { _ in
|
||||
return nil
|
||||
}, action: { _, f in
|
||||
guard let strongSelf = self else {
|
||||
f(.default)
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.context.fetchManager.raisePriority(resourceId: downloadResource.id)
|
||||
|
||||
Queue.mainQueue().after(0.1, {
|
||||
f(.default)
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Cancel Downloading", textColor: .primary, icon: { _ in
|
||||
return nil
|
||||
}, action: { _, f in
|
||||
guard let strongSelf = self, let downloadResource = downloadResource else {
|
||||
f(.default)
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.context.fetchManager.cancelInteractiveFetches(resourceId: downloadResource.id)
|
||||
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
|
||||
c.dismiss(completion: { [weak self] in
|
||||
self?.openMessage(EnginePeer(message.peers[message.id.peerId]!), message.id, false)
|
||||
})
|
||||
})))
|
||||
|
||||
if !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty {
|
||||
if !items.isEmpty {
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
if actions.options.contains(.deleteLocally) {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, textColor: .destructive, icon: { _ in
|
||||
return nil
|
||||
}, action: { controller, f in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forLocalPeer).start()
|
||||
f(.dismissWithoutContent)
|
||||
})))
|
||||
}
|
||||
|
||||
if actions.options.contains(.deleteGlobally) {
|
||||
let text: String
|
||||
if let mainPeer = message.peers[message.id.peerId] {
|
||||
if mainPeer is TelegramUser {
|
||||
text = strongSelf.presentationData.strings.ChatList_DeleteForEveryone(EnginePeer(mainPeer).compactDisplayTitle).string
|
||||
} else {
|
||||
text = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||||
}
|
||||
} else {
|
||||
text = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: text, textColor: .destructive, icon: { _ in
|
||||
return nil
|
||||
}, action: { controller, f in
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: [message.id], type: .forEveryone).start()
|
||||
f(.dismissWithoutContent)
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(MessageContextExtractedContentSource(sourceNode: node, shouldBeDismissed: shouldBeDismissed)), items: items |> map { ContextController.Items(content: .list($0)) }, recognizer: nil, gesture: gesture)
|
||||
self.presentInGlobalOverlay?(controller, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let _ = storedMessageFromSearch(account: self.context.account, message: message._asMessage()).start()
|
||||
|
||||
var linkForCopying: String?
|
||||
@ -756,8 +931,6 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
}
|
||||
}
|
||||
|
||||
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
|
||||
|
||||
let context = self.context
|
||||
let (peers, messages) = self.currentMessages
|
||||
let items = context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: [message.id], messages: messages, peers: peers)
|
||||
@ -1150,10 +1323,13 @@ private final class MessageContextExtractedContentSource: ContextExtractedConten
|
||||
let ignoreContentTouches: Bool = true
|
||||
let blurBackground: Bool = true
|
||||
|
||||
let shouldBeDismissed: Signal<Bool, NoError>
|
||||
|
||||
private let sourceNode: ContextExtractedContentContainingNode
|
||||
|
||||
init(sourceNode: ContextExtractedContentContainingNode) {
|
||||
init(sourceNode: ContextExtractedContentContainingNode, shouldBeDismissed: Signal<Bool, NoError>? = nil) {
|
||||
self.sourceNode = sourceNode
|
||||
self.shouldBeDismissed = shouldBeDismissed ?? .single(false)
|
||||
}
|
||||
|
||||
func takeView() -> ContextControllerTakeViewInfo? {
|
||||
|
@ -84,6 +84,10 @@ private final class ItemNode: ASDisplayNode {
|
||||
case .media:
|
||||
title = presentationData.strings.ChatList_Search_FilterMedia
|
||||
icon = nil
|
||||
case .downloads:
|
||||
//TODO:localize
|
||||
title = "Downloads"
|
||||
icon = nil
|
||||
case .links:
|
||||
title = presentationData.strings.ChatList_Search_FilterLinks
|
||||
icon = nil
|
||||
|
@ -26,6 +26,8 @@ import AppBundle
|
||||
import ShimmerEffect
|
||||
import ChatListSearchRecentPeersNode
|
||||
import UndoUI
|
||||
import Postbox
|
||||
import FetchManagerImpl
|
||||
|
||||
private enum ChatListRecentEntryStableId: Hashable {
|
||||
case topPeers
|
||||
@ -217,7 +219,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
|
||||
public enum ChatListSearchEntryStableId: Hashable {
|
||||
case localPeerId(EnginePeer.Id)
|
||||
case globalPeerId(EnginePeer.Id)
|
||||
case messageId(EngineMessage.Id)
|
||||
case messageId(EngineMessage.Id, ChatListSearchEntry.MessageSection)
|
||||
case addContact
|
||||
}
|
||||
|
||||
@ -228,9 +230,54 @@ public enum ChatListSearchSectionExpandType {
|
||||
}
|
||||
|
||||
public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
public enum MessageOrderingKey: Comparable {
|
||||
case index(MessageIndex)
|
||||
case downloading(FetchManagerPriorityKey)
|
||||
case downloaded(timestamp: Int32, index: MessageIndex)
|
||||
|
||||
public static func <(lhs: MessageOrderingKey, rhs: MessageOrderingKey) -> Bool {
|
||||
switch lhs {
|
||||
case let .index(lhsIndex):
|
||||
if case let .index(rhsIndex) = rhs {
|
||||
return lhsIndex > rhsIndex
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
case let .downloading(lhsKey):
|
||||
switch rhs {
|
||||
case let .downloading(rhsKey):
|
||||
return lhsKey < rhsKey
|
||||
case .index:
|
||||
return false
|
||||
case .downloaded:
|
||||
return true
|
||||
}
|
||||
case let .downloaded(lhsTimestamp, lhsIndex):
|
||||
switch rhs {
|
||||
case let .downloaded(rhsTimestamp, rhsIndex):
|
||||
if lhsTimestamp != rhsTimestamp {
|
||||
return lhsTimestamp > rhsTimestamp
|
||||
} else {
|
||||
return lhsIndex > rhsIndex
|
||||
}
|
||||
case .downloading:
|
||||
return false
|
||||
case .index:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum MessageSection: Hashable {
|
||||
case generic
|
||||
case downloading
|
||||
case recentlyDownloaded
|
||||
}
|
||||
|
||||
case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType)
|
||||
case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType)
|
||||
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, ChatListPresentationData, Int32, Bool?, Bool)
|
||||
case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int, isFirstInList: Bool)?, MessageSection, Bool)
|
||||
case addContact(String, PresentationTheme, PresentationStrings)
|
||||
|
||||
public var stableId: ChatListSearchEntryStableId {
|
||||
@ -239,8 +286,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
return .localPeerId(peer.id)
|
||||
case let .globalPeer(peer, _, _, _, _, _, _, _):
|
||||
return .globalPeerId(peer.peer.id)
|
||||
case let .message(message, _, _, _, _, _, _):
|
||||
return .messageId(message.id)
|
||||
case let .message(message, _, _, _, _, _, _, _, _, section, _):
|
||||
return .messageId(message.id, section)
|
||||
case .addContact:
|
||||
return .addContact
|
||||
}
|
||||
@ -260,8 +307,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader):
|
||||
if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader) = rhs {
|
||||
case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection, lhsAllPaused):
|
||||
if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection, rhsAllPaused) = rhs {
|
||||
if lhsMessage.id != rhsMessage.id {
|
||||
return false
|
||||
}
|
||||
@ -286,6 +333,21 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
if lhsDisplayCustomHeader != rhsDisplayCustomHeader {
|
||||
return false
|
||||
}
|
||||
if lhsKey != rhsKey {
|
||||
return false
|
||||
}
|
||||
if lhsResourceId?.0 != rhsResourceId?.0 {
|
||||
return false
|
||||
}
|
||||
if lhsResourceId?.1 != rhsResourceId?.1 {
|
||||
return false
|
||||
}
|
||||
if lhsSection != rhsSection {
|
||||
return false
|
||||
}
|
||||
if lhsAllPaused != rhsAllPaused {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -325,9 +387,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
case .message, .addContact:
|
||||
return true
|
||||
}
|
||||
case let .message(lhsMessage, _, _, _, _, _, _):
|
||||
if case let .message(rhsMessage, _, _, _, _, _, _) = rhs {
|
||||
return lhsMessage.index < rhsMessage.index
|
||||
case let .message(_, _, _, _, _, _, _, lhsKey, _, _, _):
|
||||
if case let .message(_, _, _, _, _, _, _, rhsKey, _, _, _) = rhs {
|
||||
return lhsKey < rhsKey
|
||||
} else if case .addContact = rhs {
|
||||
return true
|
||||
} else {
|
||||
@ -338,7 +400,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ListViewItem {
|
||||
public func item(context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int, isFirstInList: Bool)?) -> Void)?, openStorageSettings: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ListViewItem {
|
||||
switch self {
|
||||
case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType):
|
||||
let primaryPeer: EnginePeer
|
||||
@ -486,11 +548,37 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
||||
peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture)
|
||||
}
|
||||
})
|
||||
case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader):
|
||||
let header = ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused):
|
||||
let header: ChatListSearchItemHeader
|
||||
switch orderingKey {
|
||||
case .downloading:
|
||||
//TODO:localize
|
||||
if allPaused {
|
||||
header = ChatListSearchItemHeader(type: .downloading, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Resume All", action: {
|
||||
toggleAllPaused()
|
||||
})
|
||||
} else {
|
||||
header = ChatListSearchItemHeader(type: .downloading, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Pause All", action: {
|
||||
toggleAllPaused()
|
||||
})
|
||||
}
|
||||
case .downloaded:
|
||||
//TODO:localize
|
||||
header = ChatListSearchItemHeader(type: .recentDownloads, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Settings", action: {
|
||||
openStorageSettings()
|
||||
})
|
||||
case .index:
|
||||
header = ChatListSearchItemHeader(type: .messages, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
}
|
||||
let selection: ChatHistoryMessageSelection = selected.flatMap { .selectable(selected: $0) } ?? .none
|
||||
var isMedia = false
|
||||
if let tagMask = tagMask, tagMask != .photoOrVideo {
|
||||
return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message._asMessage(), selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: true)
|
||||
isMedia = true
|
||||
} else if key == .downloads {
|
||||
isMedia = true
|
||||
}
|
||||
if isMedia {
|
||||
return ListMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .builtin(WallpaperSettings())), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0.0, auxiliaryRadius: 0.0, mergeBubbleCorners: false)), context: context, chatLocation: .peer(peer.peerId), interaction: listInteraction, message: message._asMessage(), selection: selection, displayHeader: enableHeaders && !displayCustomHeader, customHeader: key == .downloads ? header : nil, hintIsLink: tagMask == .webPage, isGlobalSearchResult: key != .downloads, isDownloadList: key == .downloads)
|
||||
} else {
|
||||
return ChatListItem(presentationData: presentationData, context: context, peerGroupId: .root, filterData: nil, index: EngineChatList.Item.Index(pinningIndex: nil, messageIndex: message.index), content: .peer(messages: [message], peer: peer, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
}
|
||||
@ -516,7 +604,7 @@ public struct ChatListSearchContainerTransition {
|
||||
public let isEmpty: Bool
|
||||
public let isLoading: Bool
|
||||
public let query: String?
|
||||
public let animated: Bool
|
||||
public var animated: Bool
|
||||
|
||||
public init(deletions: [ListViewDeleteItem], insertions: [ListViewInsertItem], updates: [ListViewUpdateItem], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, query: String?, animated: Bool) {
|
||||
self.deletions = deletions
|
||||
@ -540,12 +628,12 @@ private func chatListSearchContainerPreparedRecentTransition(from fromEntries: [
|
||||
return ChatListSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates)
|
||||
}
|
||||
|
||||
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?) -> Void)?) -> ChatListSearchContainerTransition {
|
||||
public func chatListSearchContainerPreparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, isEmpty: Bool, isLoading: Bool, animated: Bool, context: AccountContext, presentationData: PresentationData, enableHeaders: Bool, filter: ChatListNodePeersFilter, key: ChatListSearchPaneKey, tagMask: EngineMessage.Tags?, interaction: ChatListNodeInteraction, listInteraction: ListMessageItemInteraction, peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?) -> Void)?, toggleExpandLocalResults: @escaping () -> Void, toggleExpandGlobalResults: @escaping () -> Void, searchPeer: @escaping (EnginePeer) -> Void, searchQuery: String?, searchOptions: ChatListSearchOptions?, messageContextAction: ((EngineMessage, ASDisplayNode?, CGRect?, UIGestureRecognizer?, ChatListSearchPaneKey, (id: String, size: Int, isFirstInList: Bool)?) -> Void)?, openStorageSettings: @escaping () -> Void, toggleAllPaused: @escaping () -> Void) -> ChatListSearchContainerTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction), directionHint: nil) }
|
||||
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openStorageSettings: openStorageSettings, toggleAllPaused: toggleAllPaused), directionHint: nil) }
|
||||
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, presentationData: presentationData, enableHeaders: enableHeaders, filter: filter, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openStorageSettings: openStorageSettings, toggleAllPaused: toggleAllPaused), directionHint: nil) }
|
||||
|
||||
return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, isLoading: isLoading, query: searchQuery, animated: animated)
|
||||
}
|
||||
@ -617,6 +705,29 @@ public struct ChatListSearchOptions {
|
||||
}
|
||||
}
|
||||
|
||||
private struct DownloadItem: Equatable {
|
||||
let resourceId: MediaResourceId
|
||||
let message: Message
|
||||
let priority: FetchManagerPriorityKey
|
||||
let isPaused: Bool
|
||||
|
||||
static func ==(lhs: DownloadItem, rhs: DownloadItem) -> Bool {
|
||||
if lhs.resourceId != rhs.resourceId {
|
||||
return false
|
||||
}
|
||||
if lhs.message.id != rhs.message.id {
|
||||
return false
|
||||
}
|
||||
if lhs.priority != rhs.priority {
|
||||
return false
|
||||
}
|
||||
if lhs.isPaused != rhs.isPaused {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
private let context: AccountContext
|
||||
private let interaction: ChatListSearchInteraction
|
||||
@ -704,6 +815,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
tagMask = nil
|
||||
case .media:
|
||||
tagMask = .photoOrVideo
|
||||
case .downloads:
|
||||
tagMask = nil
|
||||
case .links:
|
||||
tagMask = .webPage
|
||||
case .files:
|
||||
@ -811,13 +924,135 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
let presentationDataPromise = self.presentationDataPromise
|
||||
let searchStatePromise = self.searchStatePromise
|
||||
let selectionPromise = self.selectedMessagesPromise
|
||||
let foundItems = combineLatest(searchQuery, searchOptions)
|
||||
|> mapToSignal { query, options -> Signal<([ChatListSearchEntry], Bool)?, NoError> in
|
||||
if query == nil && options == nil && tagMask == nil {
|
||||
|
||||
let downloadItems: Signal<(inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]), NoError>
|
||||
if key == .downloads {
|
||||
downloadItems = combineLatest(queue: .mainQueue(), (context.fetchManager as! FetchManagerImpl).entriesSummary, recentDownloadItems(postbox: context.account.postbox))
|
||||
|> mapToSignal { entries, recentDownloadItems -> Signal<(inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]), NoError> in
|
||||
var itemSignals: [Signal<DownloadItem?, NoError>] = []
|
||||
|
||||
for entry in entries {
|
||||
switch entry.id.locationKey {
|
||||
case let .messageId(id):
|
||||
itemSignals.append(context.account.postbox.transaction { transaction -> DownloadItem? in
|
||||
if let message = transaction.getMessage(id) {
|
||||
return DownloadItem(resourceId: entry.resourceReference.resource.id, message: message, priority: entry.priority, isPaused: entry.isPaused)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return combineLatest(queue: .mainQueue(), itemSignals)
|
||||
|> map { items -> (inProgressItems: [DownloadItem], doneItems: [RenderedRecentDownloadItem]) in
|
||||
return (items.compactMap { $0 }, recentDownloadItems)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
downloadItems = .single(([], []))
|
||||
}
|
||||
|
||||
let foundItems = combineLatest(queue: .mainQueue(), searchQuery, searchOptions, downloadItems)
|
||||
|> mapToSignal { [weak self] query, options, downloadItems -> Signal<([ChatListSearchEntry], Bool)?, NoError> in
|
||||
if query == nil && options == nil && key == .chats {
|
||||
let _ = currentRemotePeers.swap(nil)
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
if key == .downloads {
|
||||
let queryTokens = stringIndexTokens(query ?? "", transliteration: .combined)
|
||||
|
||||
func messageMatchesTokens(message: EngineMessage, tokens: [ValueBoxKey]) -> Bool {
|
||||
for media in message.media {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if let fileName = file.fileName {
|
||||
if matchStringIndexTokens(stringIndexTokens(fileName, transliteration: .none), with: tokens) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if let _ = media as? TelegramMediaImage {
|
||||
if matchStringIndexTokens(stringIndexTokens("Photo Image", transliteration: .none), with: tokens) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return presentationDataPromise.get()
|
||||
|> map { presentationData -> ([ChatListSearchEntry], Bool)? in
|
||||
var entries: [ChatListSearchEntry] = []
|
||||
var existingMessageIds = Set<MessageId>()
|
||||
|
||||
var allPaused = true
|
||||
for item in downloadItems.inProgressItems {
|
||||
if !item.isPaused {
|
||||
allPaused = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for item in downloadItems.inProgressItems.sorted(by: { $0.priority < $1.priority }) {
|
||||
if existingMessageIds.contains(item.message.id) {
|
||||
continue
|
||||
}
|
||||
existingMessageIds.insert(item.message.id)
|
||||
|
||||
let message = EngineMessage(item.message)
|
||||
|
||||
if !queryTokens.isEmpty {
|
||||
if !messageMatchesTokens(message: message, tokens: queryTokens) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var peer = EngineRenderedPeer(message: message)
|
||||
if let group = item.message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference {
|
||||
if let channelPeer = message.peers[migrationReference.peerId] {
|
||||
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
|
||||
}
|
||||
}
|
||||
|
||||
var resource: (id: String, size: Int, isFirstInList: Bool)?
|
||||
if let resourceValue = findMediaResourceById(message: item.message, resourceId: item.resourceId), let size = resourceValue.size {
|
||||
resource = (resourceValue.id.stringRepresentation, size, entries.isEmpty)
|
||||
}
|
||||
|
||||
entries.append(.message(message, peer, nil, presentationData, 1, nil, false, .downloading(item.priority), resource, .downloading, allPaused))
|
||||
}
|
||||
for item in downloadItems.doneItems.sorted(by: { ChatListSearchEntry.MessageOrderingKey.downloaded(timestamp: $0.timestamp, index: $0.message.index) < ChatListSearchEntry.MessageOrderingKey.downloaded(timestamp: $1.timestamp, index: $1.message.index) }) {
|
||||
if !item.isSeen {
|
||||
Queue.mainQueue().async {
|
||||
self?.scheduleMarkRecentDownloadsAsSeen()
|
||||
}
|
||||
}
|
||||
if existingMessageIds.contains(item.message.id) {
|
||||
continue
|
||||
}
|
||||
existingMessageIds.insert(item.message.id)
|
||||
|
||||
let message = EngineMessage(item.message)
|
||||
|
||||
if !queryTokens.isEmpty {
|
||||
if !messageMatchesTokens(message: message, tokens: queryTokens) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var peer = EngineRenderedPeer(message: message)
|
||||
if let group = item.message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference {
|
||||
if let channelPeer = message.peers[migrationReference.peerId] {
|
||||
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
|
||||
}
|
||||
}
|
||||
entries.append(.message(message, peer, nil, presentationData, 1, nil, false, .downloaded(timestamp: item.timestamp, index: message.index), (item.resourceId, item.size, false), .recentlyDownloaded, false))
|
||||
}
|
||||
return (entries.sorted(), false)
|
||||
}
|
||||
}
|
||||
|
||||
let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1)
|
||||
let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)]), NoError>
|
||||
if let query = query {
|
||||
@ -1108,7 +1343,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
|
||||
}
|
||||
}
|
||||
entries.append(.message(message, peer, nil, presentationData, 1, nil, true))
|
||||
entries.append(.message(message, peer, nil, presentationData, 1, nil, true, .index(message.index), nil, .generic, false))
|
||||
index += 1
|
||||
}
|
||||
|
||||
@ -1131,7 +1366,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
peer = EngineRenderedPeer(peer: EnginePeer(channelPeer))
|
||||
}
|
||||
}
|
||||
entries.append(.message(message, peer, foundRemoteMessages.0.1[message.id.peerId], presentationData, foundRemoteMessages.0.2, selectionState?.contains(message.id), headerId == firstHeaderId))
|
||||
entries.append(.message(message, peer, foundRemoteMessages.0.1[message.id.peerId], presentationData, foundRemoteMessages.0.2, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic, false))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
@ -1249,8 +1484,24 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
})
|
||||
|
||||
let listInteraction = ListMessageItemInteraction(openMessage: { [weak self] message, mode -> Bool in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
}
|
||||
interaction.dismissInput()
|
||||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: nil, chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: {
|
||||
|
||||
let gallerySource: GalleryControllerItemSource
|
||||
|
||||
if strongSelf.key == .downloads {
|
||||
gallerySource = .peerMessagesAtId(messageId: message.id, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil))
|
||||
} else {
|
||||
gallerySource = .custom(messages: foundMessages |> map { message, a, b in
|
||||
return (message.map { $0._asMessage() }, a, b)
|
||||
}, messageId: message.id, loadMore: {
|
||||
loadMore()
|
||||
})
|
||||
}
|
||||
|
||||
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: nil, message: message, standalone: false, reverseMessageGalleryOrder: true, mode: mode, navigationController: navigationController, dismissInput: {
|
||||
interaction.dismissInput()
|
||||
}, present: { c, a in
|
||||
interaction.present(c, a)
|
||||
@ -1277,13 +1528,25 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
return (message.map { $0._asMessage() }, a, b)
|
||||
}, at: message.id, loadMore: {
|
||||
loadMore()
|
||||
}), gallerySource: .custom(messages: foundMessages |> map { message, a, b in
|
||||
return (message.map { $0._asMessage() }, a, b)
|
||||
}, messageId: message.id, loadMore: {
|
||||
loadMore()
|
||||
})))
|
||||
}, openMessageContextMenu: { message, _, node, rect, gesture in
|
||||
interaction.messageContextAction(EngineMessage(message), node, rect, gesture)
|
||||
}), gallerySource: gallerySource))
|
||||
}, openMessageContextMenu: { [weak self] message, _, node, rect, gesture in
|
||||
guard let strongSelf = self, let currentEntries = strongSelf.currentEntries else {
|
||||
return
|
||||
}
|
||||
|
||||
var fetchResourceId: (id: String, size: Int, isFirstInList: Bool)?
|
||||
for entry in currentEntries {
|
||||
switch entry {
|
||||
case let .message(m, _, _, _, _, _, _, _, resource, _, _):
|
||||
if m.id == message.id {
|
||||
fetchResourceId = resource
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
interaction.messageContextAction(EngineMessage(message), node, rect, gesture, key, fetchResourceId)
|
||||
}, toggleMessagesSelection: { messageId, selected in
|
||||
if let messageId = messageId.first {
|
||||
interaction.toggleMessageSelection(messageId, selected)
|
||||
@ -1355,7 +1618,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
|
||||
let animated = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil)
|
||||
let firstTime = previousEntries == nil
|
||||
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture in
|
||||
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags?.0 != nil, isEmpty: !isSearching && (entriesAndFlags?.0.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, key: strongSelf.key, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture in
|
||||
interaction.peerContextAction?(message, node, rect, gesture)
|
||||
}, toggleExpandLocalResults: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
@ -1376,15 +1639,46 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
return state
|
||||
}
|
||||
}, searchPeer: { peer in
|
||||
}, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture in
|
||||
interaction.messageContextAction(message, node, rect, gesture)
|
||||
}, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResource in
|
||||
interaction.messageContextAction(message, node, rect, gesture, paneKey, downloadResource)
|
||||
}, openStorageSettings: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.context.sharedContext.openStorageUsage(context: strongSelf.context)
|
||||
}, toggleAllPaused: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = ((strongSelf.context.fetchManager as! FetchManagerImpl).entriesSummary
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { entries in
|
||||
guard let strongSelf = self, !entries.isEmpty else {
|
||||
return
|
||||
}
|
||||
var allPaused = true
|
||||
for entry in entries {
|
||||
if !entry.isPaused {
|
||||
allPaused = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
strongSelf.context.fetchManager.toggleInteractiveFetchPaused(resourceId: entry.resourceReference.resource.id.stringRepresentation, isPaused: !allPaused)
|
||||
}
|
||||
})
|
||||
})
|
||||
strongSelf.currentEntries = newEntries
|
||||
/*if key == .downloads, !firstTime {
|
||||
transition.animated = true
|
||||
}*/
|
||||
strongSelf.enqueueTransition(transition, firstTime: firstTime)
|
||||
|
||||
var messages: [EngineMessage] = []
|
||||
for entry in newEntries {
|
||||
if case let .message(message, _, _, _, _, _, _) = entry {
|
||||
if case let .message(message, _, _, _, _, _, _, _, _, _, _) = entry {
|
||||
messages.append(message)
|
||||
}
|
||||
}
|
||||
@ -1652,6 +1946,27 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}
|
||||
}
|
||||
|
||||
func didBecomeFocused() {
|
||||
if self.key == .downloads {
|
||||
self.scheduleMarkRecentDownloadsAsSeen()
|
||||
}
|
||||
}
|
||||
|
||||
private var scheduledMarkRecentDownloadsAsSeen: Bool = false
|
||||
|
||||
func scheduleMarkRecentDownloadsAsSeen() {
|
||||
if !self.scheduledMarkRecentDownloadsAsSeen {
|
||||
self.scheduledMarkRecentDownloadsAsSeen = true
|
||||
Queue.mainQueue().after(0.1, { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.scheduledMarkRecentDownloadsAsSeen = false
|
||||
let _ = markAllRecentDownloadItemsAsSeen(postbox: strongSelf.context.account.postbox).start()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func scrollToTop() -> Bool {
|
||||
if !self.mediaNode.isHidden {
|
||||
return self.mediaNode.scrollToTop()
|
||||
@ -2110,16 +2425,26 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsAnimationNode.visibility = emptyResults
|
||||
if strongSelf.key == .downloads {
|
||||
strongSelf.emptyResultsAnimationNode.isHidden = true
|
||||
strongSelf.emptyResultsTitleNode.isHidden = true
|
||||
strongSelf.emptyResultsTextNode.isHidden = true
|
||||
strongSelf.emptyResultsAnimationNode.visibility = false
|
||||
} else {
|
||||
strongSelf.emptyResultsAnimationNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsTitleNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsTextNode.isHidden = !emptyResults
|
||||
strongSelf.emptyResultsAnimationNode.visibility = emptyResults
|
||||
}
|
||||
|
||||
let displayPlaceholder = transition.isLoading && (strongSelf.key != .chats || (strongSelf.currentEntries?.isEmpty ?? true))
|
||||
var displayPlaceholder = transition.isLoading && (strongSelf.key != .chats || (strongSelf.currentEntries?.isEmpty ?? true))
|
||||
if strongSelf.key == .downloads {
|
||||
displayPlaceholder = false
|
||||
}
|
||||
|
||||
let targetAlpha: CGFloat = displayPlaceholder ? 1.0 : 0.0
|
||||
if strongSelf.shimmerNode.alpha != targetAlpha {
|
||||
let transition: ContainedViewLayoutTransition = displayPlaceholder ? .immediate : .animated(duration: 0.2, curve: .linear)
|
||||
let transition: ContainedViewLayoutTransition = (displayPlaceholder || isFirstTime) ? .immediate : .animated(duration: 0.2, curve: .linear)
|
||||
transition.updateAlpha(node: strongSelf.shimmerNode, alpha: targetAlpha, delay: 0.1)
|
||||
}
|
||||
|
||||
@ -2334,7 +2659,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
|
||||
let items = (0 ..< 2).compactMap { _ -> ListViewItem? in
|
||||
switch key {
|
||||
case .chats:
|
||||
case .chats, .downloads:
|
||||
let message = EngineMessage(
|
||||
stableId: 0,
|
||||
stableVersion: 0,
|
||||
@ -2358,7 +2683,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
associatedMessageIds: []
|
||||
)
|
||||
let readState = EnginePeerReadCounters()
|
||||
return ChatListItem(presentationData: chatListPresentationData, context: context, peerGroupId: .root, filterData: nil, index: EngineChatList.Item.Index(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(messages: [message], peer: EngineRenderedPeer(peer: peer1), combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
return ChatListItem(presentationData: chatListPresentationData, context: context, peerGroupId: .root, filterData: nil, index: EngineChatList.Item.Index(pinningIndex: 0, messageIndex: EngineMessage.Index(id: EngineMessage.Id(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(messages: [message], peer: EngineRenderedPeer(peer: peer1), combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
case .media:
|
||||
return nil
|
||||
case .links:
|
||||
|
@ -165,7 +165,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
messageMediaFileCancelInteractiveFetch(context: self.context, messageId: message.id, file: file)
|
||||
case .Local:
|
||||
self.interaction.openMessage(message)
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
self.fetchDisposable.set(messageMediaFileInteractiveFetched(context: self.context, message: message, file: file, userInitiated: true).start())
|
||||
}
|
||||
}
|
||||
@ -236,7 +236,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
|
||||
case .Local:
|
||||
statusState = .none
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
statusState = .download(.white)
|
||||
}
|
||||
}
|
||||
@ -270,7 +270,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
mediaDownloadState = .compactFetching(progress: 0.0)
|
||||
case .Local:
|
||||
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
|
||||
mediaDownloadState = .compactRemote
|
||||
}
|
||||
@ -702,7 +702,7 @@ final class ChatListSearchMediaNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
var index: UInt32 = 0
|
||||
if let entries = entries {
|
||||
for entry in entries {
|
||||
if case let .message(message, _, _, _, _, _, _) = entry {
|
||||
if case let .message(message, _, _, _, _, _, _, _, _, _, _) = entry {
|
||||
self.mediaItems.append(VisualMediaItem(message: message._asMessage(), index: nil))
|
||||
}
|
||||
index += 1
|
||||
|
@ -19,6 +19,7 @@ protocol ChatListSearchPaneNode: ASDisplayNode {
|
||||
func updateHiddenMedia()
|
||||
func updateSelectedMessages(animated: Bool)
|
||||
func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)?
|
||||
func didBecomeFocused()
|
||||
var searchCurrentMessages: [EngineMessage]? { get }
|
||||
}
|
||||
|
||||
@ -44,16 +45,17 @@ final class ChatListSearchPaneWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatListSearchPaneKey {
|
||||
public enum ChatListSearchPaneKey {
|
||||
case chats
|
||||
case media
|
||||
case downloads
|
||||
case links
|
||||
case files
|
||||
case music
|
||||
case voice
|
||||
}
|
||||
|
||||
let defaultAvailableSearchPanes: [ChatListSearchPaneKey] = [.chats, .media, .links, .files, .music, .voice]
|
||||
let defaultAvailableSearchPanes: [ChatListSearchPaneKey] = [.chats, .media, .downloads, .links, .files, .music, .voice]
|
||||
|
||||
struct ChatListSearchPaneSpecifier: Equatable {
|
||||
var key: ChatListSearchPaneKey
|
||||
@ -475,6 +477,9 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, UIGestureRecognizerD
|
||||
})
|
||||
}
|
||||
pane.update(size: paneFrame.size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition)
|
||||
if paneWasAdded && key == self.currentPaneKey {
|
||||
pane.node.didBecomeFocused()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -305,13 +305,6 @@ public class InteractiveCheckNode: CheckNode {
|
||||
}
|
||||
}
|
||||
|
||||
private final class NullActionClass: NSObject, CAAction {
|
||||
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
|
||||
}
|
||||
}
|
||||
|
||||
private let nullAction = NullActionClass()
|
||||
|
||||
public class CheckLayer: CALayer {
|
||||
private var animatingOut = false
|
||||
private var animationProgress: CGFloat = 0.0
|
||||
|
@ -0,0 +1,3 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
49
submodules/ComponentFlow/Source/Components/ZStack.swift
Normal file
49
submodules/ComponentFlow/Source/Components/ZStack.swift
Normal file
@ -0,0 +1,49 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class ZStack<ChildEnvironment: Equatable>: CombinedComponent {
|
||||
public typealias EnvironmentType = ChildEnvironment
|
||||
|
||||
private let items: [AnyComponentWithIdentity<ChildEnvironment>]
|
||||
|
||||
public init(_ items: [AnyComponentWithIdentity<ChildEnvironment>]) {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
public static func ==(lhs: ZStack<ChildEnvironment>, rhs: ZStack<ChildEnvironment>) -> Bool {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public static var body: Body {
|
||||
let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self)
|
||||
|
||||
return { context in
|
||||
let updatedChildren = context.component.items.map { item in
|
||||
return children[item.id].update(
|
||||
component: item.component, environment: {
|
||||
context.environment[ChildEnvironment.self]
|
||||
},
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
}
|
||||
|
||||
var size = CGSize(width: 0.0, height: 0.0)
|
||||
for child in updatedChildren {
|
||||
size.width = max(size.width, child.size.width)
|
||||
size.height = max(size.height, child.size.height)
|
||||
}
|
||||
|
||||
for child in updatedChildren {
|
||||
context.add(child
|
||||
.position(child.size.centered(in: CGRect(origin: CGPoint(), size: size)).center)
|
||||
)
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
20
submodules/Components/LottieAnimationComponent/BUILD
Normal file
20
submodules/Components/LottieAnimationComponent/BUILD
Normal file
@ -0,0 +1,20 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "LottieAnimationComponent",
|
||||
module_name = "LottieAnimationComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/lottie-ios:Lottie",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,101 @@
|
||||
import Foundation
|
||||
import ComponentFlow
|
||||
import Lottie
|
||||
import AppBundle
|
||||
|
||||
public final class LottieAnimationComponent: Component {
|
||||
public struct Animation: Equatable {
|
||||
public var name: String
|
||||
public var loop: Bool
|
||||
public var colors: [String: UIColor]
|
||||
|
||||
public init(name: String, colors: [String: UIColor], loop: Bool) {
|
||||
self.name = name
|
||||
self.colors = colors
|
||||
self.loop = loop
|
||||
}
|
||||
}
|
||||
|
||||
public let animation: Animation
|
||||
public let size: CGSize
|
||||
|
||||
public init(animation: Animation, size: CGSize) {
|
||||
self.animation = animation
|
||||
self.size = size
|
||||
}
|
||||
|
||||
public static func ==(lhs: LottieAnimationComponent, rhs: LottieAnimationComponent) -> Bool {
|
||||
if lhs.animation != rhs.animation {
|
||||
return false
|
||||
}
|
||||
if lhs.size != rhs.size {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var currentAnimation: Animation?
|
||||
|
||||
private var colorCallbacks: [LOTColorValueCallback] = []
|
||||
private var animationView: LOTAnimationView?
|
||||
|
||||
func update(component: LottieAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let size = CGSize(width: min(component.size.width, availableSize.width), height: min(component.size.height, availableSize.height))
|
||||
|
||||
if self.currentAnimation != component.animation {
|
||||
if let animationView = self.animationView, animationView.isAnimationPlaying {
|
||||
animationView.completionBlock = { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.update(component: component, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
animationView.loopAnimation = false
|
||||
} else {
|
||||
self.currentAnimation = component.animation
|
||||
|
||||
self.animationView?.removeFromSuperview()
|
||||
|
||||
if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"), let composition = LOTComposition(filePath: url.path) {
|
||||
let view = LOTAnimationView(model: composition, in: getAppBundle())
|
||||
view.loopAnimation = component.animation.loop
|
||||
view.animationSpeed = 1.0
|
||||
view.backgroundColor = .clear
|
||||
view.isOpaque = false
|
||||
|
||||
//view.logHierarchyKeypaths()
|
||||
|
||||
for (key, value) in component.animation.colors {
|
||||
let colorCallback = LOTColorValueCallback(color: value.cgColor)
|
||||
self.colorCallbacks.append(colorCallback)
|
||||
view.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color"))
|
||||
}
|
||||
|
||||
self.animationView = view
|
||||
self.addSubview(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let animationView = self.animationView {
|
||||
animationView.frame = CGRect(origin: CGPoint(x: floor((size.width - component.size.width) / 2.0), y: floor((size.height - component.size.height) / 2.0)), size: component.size)
|
||||
|
||||
if !animationView.isAnimationPlaying {
|
||||
animationView.play { _ in
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
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, transition: transition)
|
||||
}
|
||||
}
|
18
submodules/Components/MetalImageView/BUILD
Normal file
18
submodules/Components/MetalImageView/BUILD
Normal file
@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "MetalImageView",
|
||||
module_name = "MetalImageView",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display:Display",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,252 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Metal
|
||||
import Display
|
||||
|
||||
private func alignUp(size: Int, align: Int) -> Int {
|
||||
precondition(((align - 1) & align) == 0, "Align must be a power of two")
|
||||
|
||||
let alignmentMask = align - 1
|
||||
return (size + alignmentMask) & ~alignmentMask
|
||||
}
|
||||
|
||||
open class MetalImageLayer: CALayer {
|
||||
fileprivate final class TextureStoragePool {
|
||||
let width: Int
|
||||
let height: Int
|
||||
|
||||
private var items: [TextureStorage.Content] = []
|
||||
|
||||
init(width: Int, height: Int) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
}
|
||||
|
||||
func recycle(content: TextureStorage.Content) {
|
||||
if self.items.count < 4 {
|
||||
self.items.append(content)
|
||||
} else {
|
||||
print("Warning: over-recycling texture storage")
|
||||
}
|
||||
}
|
||||
|
||||
func take() -> TextureStorage.Content? {
|
||||
if self.items.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return self.items.removeLast()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate final class TextureStorage {
|
||||
final class Content {
|
||||
#if !targetEnvironment(simulator)
|
||||
let buffer: MTLBuffer
|
||||
#endif
|
||||
|
||||
let width: Int
|
||||
let height: Int
|
||||
let bytesPerRow: Int
|
||||
let texture: MTLTexture
|
||||
|
||||
init?(device: MTLDevice, width: Int, height: Int) {
|
||||
if #available(iOS 12.0, *) {
|
||||
let bytesPerPixel = 4
|
||||
let pixelRowAlignment = device.minimumLinearTextureAlignment(for: .bgra8Unorm)
|
||||
let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment)
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bytesPerRow = bytesPerRow
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.pixelFormat = .bgra8Unorm
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.usage = [.renderTarget]
|
||||
textureDescriptor.storageMode = .shared
|
||||
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
return nil
|
||||
}
|
||||
#else
|
||||
guard let buffer = device.makeBuffer(length: bytesPerRow * height, options: MTLResourceOptions.storageModeShared) else {
|
||||
return nil
|
||||
}
|
||||
self.buffer = buffer
|
||||
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.pixelFormat = .bgra8Unorm
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.usage = [.renderTarget]
|
||||
textureDescriptor.storageMode = buffer.storageMode
|
||||
|
||||
guard let texture = buffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: bytesPerRow) else {
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
self.texture = texture
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private weak var pool: TextureStoragePool?
|
||||
let content: Content
|
||||
private var isInvalidated: Bool = false
|
||||
|
||||
init(pool: TextureStoragePool, content: Content) {
|
||||
self.pool = pool
|
||||
self.content = content
|
||||
}
|
||||
|
||||
deinit {
|
||||
if !self.isInvalidated {
|
||||
self.pool?.recycle(content: self.content)
|
||||
}
|
||||
}
|
||||
|
||||
func createCGImage() -> CGImage? {
|
||||
if self.isInvalidated {
|
||||
return nil
|
||||
}
|
||||
self.isInvalidated = true
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
guard let data = NSMutableData(capacity: self.content.bytesPerRow * self.content.height) else {
|
||||
return nil
|
||||
}
|
||||
data.length = self.content.bytesPerRow * self.content.height
|
||||
self.content.texture.getBytes(data.mutableBytes, bytesPerRow: self.content.bytesPerRow, bytesPerImage: self.content.bytesPerRow * self.content.height, from: MTLRegion(origin: MTLOrigin(), size: MTLSize(width: self.content.width, height: self.content.height, depth: 1)), mipmapLevel: 0, slice: 0)
|
||||
|
||||
guard let dataProvider = CGDataProvider(data: data as CFData) else {
|
||||
return nil
|
||||
}
|
||||
#else
|
||||
let content = self.content
|
||||
let pool = self.pool
|
||||
guard let dataProvider = CGDataProvider(data: Data(bytesNoCopy: self.content.buffer.contents(), count: self.content.buffer.length, deallocator: .custom { [weak pool] _, _ in
|
||||
guard let pool = pool else {
|
||||
return
|
||||
}
|
||||
pool.recycle(content: content)
|
||||
}) as CFData) else {
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
guard let image = CGImage(
|
||||
width: Int(self.content.width),
|
||||
height: Int(self.content.height),
|
||||
bitsPerComponent: 8,
|
||||
bitsPerPixel: 8 * 4,
|
||||
bytesPerRow: self.content.bytesPerRow,
|
||||
space: DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo,
|
||||
provider: dataProvider,
|
||||
decode: nil,
|
||||
shouldInterpolate: true,
|
||||
intent: .defaultIntent
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
public final class Drawable {
|
||||
private weak var renderer: Renderer?
|
||||
fileprivate let textureStorage: TextureStorage
|
||||
public var texture: MTLTexture {
|
||||
return self.textureStorage.content.texture
|
||||
}
|
||||
|
||||
fileprivate init(renderer: Renderer, textureStorage: TextureStorage) {
|
||||
self.renderer = renderer
|
||||
self.textureStorage = textureStorage
|
||||
}
|
||||
|
||||
public func present(completion: @escaping () -> Void) {
|
||||
self.renderer?.present(drawable: self)
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
public final class Renderer {
|
||||
public var device: MTLDevice?
|
||||
private var storagePool: TextureStoragePool?
|
||||
|
||||
public var imageUpdated: ((CGImage?) -> Void)?
|
||||
|
||||
public var drawableSize: CGSize = CGSize() {
|
||||
didSet {
|
||||
if self.drawableSize != oldValue {
|
||||
if !self.drawableSize.width.isZero && !self.drawableSize.height.isZero {
|
||||
self.storagePool = TextureStoragePool(width: Int(self.drawableSize.width), height: Int(self.drawableSize.height))
|
||||
} else {
|
||||
self.storagePool = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func nextDrawable() -> Drawable? {
|
||||
guard let device = self.device else {
|
||||
return nil
|
||||
}
|
||||
guard let storagePool = self.storagePool else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let content = storagePool.take() {
|
||||
return Drawable(renderer: self, textureStorage: TextureStorage(pool: storagePool, content: content))
|
||||
} else {
|
||||
guard let content = TextureStorage.Content(device: device, width: storagePool.width, height: storagePool.height) else {
|
||||
return nil
|
||||
}
|
||||
return Drawable(renderer: self, textureStorage: TextureStorage(pool: storagePool, content: content))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func present(drawable: Drawable) {
|
||||
if let imageUpdated = self.imageUpdated {
|
||||
imageUpdated(drawable.textureStorage.createCGImage())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public let renderer = Renderer()
|
||||
|
||||
override public init() {
|
||||
super.init()
|
||||
|
||||
self.renderer.imageUpdated = { [weak self] image in
|
||||
self?.contents = image
|
||||
}
|
||||
}
|
||||
|
||||
override public init(layer: Any) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override open func action(forKey event: String) -> CAAction? {
|
||||
return nullAction
|
||||
}
|
||||
}
|
||||
|
||||
open class MetalImageView: UIView {
|
||||
public static override var layerClass: AnyClass {
|
||||
return MetalImageLayer.self
|
||||
}
|
||||
}
|
19
submodules/Components/ProgressIndicatorComponent/BUILD
Normal file
19
submodules/Components/ProgressIndicatorComponent/BUILD
Normal file
@ -0,0 +1,19 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ProgressIndicatorComponent",
|
||||
module_name = "ProgressIndicatorComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/Display:Display",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,113 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
public final class ProgressIndicatorComponent: Component {
|
||||
public let diameter: CGFloat
|
||||
public let value: Double
|
||||
public let backgroundColor: UIColor
|
||||
public let foregroundColor: UIColor
|
||||
|
||||
public init(
|
||||
diameter: CGFloat,
|
||||
backgroundColor: UIColor,
|
||||
foregroundColor: UIColor,
|
||||
value: Double
|
||||
) {
|
||||
self.diameter = diameter
|
||||
self.backgroundColor = backgroundColor
|
||||
self.foregroundColor = foregroundColor
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public static func ==(lhs: ProgressIndicatorComponent, rhs: ProgressIndicatorComponent) -> Bool {
|
||||
if lhs.diameter != rhs.diameter {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.foregroundColor != rhs.foregroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var currentComponent: ProgressIndicatorComponent?
|
||||
|
||||
private let foregroundShapeLayer: SimpleShapeLayer
|
||||
|
||||
public init() {
|
||||
self.foregroundShapeLayer = SimpleShapeLayer()
|
||||
self.foregroundShapeLayer.isOpaque = false
|
||||
self.foregroundShapeLayer.backgroundColor = nil
|
||||
self.foregroundShapeLayer.fillColor = nil
|
||||
self.foregroundShapeLayer.lineCap = .round
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
let shapeLayer = self.layer as! CAShapeLayer
|
||||
shapeLayer.isOpaque = false
|
||||
shapeLayer.backgroundColor = nil
|
||||
shapeLayer.fillColor = nil
|
||||
|
||||
self.layer.addSublayer(self.foregroundShapeLayer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public static var layerClass: AnyClass {
|
||||
return CAShapeLayer.self
|
||||
}
|
||||
|
||||
func update(component: ProgressIndicatorComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let lineWidth: CGFloat = 1.33
|
||||
let size = CGSize(width: component.diameter, height: component.diameter)
|
||||
|
||||
let shapeLayer = self.layer as! CAShapeLayer
|
||||
|
||||
if self.currentComponent?.backgroundColor != component.backgroundColor {
|
||||
shapeLayer.strokeColor = component.backgroundColor.cgColor
|
||||
shapeLayer.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
if self.currentComponent?.foregroundColor != component.foregroundColor {
|
||||
self.foregroundShapeLayer.strokeColor = component.foregroundColor.cgColor
|
||||
self.foregroundShapeLayer.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
if self.currentComponent?.diameter != component.diameter {
|
||||
let path = UIBezierPath(arcCenter: CGPoint(x: component.diameter / 2.0, y: component.diameter / 2.0), radius: component.diameter / 2.0, startAngle: -CGFloat.pi / 2.0, endAngle: 2.0 * CGFloat.pi - CGFloat.pi / 2.0, clockwise: true).cgPath
|
||||
shapeLayer.path = path
|
||||
self.foregroundShapeLayer.path = path
|
||||
|
||||
self.foregroundShapeLayer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
|
||||
if self.currentComponent != nil {
|
||||
let previousValue: CGFloat = self.foregroundShapeLayer.presentation()?.strokeEnd ?? self.foregroundShapeLayer.strokeEnd
|
||||
self.foregroundShapeLayer.animate(from: CGFloat(previousValue) as NSNumber, to: CGFloat(component.value) as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.12)
|
||||
}
|
||||
self.foregroundShapeLayer.strokeEnd = CGFloat(component.value)
|
||||
|
||||
self.currentComponent = component
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
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, transition: transition)
|
||||
}
|
||||
}
|
@ -7,13 +7,6 @@ private struct Vector2 {
|
||||
var y: Float
|
||||
}
|
||||
|
||||
private final class NullActionClass: NSObject, CAAction {
|
||||
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
|
||||
}
|
||||
}
|
||||
|
||||
private let nullAction = NullActionClass()
|
||||
|
||||
private final class ParticleLayer: CALayer {
|
||||
let mass: Float
|
||||
var velocity: Vector2
|
||||
|
@ -270,7 +270,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
let fileResource = LocalFileMediaResource(fileId: id, size: gzippedData.count, isSecretRelated: false)
|
||||
context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData)
|
||||
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: gzippedData.count, attributes: [.FileName(fileName: "Log-iOS-Full.txt.gz")])
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: gzippedData.count, attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")])
|
||||
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)
|
||||
|
||||
let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start()
|
||||
@ -436,7 +436,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
let fileResource = LocalFileMediaResource(fileId: id, size: gzippedData.count, isSecretRelated: false)
|
||||
context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData)
|
||||
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: gzippedData.count, attributes: [.FileName(fileName: "Log-iOS-Full.txt.gz")])
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: gzippedData.count, attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")])
|
||||
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)
|
||||
|
||||
let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start()
|
||||
@ -520,7 +520,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
let fileResource = LocalFileMediaResource(fileId: id, size: gzippedData.count, isSecretRelated: false)
|
||||
context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData)
|
||||
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: gzippedData.count, attributes: [.FileName(fileName: "Log-iOS-Full.txt.gz")])
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: gzippedData.count, attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")])
|
||||
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)
|
||||
|
||||
let _ = enqueueMessages(account: context.account, peerId: peerId, messages: [message]).start()
|
||||
|
@ -1,11 +1,11 @@
|
||||
import UIKit
|
||||
|
||||
private final class NullActionClass: NSObject, CAAction {
|
||||
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
|
||||
public final class NullActionClass: NSObject, CAAction {
|
||||
@objc public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
|
||||
}
|
||||
}
|
||||
|
||||
private let nullAction = NullActionClass()
|
||||
public let nullAction = NullActionClass()
|
||||
|
||||
open class SimpleLayer: CALayer {
|
||||
override open func action(forKey event: String) -> CAAction? {
|
||||
@ -24,3 +24,21 @@ open class SimpleLayer: CALayer {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
open class SimpleShapeLayer: CAShapeLayer {
|
||||
override open func action(forKey event: String) -> CAAction? {
|
||||
return nullAction
|
||||
}
|
||||
|
||||
override public init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
@ -277,13 +277,6 @@ open class TransformImageNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private final class NullActionClass: NSObject, CAAction {
|
||||
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
|
||||
}
|
||||
}
|
||||
|
||||
private let nullAction = NullActionClass()
|
||||
|
||||
private class CaptureProtectedContentLayer: AVSampleBufferDisplayLayer {
|
||||
override func action(forKey event: String) -> CAAction? {
|
||||
return nullAction
|
||||
|
@ -44,6 +44,7 @@ private final class FetchManagerLocationEntry {
|
||||
let ranges = Bag<IndexSet>()
|
||||
var elevatedPriorityReferenceCount: Int32 = 0
|
||||
var userInitiatedPriorityIndices: [Int32] = []
|
||||
var isPaused: Bool = false
|
||||
|
||||
var combinedRanges: IndexSet {
|
||||
var result = IndexSet()
|
||||
@ -90,6 +91,7 @@ private final class FetchManagerStatusContext {
|
||||
var subscribers = Bag<(MediaResourceStatus) -> Void>()
|
||||
|
||||
var hasEntry = false
|
||||
var isPaused = false
|
||||
|
||||
var isEmpty: Bool {
|
||||
return !self.hasEntry && self.subscribers.isEmpty
|
||||
@ -97,14 +99,18 @@ private final class FetchManagerStatusContext {
|
||||
|
||||
var combinedStatus: MediaResourceStatus? {
|
||||
if let originalStatus = self.originalStatus {
|
||||
if originalStatus == .Remote && self.hasEntry {
|
||||
return .Fetching(isActive: false, progress: 0.0)
|
||||
if case let .Remote(progress) = originalStatus, self.hasEntry {
|
||||
if self.isPaused {
|
||||
return .Paused(progress: progress)
|
||||
} else {
|
||||
return .Fetching(isActive: false, progress: progress)
|
||||
}
|
||||
} else if self.hasEntry {
|
||||
return originalStatus
|
||||
} else if case .Local = originalStatus {
|
||||
return originalStatus
|
||||
} else {
|
||||
return .Remote
|
||||
return .Remote(progress: 0.0)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
@ -116,7 +122,7 @@ private final class FetchManagerCategoryContext {
|
||||
private let postbox: Postbox
|
||||
private let storeManager: DownloadedMediaStoreManager?
|
||||
private let entryCompleted: (FetchManagerLocationEntryId) -> Void
|
||||
private let activeEntriesUpdated: ([FetchManagerEntrySummary]) -> Void
|
||||
private let activeEntriesUpdated: () -> Void
|
||||
|
||||
private var topEntryIdAndPriority: (FetchManagerLocationEntryId, FetchManagerPriorityKey)?
|
||||
private var entries: [FetchManagerLocationEntryId: FetchManagerLocationEntry] = [:]
|
||||
@ -133,13 +139,17 @@ private final class FetchManagerCategoryContext {
|
||||
return false
|
||||
}
|
||||
|
||||
init(postbox: Postbox, storeManager: DownloadedMediaStoreManager?, entryCompleted: @escaping (FetchManagerLocationEntryId) -> Void, activeEntriesUpdated: @escaping ([FetchManagerEntrySummary]) -> Void) {
|
||||
init(postbox: Postbox, storeManager: DownloadedMediaStoreManager?, entryCompleted: @escaping (FetchManagerLocationEntryId) -> Void, activeEntriesUpdated: @escaping () -> Void) {
|
||||
self.postbox = postbox
|
||||
self.storeManager = storeManager
|
||||
self.entryCompleted = entryCompleted
|
||||
self.activeEntriesUpdated = activeEntriesUpdated
|
||||
}
|
||||
|
||||
func getActiveEntries() -> [FetchManagerLocationEntry] {
|
||||
return Array(self.entries.values)
|
||||
}
|
||||
|
||||
func withEntry(id: FetchManagerLocationEntryId, takeNew: (() -> (AnyMediaReference?, MediaResourceReference, MediaResourceStatsCategory, Int32))?, _ f: (FetchManagerLocationEntry) -> Void) {
|
||||
let entry: FetchManagerLocationEntry
|
||||
let previousPriorityKey: FetchManagerPriorityKey?
|
||||
@ -221,8 +231,13 @@ private final class FetchManagerCategoryContext {
|
||||
parsedRanges = resultRanges
|
||||
}
|
||||
activeContext.disposable?.dispose()
|
||||
activeContext.disposable = (fetchedMediaResource(mediaBox: self.postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
|
||||
let postbox = self.postbox
|
||||
activeContext.disposable = (fetchedMediaResource(mediaBox: postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
|
||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
if filterDownloadStatsEntry(entry: entry), case let .message(message, _) = entry.mediaReference, let messageId = message.id, case .remote = type {
|
||||
let _ = addRecentDownloadItem(postbox: postbox, item: RecentDownloadItem(messageId: messageId, resourceId: entry.resourceReference.resource.id.stringRepresentation, timestamp: Int32(Date().timeIntervalSince1970), isSeen: false)).start()
|
||||
}
|
||||
|
||||
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
|
||||
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|
||||
|> castError(FetchResourceError.self)
|
||||
@ -241,58 +256,32 @@ private final class FetchManagerCategoryContext {
|
||||
}
|
||||
}
|
||||
|
||||
if (previousPriorityKey != nil) != (updatedPriorityKey != nil) {
|
||||
if let statusContext = self.statusContexts[id] {
|
||||
let previousStatus = statusContext.combinedStatus
|
||||
statusContext.hasEntry = self.entries[id] != nil
|
||||
if let combinedStatus = statusContext.combinedStatus, combinedStatus != previousStatus {
|
||||
for f in statusContext.subscribers.copyItems() {
|
||||
f(combinedStatus)
|
||||
}
|
||||
if let statusContext = self.statusContexts[id] {
|
||||
let previousStatus = statusContext.combinedStatus
|
||||
if let entry = self.entries[id] {
|
||||
statusContext.hasEntry = true
|
||||
statusContext.isPaused = entry.isPaused
|
||||
} else {
|
||||
statusContext.hasEntry = false
|
||||
statusContext.isPaused = false
|
||||
}
|
||||
if let combinedStatus = statusContext.combinedStatus, combinedStatus != previousStatus {
|
||||
for f in statusContext.subscribers.copyItems() {
|
||||
f(combinedStatus)
|
||||
}
|
||||
/*var hasForegroundPriorityKey = false
|
||||
if let updatedPriorityKey = updatedPriorityKey, let topReference = updatedPriorityKey.topReference {
|
||||
switch topReference {
|
||||
case .userInitiated:
|
||||
hasForegroundPriorityKey = true
|
||||
default:
|
||||
hasForegroundPriorityKey = false
|
||||
}
|
||||
}
|
||||
|
||||
if hasForegroundPriorityKey {
|
||||
if !statusContext.hasEntry {
|
||||
let previousStatus = statusContext.combinedStatus
|
||||
statusContext.hasEntry = true
|
||||
if let combinedStatus = statusContext.combinedStatus, combinedStatus != previousStatus {
|
||||
for f in statusContext.subscribers.copyItems() {
|
||||
f(combinedStatus)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
} else {
|
||||
if statusContext.hasEntry {
|
||||
let previousStatus = statusContext.combinedStatus
|
||||
statusContext.hasEntry = false
|
||||
if let combinedStatus = statusContext.combinedStatus, combinedStatus != previousStatus {
|
||||
for f in statusContext.subscribers.copyItems() {
|
||||
f(combinedStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
self.activeEntriesUpdated(self.entries.values.compactMap(FetchManagerEntrySummary.init).sorted(by: { $0.priority < $1.priority }))
|
||||
self.activeEntriesUpdated()
|
||||
}
|
||||
|
||||
func maybeFindAndActivateNewTopEntry() -> Bool {
|
||||
if self.topEntryIdAndPriority == nil && !self.entries.isEmpty {
|
||||
var topEntryIdAndPriority: (FetchManagerLocationEntryId, FetchManagerPriorityKey)?
|
||||
for (id, entry) in self.entries {
|
||||
if entry.isPaused {
|
||||
continue
|
||||
}
|
||||
if let entryPriorityKey = entry.priorityKey {
|
||||
if let (_, topKey) = topEntryIdAndPriority {
|
||||
if entryPriorityKey < topKey {
|
||||
@ -366,8 +355,13 @@ private final class FetchManagerCategoryContext {
|
||||
})
|
||||
} else if ranges.isEmpty {
|
||||
} else {
|
||||
activeContext.disposable = (fetchedMediaResource(mediaBox: self.postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
|
||||
let postbox = self.postbox
|
||||
activeContext.disposable = (fetchedMediaResource(mediaBox: postbox.mediaBox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated)
|
||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
if filterDownloadStatsEntry(entry: entry), case let .message(message, _) = entry.mediaReference, let messageId = message.id, case .remote = type {
|
||||
let _ = addRecentDownloadItem(postbox: postbox, item: RecentDownloadItem(messageId: messageId, resourceId: entry.resourceReference.resource.id.stringRepresentation, timestamp: Int32(Date().timeIntervalSince1970), isSeen: false)).start()
|
||||
}
|
||||
|
||||
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
|
||||
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|
||||
|> castError(FetchResourceError.self)
|
||||
@ -394,6 +388,15 @@ private final class FetchManagerCategoryContext {
|
||||
}
|
||||
}
|
||||
|
||||
func getEntryId(resourceId: String) -> FetchManagerLocationEntryId? {
|
||||
for (id, _) in self.entries {
|
||||
if id.resourceId.stringRepresentation == resourceId {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelEntry(_ entryId: FetchManagerLocationEntryId, isCompleted: Bool) {
|
||||
var id: FetchManagerLocationEntryId = entryId
|
||||
if self.entries[id] == nil {
|
||||
@ -419,6 +422,7 @@ private final class FetchManagerCategoryContext {
|
||||
}
|
||||
let previousStatus = statusContext.combinedStatus
|
||||
statusContext.hasEntry = false
|
||||
statusContext.isPaused = false
|
||||
if let combinedStatus = statusContext.combinedStatus, combinedStatus != previousStatus {
|
||||
for f in statusContext.subscribers.copyItems() {
|
||||
f(combinedStatus)
|
||||
@ -446,7 +450,88 @@ private final class FetchManagerCategoryContext {
|
||||
activeContextsUpdated = true
|
||||
}
|
||||
|
||||
self.activeEntriesUpdated(self.entries.values.compactMap(FetchManagerEntrySummary.init).sorted(by: { $0.priority < $1.priority }))
|
||||
self.activeEntriesUpdated()
|
||||
}
|
||||
|
||||
func toggleEntryPaused(_ entryId: FetchManagerLocationEntryId, isPaused: Bool) -> Bool {
|
||||
var id: FetchManagerLocationEntryId = entryId
|
||||
if self.entries[id] == nil {
|
||||
for (key, _) in self.entries {
|
||||
if key.resourceId == entryId.resourceId {
|
||||
id = key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let entry = self.entries[id] {
|
||||
if entry.isPaused == isPaused {
|
||||
return entry.isPaused
|
||||
}
|
||||
entry.isPaused = !entry.isPaused
|
||||
|
||||
if entry.isPaused {
|
||||
if self.topEntryIdAndPriority?.0 == id {
|
||||
self.topEntryIdAndPriority = nil
|
||||
}
|
||||
|
||||
if let activeContext = self.activeContexts[id] {
|
||||
activeContext.disposable?.dispose()
|
||||
activeContext.disposable = nil
|
||||
self.activeContexts.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
if let statusContext = self.statusContexts[id] {
|
||||
let previousStatus = statusContext.combinedStatus
|
||||
statusContext.hasEntry = true
|
||||
statusContext.isPaused = entry.isPaused
|
||||
if let combinedStatus = statusContext.combinedStatus, combinedStatus != previousStatus {
|
||||
for f in statusContext.subscribers.copyItems() {
|
||||
f(combinedStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.maybeFindAndActivateNewTopEntry()
|
||||
self.activeEntriesUpdated()
|
||||
|
||||
return entry.isPaused
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func raiseEntryPriority(_ entryId: FetchManagerLocationEntryId) {
|
||||
var id: FetchManagerLocationEntryId = entryId
|
||||
if self.entries[id] == nil {
|
||||
for (key, _) in self.entries {
|
||||
if key.resourceId == entryId.resourceId {
|
||||
id = key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.entries[id] == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var topUserInitiatedPriority: Int32 = 0
|
||||
for (_, entry) in self.entries {
|
||||
for index in entry.userInitiatedPriorityIndices {
|
||||
topUserInitiatedPriority = min(topUserInitiatedPriority, index)
|
||||
}
|
||||
}
|
||||
|
||||
self.withEntry(id: id, takeNew: nil, { entry in
|
||||
if entry.userInitiatedPriorityIndices.last == topUserInitiatedPriority {
|
||||
return
|
||||
}
|
||||
|
||||
entry.userInitiatedPriorityIndices.removeAll()
|
||||
entry.userInitiatedPriorityIndices.append(topUserInitiatedPriority - 1)
|
||||
})
|
||||
}
|
||||
|
||||
func withFetchStatusContext(_ id: FetchManagerLocationEntryId, _ f: (FetchManagerStatusContext) -> Void) {
|
||||
@ -456,8 +541,9 @@ private final class FetchManagerCategoryContext {
|
||||
} else {
|
||||
statusContext = FetchManagerStatusContext()
|
||||
self.statusContexts[id] = statusContext
|
||||
if self.entries[id] != nil {
|
||||
if let entry = self.entries[id] {
|
||||
statusContext.hasEntry = true
|
||||
statusContext.isPaused = entry.isPaused
|
||||
}
|
||||
}
|
||||
|
||||
@ -479,6 +565,7 @@ public struct FetchManagerEntrySummary: Equatable {
|
||||
public let mediaReference: AnyMediaReference?
|
||||
public let resourceReference: MediaResourceReference
|
||||
public let priority: FetchManagerPriorityKey
|
||||
public let isPaused: Bool
|
||||
}
|
||||
|
||||
private extension FetchManagerEntrySummary {
|
||||
@ -490,6 +577,38 @@ private extension FetchManagerEntrySummary {
|
||||
self.mediaReference = entry.mediaReference
|
||||
self.resourceReference = entry.resourceReference
|
||||
self.priority = priority
|
||||
self.isPaused = entry.isPaused
|
||||
}
|
||||
}
|
||||
|
||||
private func filterDownloadStatsEntry(entry: FetchManagerLocationEntry) -> Bool {
|
||||
guard let mediaReference = entry.mediaReference else {
|
||||
return false
|
||||
}
|
||||
if !entry.userInitiated {
|
||||
return false
|
||||
}
|
||||
switch mediaReference {
|
||||
case let .message(_, media):
|
||||
switch media {
|
||||
case let file as TelegramMediaFile:
|
||||
if file.isVideo {
|
||||
if file.isAnimated {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if file.isSticker {
|
||||
return false
|
||||
}
|
||||
if file.isVoice {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -545,21 +664,28 @@ public final class FetchManagerImpl: FetchManager {
|
||||
context.cancelEntry(id, isCompleted: true)
|
||||
})
|
||||
}
|
||||
}, activeEntriesUpdated: { [weak self] entries in
|
||||
}, activeEntriesUpdated: { [weak self] in
|
||||
queue.async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var hasActiveUserInitiatedEntries = false
|
||||
var activeEntries: [FetchManagerEntrySummary] = []
|
||||
for (_, context) in strongSelf.categoryContexts {
|
||||
if context.hasActiveUserInitiatedEntries {
|
||||
hasActiveUserInitiatedEntries = true
|
||||
break
|
||||
}
|
||||
activeEntries.append(contentsOf: context.getActiveEntries().filter { entry in
|
||||
return filterDownloadStatsEntry(entry: entry)
|
||||
}.compactMap { entry -> FetchManagerEntrySummary? in
|
||||
return FetchManagerEntrySummary(entry: entry)
|
||||
})
|
||||
}
|
||||
strongSelf.hasUserInitiatedEntriesValue.set(hasActiveUserInitiatedEntries)
|
||||
|
||||
strongSelf.entriesSummaryValue.set(entries)
|
||||
let sortedEntries = activeEntries.sorted(by: { $0.priority < $1.priority })
|
||||
|
||||
strongSelf.entriesSummaryValue.set(sortedEntries)
|
||||
}
|
||||
})
|
||||
self.categoryContexts[key] = context
|
||||
@ -578,6 +704,7 @@ public final class FetchManagerImpl: FetchManager {
|
||||
if let strongSelf = self {
|
||||
var assignedEpisode: Int32?
|
||||
var assignedUserInitiatedIndex: Int32?
|
||||
let _ = assignedUserInitiatedIndex
|
||||
|
||||
var assignedReferenceIndex: Int?
|
||||
var assignedRangeIndex: Int?
|
||||
@ -588,6 +715,8 @@ public final class FetchManagerImpl: FetchManager {
|
||||
if userInitiated {
|
||||
entry.userInitiated = true
|
||||
}
|
||||
let wasPaused = entry.isPaused
|
||||
entry.isPaused = false
|
||||
if let peerType = storeToDownloadsPeerType {
|
||||
entry.storeToDownloadsPeerType = peerType
|
||||
}
|
||||
@ -596,9 +725,10 @@ public final class FetchManagerImpl: FetchManager {
|
||||
entry.elevatedPriorityReferenceCount += 1
|
||||
}
|
||||
assignedRangeIndex = entry.ranges.add(ranges)
|
||||
if userInitiated {
|
||||
if userInitiated && !wasPaused {
|
||||
let userInitiatedIndex = strongSelf.takeNextUserInitiatedIndex()
|
||||
assignedUserInitiatedIndex = userInitiatedIndex
|
||||
entry.userInitiatedPriorityIndices.removeAll()
|
||||
entry.userInitiatedPriorityIndices.append(userInitiatedIndex)
|
||||
entry.userInitiatedPriorityIndices.sort()
|
||||
}
|
||||
@ -628,13 +758,13 @@ public final class FetchManagerImpl: FetchManager {
|
||||
entry.elevatedPriorityReferenceCount -= 1
|
||||
assert(entry.elevatedPriorityReferenceCount >= 0)
|
||||
}
|
||||
if let userInitiatedIndex = assignedUserInitiatedIndex {
|
||||
/*if let userInitiatedIndex = assignedUserInitiatedIndex {
|
||||
if let index = entry.userInitiatedPriorityIndices.firstIndex(of: userInitiatedIndex) {
|
||||
entry.userInitiatedPriorityIndices.remove(at: index)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -657,6 +787,48 @@ public final class FetchManagerImpl: FetchManager {
|
||||
}
|
||||
}
|
||||
|
||||
public func cancelInteractiveFetches(resourceId: String) {
|
||||
self.queue.async {
|
||||
var removeCategories: [FetchManagerCategory] = []
|
||||
for (category, categoryContext) in self.categoryContexts {
|
||||
if let id = categoryContext.getEntryId(resourceId: resourceId) {
|
||||
categoryContext.cancelEntry(id, isCompleted: false)
|
||||
if categoryContext.isEmpty {
|
||||
removeCategories.append(category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for category in removeCategories {
|
||||
self.categoryContexts.removeValue(forKey: category)
|
||||
}
|
||||
|
||||
self.postbox.mediaBox.cancelInteractiveResourceFetch(resourceId: MediaResourceId(resourceId))
|
||||
}
|
||||
}
|
||||
|
||||
public func toggleInteractiveFetchPaused(resourceId: String, isPaused: Bool) {
|
||||
self.queue.async {
|
||||
for (_, categoryContext) in self.categoryContexts {
|
||||
if let id = categoryContext.getEntryId(resourceId: resourceId) {
|
||||
if categoryContext.toggleEntryPaused(id, isPaused: isPaused) {
|
||||
self.postbox.mediaBox.cancelInteractiveResourceFetch(resourceId: MediaResourceId(resourceId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func raisePriority(resourceId: String) {
|
||||
self.queue.async {
|
||||
for (_, categoryContext) in self.categoryContexts {
|
||||
if let id = categoryContext.getEntryId(resourceId: resourceId) {
|
||||
categoryContext.raiseEntryPriority(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchStatus(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, resource: MediaResource) -> Signal<MediaResourceStatus, NoError> {
|
||||
let queue = self.queue
|
||||
return Signal { [weak self] subscriber in
|
||||
|
@ -209,7 +209,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
|
||||
statusState = .cloudProgress(color: UIColor.white, strokeBackgroundColor: UIColor.white.withAlphaComponent(0.5), lineWidth: 2.0, value: CGFloat(adjustedProgress))
|
||||
case .Local:
|
||||
break
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if let image = cloudFetchIcon {
|
||||
statusState = .customIcon(image)
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
let previousStatus = strongSelf.status
|
||||
strongSelf.status = status
|
||||
switch status {
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
strongSelf.statusNode.isHidden = false
|
||||
strongSelf.statusNode.alpha = 1.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
|
||||
|
@ -192,7 +192,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD
|
||||
let previousStatus = strongSelf.status
|
||||
strongSelf.status = status
|
||||
switch status {
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
strongSelf.statusNode.isHidden = false
|
||||
strongSelf.statusNode.alpha = 1.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
|
||||
|
@ -190,7 +190,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode {
|
||||
let previousStatus = strongSelf.status
|
||||
strongSelf.status = status
|
||||
switch status {
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
strongSelf.statusNode.isHidden = false
|
||||
strongSelf.statusNode.alpha = 1.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
|
||||
|
@ -529,7 +529,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
let previousStatus = strongSelf.status
|
||||
strongSelf.status = status
|
||||
switch status {
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
strongSelf.statusNode.isHidden = false
|
||||
strongSelf.statusNode.alpha = 1.0
|
||||
strongSelf.statusNodeContainer.isUserInteractionEnabled = true
|
||||
|
@ -866,7 +866,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
switch fetchStatus {
|
||||
case .Fetching:
|
||||
fetchControls.cancel()
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
fetchControls.fetch()
|
||||
case .Local:
|
||||
break
|
||||
@ -2044,7 +2044,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
switch fetchStatus {
|
||||
case .Local:
|
||||
videoNode.playOnceWithSound(playAndRecord: false, seek: .none, actionAtEnd: self.actionAtEnd)
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if self.requiresDownload {
|
||||
self.fetchControls?.fetch()
|
||||
} else {
|
||||
|
@ -60,13 +60,6 @@ public final class GridMessageSelectionNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private final class NullActionClass: NSObject, CAAction {
|
||||
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
|
||||
}
|
||||
}
|
||||
|
||||
private let nullAction = NullActionClass()
|
||||
|
||||
public final class GridMessageSelectionLayer: CALayer {
|
||||
private var selected = false
|
||||
private let checkLayer: CheckLayer
|
||||
|
@ -45,7 +45,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
|> map { result, presentationData in
|
||||
let result = result.0
|
||||
let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), chatListPresentationData, result.totalCount, nil, false) })
|
||||
return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap(EnginePeerReadCounters.init), chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false) })
|
||||
}
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {
|
||||
}, peerSelected: { _, _, _ in
|
||||
@ -95,10 +95,10 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
})
|
||||
|
||||
let firstTime = previousEntries == nil
|
||||
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: {
|
||||
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], key: .chats, tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: {
|
||||
}, toggleExpandGlobalResults: {
|
||||
}, searchPeer: { _ in
|
||||
}, searchQuery: "", searchOptions: nil, messageContextAction: nil)
|
||||
}, searchQuery: "", searchOptions: nil, messageContextAction: nil, openStorageSettings: {}, toggleAllPaused: {})
|
||||
strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime)
|
||||
}
|
||||
})
|
||||
|
@ -299,7 +299,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
default:
|
||||
break
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if case .tap = gesture {
|
||||
self.fetchControls?.fetch(true)
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
|
||||
switch fetchStatus {
|
||||
case .Local:
|
||||
self.openMedia(self.media)
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
self.fetchControls?.fetch(true)
|
||||
case .Fetching:
|
||||
self.fetchControls?.cancel()
|
||||
|
@ -360,7 +360,6 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
|
||||
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
var rect = rect
|
||||
rect.origin.y += self.insets.top
|
||||
@ -446,7 +445,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
var descriptionString: String
|
||||
if let performer = performer {
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
descriptionString = performer
|
||||
} else {
|
||||
descriptionString = "\(stringForDuration(Int32(duration))) • \(performer)"
|
||||
@ -457,7 +456,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
descriptionString = ""
|
||||
}
|
||||
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
||||
if descriptionString.isEmpty {
|
||||
descriptionString = authorString
|
||||
@ -501,7 +500,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
authorName = " "
|
||||
}
|
||||
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
authorName = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
||||
}
|
||||
|
||||
@ -509,13 +508,13 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
let dateString = stringForFullDate(timestamp: message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
var descriptionString: String = ""
|
||||
if let duration = file.duration {
|
||||
if item.isGlobalSearchResult || !item.displayFileInfo {
|
||||
if item.isGlobalSearchResult || item.isDownloadList || !item.displayFileInfo {
|
||||
descriptionString = stringForDuration(Int32(duration))
|
||||
} else {
|
||||
descriptionString = "\(stringForDuration(Int32(duration))) • \(dateString)"
|
||||
}
|
||||
} else {
|
||||
if !item.isGlobalSearchResult {
|
||||
if !(item.isGlobalSearchResult || item.isDownloadList) {
|
||||
descriptionString = dateString
|
||||
}
|
||||
}
|
||||
@ -523,7 +522,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
iconImage = .roundVideo(file)
|
||||
} else if !isAudio {
|
||||
let fileName: String = file.fileName ?? ""
|
||||
let fileName: String = file.fileName ?? "File"
|
||||
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
||||
|
||||
var fileExtension: String?
|
||||
@ -543,18 +542,18 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
var descriptionString: String = ""
|
||||
if let size = file.size {
|
||||
if item.isGlobalSearchResult || !item.displayFileInfo {
|
||||
if item.isGlobalSearchResult || item.isDownloadList || !item.displayFileInfo {
|
||||
descriptionString = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))
|
||||
} else {
|
||||
descriptionString = "\(dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))) • \(dateString)"
|
||||
}
|
||||
} else {
|
||||
if !item.isGlobalSearchResult {
|
||||
if !(item.isGlobalSearchResult || item.isDownloadList) {
|
||||
descriptionString = "\(dateString)"
|
||||
}
|
||||
}
|
||||
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
||||
if descriptionString.isEmpty {
|
||||
descriptionString = authorString
|
||||
@ -581,11 +580,11 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
let dateString = stringForFullDate(timestamp: message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
|
||||
var descriptionString: String = ""
|
||||
if !item.isGlobalSearchResult {
|
||||
if !(item.isGlobalSearchResult || item.isDownloadList) {
|
||||
descriptionString = "\(dateString)"
|
||||
}
|
||||
|
||||
if item.isGlobalSearchResult {
|
||||
if item.isGlobalSearchResult || item.isDownloadList {
|
||||
let authorString = stringForFullAuthorName(message: EngineMessage(message), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, accountPeerId: item.context.account.peerId)
|
||||
if descriptionString.isEmpty {
|
||||
descriptionString = authorString
|
||||
@ -597,7 +596,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
|
||||
isRestricted = true
|
||||
@ -638,16 +637,24 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
}, cancel: {
|
||||
if let file = selectedMedia as? TelegramMediaFile {
|
||||
messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file)
|
||||
if item.isDownloadList {
|
||||
context.fetchManager.toggleInteractiveFetchPaused(resourceId: file.resource.id.stringRepresentation, isPaused: true)
|
||||
} else {
|
||||
messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file)
|
||||
}
|
||||
} else if let image = selectedMedia as? TelegramMediaImage, let representation = image.representations.last {
|
||||
messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: image, resource: representation.resource)
|
||||
if item.isDownloadList {
|
||||
context.fetchManager.toggleInteractiveFetchPaused(resourceId: representation.resource.id.stringRepresentation, isPaused: true)
|
||||
} else {
|
||||
messageMediaImageCancelInteractiveFetch(context: context, messageId: message.id, image: image, resource: representation.resource)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if statusUpdated && item.displayFileInfo {
|
||||
if let file = selectedMedia as? TelegramMediaFile {
|
||||
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult)
|
||||
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|
||||
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
|
||||
if case .Fetching = value.fetchStatus {
|
||||
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
||||
@ -670,10 +677,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
if isVoice {
|
||||
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: message, isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult)
|
||||
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: message, isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|
||||
}
|
||||
} else if let image = selectedMedia as? TelegramMediaImage {
|
||||
updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult)
|
||||
updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|
||||
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
|
||||
if case .Fetching = value.fetchStatus {
|
||||
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
||||
@ -929,7 +936,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
break
|
||||
case let .fetchStatus(fetchStatus):
|
||||
switch fetchStatus {
|
||||
case .Remote, .Fetching:
|
||||
case .Remote, .Fetching, .Paused:
|
||||
descriptionOffset = 14.0
|
||||
case .Local:
|
||||
break
|
||||
@ -1100,7 +1107,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
if isAudio || isInstantVideo {
|
||||
iconStatusState = .play
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if isAudio || isInstantVideo {
|
||||
iconStatusState = .play
|
||||
}
|
||||
@ -1151,7 +1158,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
|
||||
override public func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
if let item = self.item, item.message?.id == id, self.iconImageNode.supernode != nil {
|
||||
if let item = self.item, let message = item.message, message.id == id, self.iconImageNode.supernode != nil {
|
||||
let iconImageNode = self.iconImageNode
|
||||
return (self.iconImageNode, self.iconImageNode.bounds, { [weak iconImageNode] in
|
||||
return (iconImageNode?.view.snapshotContentTree(unhide: true), nil)
|
||||
@ -1190,7 +1197,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
case let .fetchStatus(fetchStatus):
|
||||
maybeFetchStatus = fetchStatus
|
||||
switch fetchStatus {
|
||||
case let .Fetching(_, progress):
|
||||
case .Fetching(_, let progress), .Paused(let progress):
|
||||
if let file = self.currentMedia as? TelegramMediaFile, let size = file.size {
|
||||
downloadingString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData))) / \(dataSizeString(size, forceDecimal: true, formatting: DataSizeStringFormatting(chatPresentationData: item.presentationData)))"
|
||||
}
|
||||
@ -1203,8 +1210,8 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
|
||||
switch maybeFetchStatus {
|
||||
case let .Fetching(_, progress):
|
||||
let progressFrame = CGRect(x: self.currentLeftOffset + leftInset + 65.0, y: size.height - 2.0, width: floor((size.width - 65.0 - leftInset - rightInset)), height: 3.0)
|
||||
case .Fetching(_, let progress), .Paused(let progress):
|
||||
let progressFrame = CGRect(x: self.currentLeftOffset + leftInset + 65.0, y: size.height - 3.0, width: floor((size.width - 65.0 - leftInset - rightInset)), height: 3.0)
|
||||
let linearProgressNode: LinearProgressNode
|
||||
if let current = self.linearProgressNode {
|
||||
linearProgressNode = current
|
||||
@ -1223,7 +1230,11 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
animated = false
|
||||
self.offsetContainerNode.addSubnode(downloadStatusIconNode)
|
||||
}
|
||||
downloadStatusIconNode.enqueueState(.pause, animated: animated)
|
||||
if case .Paused = maybeFetchStatus {
|
||||
downloadStatusIconNode.enqueueState(.download, animated: animated)
|
||||
} else {
|
||||
downloadStatusIconNode.enqueueState(.pause, animated: animated)
|
||||
}
|
||||
}
|
||||
case .Local:
|
||||
if let linearProgressNode = self.linearProgressNode {
|
||||
@ -1304,12 +1315,12 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
|
||||
cancel()
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
|
||||
fetch()
|
||||
}
|
||||
case .Local:
|
||||
if let item = self.item, let interaction = self.interaction, let message = item.message {
|
||||
if let item = self.item, let message = item.message, let interaction = self.interaction {
|
||||
let _ = interaction.openMessage(message, .default)
|
||||
}
|
||||
}
|
||||
@ -1344,7 +1355,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
|
||||
cancel()
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
|
||||
fetch()
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ public final class ListMessageItem: ListViewItem {
|
||||
public let selection: ChatHistoryMessageSelection
|
||||
let hintIsLink: Bool
|
||||
let isGlobalSearchResult: Bool
|
||||
let isDownloadList: Bool
|
||||
let displayFileInfo: Bool
|
||||
let displayBackground: Bool
|
||||
let style: ItemListStyle
|
||||
@ -60,7 +61,7 @@ public final class ListMessageItem: ListViewItem {
|
||||
|
||||
public let selectable: Bool = true
|
||||
|
||||
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, style: ItemListStyle = .plain) {
|
||||
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, style: ItemListStyle = .plain) {
|
||||
self.presentationData = presentationData
|
||||
self.context = context
|
||||
self.chatLocation = chatLocation
|
||||
@ -76,6 +77,7 @@ public final class ListMessageItem: ListViewItem {
|
||||
self.selection = selection
|
||||
self.hintIsLink = hintIsLink
|
||||
self.isGlobalSearchResult = isGlobalSearchResult
|
||||
self.isDownloadList = isDownloadList
|
||||
self.displayFileInfo = displayFileInfo
|
||||
self.displayBackground = displayBackground
|
||||
self.style = style
|
||||
|
@ -1673,10 +1673,12 @@ public func chatMessagePhotoStatus(context: AccountContext, messageId: MessageId
|
||||
switch status {
|
||||
case .Local:
|
||||
return .Local
|
||||
case .Remote:
|
||||
return .Remote
|
||||
case let .Remote(progress):
|
||||
return .Remote(progress: progress)
|
||||
case let .Fetching(isActive, progress):
|
||||
return .Fetching(isActive: isActive, progress: max(progress, 0.0))
|
||||
case let .Paused(progress):
|
||||
return .Paused(progress: progress)
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
@ -21,14 +21,6 @@ final class ContactTable: Table {
|
||||
return sharedKey
|
||||
}
|
||||
|
||||
private func lowerBound() -> ValueBoxKey {
|
||||
return self.key(PeerId(0))
|
||||
}
|
||||
|
||||
private func upperBound() -> ValueBoxKey {
|
||||
return self.key(PeerId.max)
|
||||
}
|
||||
|
||||
func isContact(peerId: PeerId) -> Bool {
|
||||
return self.get().contains(peerId)
|
||||
}
|
||||
@ -38,10 +30,10 @@ final class ContactTable: Table {
|
||||
return peerIds
|
||||
} else {
|
||||
var peerIds = Set<PeerId>()
|
||||
self.valueBox.range(self.table, start: self.lowerBound(), end: self.upperBound(), keys: { key in
|
||||
self.valueBox.scan(self.table, keys: { key in
|
||||
peerIds.insert(PeerId(key.getInt64(0)))
|
||||
return true
|
||||
}, limit: 0)
|
||||
})
|
||||
self.peerIds = peerIds
|
||||
return peerIds
|
||||
}
|
||||
|
@ -142,6 +142,11 @@ public final class MediaBox {
|
||||
private let cacheQueue = Queue()
|
||||
private let timeBasedCleanup: TimeBasedCleanup
|
||||
|
||||
private let didRemoveResourcesPipe = ValuePipe<Void>()
|
||||
public var didRemoveResources: Signal<Void, NoError> {
|
||||
return .single(Void()) |> then(self.didRemoveResourcesPipe.signal())
|
||||
}
|
||||
|
||||
private var statusContexts: [MediaResourceId: ResourceStatusContext] = [:]
|
||||
private var cachedRepresentationContexts: [CachedMediaResourceRepresentationKey: CachedMediaResourceRepresentationContext] = [:]
|
||||
|
||||
@ -310,11 +315,15 @@ public final class MediaBox {
|
||||
}
|
||||
|
||||
public func resourceStatus(_ resource: MediaResource, approximateSynchronousValue: Bool = false) -> Signal<MediaResourceStatus, NoError> {
|
||||
return self.resourceStatus(resource.id, resourceSize: resource.size, approximateSynchronousValue: approximateSynchronousValue)
|
||||
}
|
||||
|
||||
public func resourceStatus(_ resourceId: MediaResourceId, resourceSize: Int?, approximateSynchronousValue: Bool = false) -> Signal<MediaResourceStatus, NoError> {
|
||||
let signal = Signal<MediaResourceStatus, NoError> { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
self.concurrentQueue.async {
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
let paths = self.storePathsForId(resourceId)
|
||||
|
||||
if let _ = fileSize(paths.complete) {
|
||||
self.timeBasedCleanup.touch(paths: [
|
||||
@ -324,7 +333,6 @@ public final class MediaBox {
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
self.statusQueue.async {
|
||||
let resourceId = resource.id
|
||||
let statusContext: ResourceStatusContext
|
||||
var statusUpdateDisposable: MetaDisposable?
|
||||
if let current = self.statusContexts[resourceId] {
|
||||
@ -347,7 +355,7 @@ public final class MediaBox {
|
||||
if let statusUpdateDisposable = statusUpdateDisposable {
|
||||
let statusQueue = self.statusQueue
|
||||
self.dataQueue.async {
|
||||
if let (fileContext, releaseContext) = self.fileContext(for: resource.id) {
|
||||
if let (fileContext, releaseContext) = self.fileContext(for: resourceId) {
|
||||
let statusDisposable = fileContext.status(next: { [weak statusContext] value in
|
||||
statusQueue.async {
|
||||
if let current = self.statusContexts[resourceId], current === statusContext, current.status != value {
|
||||
@ -367,7 +375,7 @@ public final class MediaBox {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, size: resource.size.flatMap(Int32.init))
|
||||
}, size: resourceSize.flatMap(Int32.init))
|
||||
statusUpdateDisposable.set(ActionDisposable {
|
||||
statusDisposable.dispose()
|
||||
releaseContext()
|
||||
@ -378,10 +386,10 @@ public final class MediaBox {
|
||||
|
||||
disposable.set(ActionDisposable { [weak statusContext] in
|
||||
self.statusQueue.async {
|
||||
if let current = self.statusContexts[resource.id], current === statusContext {
|
||||
if let current = self.statusContexts[resourceId], current === statusContext {
|
||||
current.subscribers.remove(index)
|
||||
if current.subscribers.isEmpty {
|
||||
self.statusContexts.removeValue(forKey: resource.id)
|
||||
self.statusContexts.removeValue(forKey: resourceId)
|
||||
current.disposable.dispose()
|
||||
}
|
||||
}
|
||||
@ -395,13 +403,13 @@ public final class MediaBox {
|
||||
}
|
||||
if approximateSynchronousValue {
|
||||
return Signal<Signal<MediaResourceStatus, NoError>, NoError> { subscriber in
|
||||
let paths = self.storePathsForId(resource.id)
|
||||
let paths = self.storePathsForId(resourceId)
|
||||
if let _ = fileSize(paths.complete) {
|
||||
subscriber.putNext(.single(.Local))
|
||||
} else if let size = fileSize(paths.partial), size == resource.size {
|
||||
} else if let size = fileSize(paths.partial), size == resourceSize {
|
||||
subscriber.putNext(.single(.Local))
|
||||
} else {
|
||||
subscriber.putNext(.single(.Remote) |> then(signal))
|
||||
subscriber.putNext(.single(.Remote(progress: 0.0)) |> then(signal))
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
@ -775,8 +783,12 @@ public final class MediaBox {
|
||||
}
|
||||
|
||||
public func cancelInteractiveResourceFetch(_ resource: MediaResource) {
|
||||
self.cancelInteractiveResourceFetch(resourceId: resource.id)
|
||||
}
|
||||
|
||||
public func cancelInteractiveResourceFetch(resourceId: MediaResourceId) {
|
||||
self.dataQueue.async {
|
||||
if let (fileContext, releaseContext) = self.fileContext(for: resource.id) {
|
||||
if let (fileContext, releaseContext) = self.fileContext(for: resourceId) {
|
||||
fileContext.cancelFullRangeFetches()
|
||||
releaseContext()
|
||||
}
|
||||
@ -1287,7 +1299,7 @@ public final class MediaBox {
|
||||
}
|
||||
}
|
||||
|
||||
public func removeCachedResources(_ ids: Set<MediaResourceId>, force: Bool = false) -> Signal<Float, NoError> {
|
||||
public func removeCachedResources(_ ids: Set<MediaResourceId>, force: Bool = false, notify: Bool = false) -> Signal<Float, NoError> {
|
||||
return Signal { subscriber in
|
||||
self.dataQueue.async {
|
||||
let uniqueIds = Set(ids.map { $0.stringRepresentation })
|
||||
@ -1352,6 +1364,21 @@ public final class MediaBox {
|
||||
reportProgress(count)
|
||||
}
|
||||
|
||||
if notify {
|
||||
for id in ids {
|
||||
if let context = self.statusContexts[id] {
|
||||
context.status = .Remote(progress: 0.0)
|
||||
for f in context.subscribers.copyItems() {
|
||||
f(.Remote(progress: 0.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.dataQueue.justDispatch {
|
||||
self.didRemoveResourcesPipe.putNext(Void())
|
||||
}
|
||||
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return EmptyDisposable
|
||||
|
@ -605,7 +605,15 @@ final class MediaBoxPartialFile {
|
||||
if let truncationSize = self.fileMap.truncationSize, self.fileMap.sum == truncationSize {
|
||||
status = .Local
|
||||
} else {
|
||||
status = .Remote
|
||||
let progress: Float
|
||||
if let truncationSize = self.fileMap.truncationSize, truncationSize != 0 {
|
||||
progress = Float(self.fileMap.sum) / Float(truncationSize)
|
||||
} else if let size = size {
|
||||
progress = Float(self.fileMap.sum) / Float(size)
|
||||
} else {
|
||||
progress = self.fileMap.progress ?? 0.0
|
||||
}
|
||||
status = .Remote(progress: progress)
|
||||
}
|
||||
} else {
|
||||
let progress: Float
|
||||
|
@ -2,33 +2,8 @@ import Foundation
|
||||
import SwiftSignalKit
|
||||
|
||||
public enum MediaResourceStatus: Equatable {
|
||||
case Remote
|
||||
case Remote(progress: Float)
|
||||
case Local
|
||||
case Fetching(isActive: Bool, progress: Float)
|
||||
}
|
||||
|
||||
public func ==(lhs: MediaResourceStatus, rhs: MediaResourceStatus) -> Bool {
|
||||
switch lhs {
|
||||
case .Remote:
|
||||
switch rhs {
|
||||
case .Remote:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .Local:
|
||||
switch rhs {
|
||||
case .Local:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .Fetching(lhsIsActive, lhsProgress):
|
||||
switch rhs {
|
||||
case let .Fetching(rhsIsActive, rhsProgress):
|
||||
return lhsIsActive == rhsIsActive && lhsProgress.isEqual(to: rhsProgress)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
case Paused(progress: Float)
|
||||
}
|
||||
|
@ -161,16 +161,18 @@ final class OrderedItemListTable: Table {
|
||||
}
|
||||
|
||||
func addItemOrMoveToFirstPosition(collectionId: Int32, item: OrderedItemListEntry, removeTailIfCountExceeds: Int?, operations: inout [Int32: [OrderedItemListOperation]]) {
|
||||
if let index = self.getIndex(collectionId: collectionId, id: item.id), index == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if operations[collectionId] == nil {
|
||||
operations[collectionId] = [.addOrMoveToFirstPosition(item, removeTailIfCountExceeds)]
|
||||
} else {
|
||||
operations[collectionId]!.append(.addOrMoveToFirstPosition(item, removeTailIfCountExceeds))
|
||||
}
|
||||
|
||||
if let index = self.getIndex(collectionId: collectionId, id: item.id), index == 0 {
|
||||
self.indexTable.set(collectionId: collectionId, id: item.id, content: item.contents)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var orderedIds = self.getItemIds(collectionId: collectionId)
|
||||
|
||||
let offsetUntilIndex: Int
|
||||
|
@ -635,6 +635,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
targetView.imageView.alpha = 0.0
|
||||
targetView.addSubnode(itemNode)
|
||||
itemNode.frame = targetView.bounds
|
||||
|
||||
if strongSelf.hapticFeedback == nil {
|
||||
strongSelf.hapticFeedback = HapticFeedback()
|
||||
}
|
||||
strongSelf.hapticFeedback?.tap()
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -56,6 +56,8 @@ public func fetchMediaData(context: AccountContext, postbox: Postbox, mediaRefer
|
||||
subscriber.putNext(.progress(0.0))
|
||||
case let .Fetching(_, progress):
|
||||
subscriber.putNext(.progress(progress))
|
||||
case let .Paused(progress):
|
||||
subscriber.putNext(.progress(progress))
|
||||
}
|
||||
})
|
||||
let data = postbox.mediaBox.resourceData(resource, pathExtension: fileExtension, option: .complete(waitUntilFetchStatus: true)).start(next: { next in
|
||||
|
@ -16,6 +16,7 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ActivityIndicator:ActivityIndicator",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1053,9 +1053,14 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
|
||||
self.textBackgroundNode.isHidden = true
|
||||
|
||||
self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in
|
||||
self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak node] _ in
|
||||
textBackgroundCompleted = true
|
||||
intermediateCompletion()
|
||||
|
||||
if let node = node, let accessoryComponentView = node.accessoryComponentView {
|
||||
accessoryComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
accessoryComponentView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
})
|
||||
|
||||
let transitionBackgroundNode = ASDisplayNode()
|
||||
|
@ -4,6 +4,7 @@ import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import AppBundle
|
||||
import ComponentFlow
|
||||
|
||||
private let templateLoupeIcon = UIImage(bundleImageName: "Components/Search Bar/Loupe")
|
||||
|
||||
@ -35,6 +36,8 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
|
||||
|
||||
public private(set) var placeholderString: NSAttributedString?
|
||||
|
||||
var accessoryComponentView: ComponentHostView<Empty>?
|
||||
|
||||
convenience public override init() {
|
||||
self.init(fieldStyle: .legacy)
|
||||
}
|
||||
@ -105,6 +108,32 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
|
||||
})
|
||||
}
|
||||
|
||||
public func setAccessoryComponent(component: AnyComponent<Empty>?) {
|
||||
if let component = component {
|
||||
let accessoryComponentView: ComponentHostView<Empty>
|
||||
if let current = self.accessoryComponentView {
|
||||
accessoryComponentView = current
|
||||
} else {
|
||||
accessoryComponentView = ComponentHostView()
|
||||
self.accessoryComponentView = accessoryComponentView
|
||||
self.view.addSubview(accessoryComponentView)
|
||||
}
|
||||
let accessorySize = accessoryComponentView.update(
|
||||
transition: .immediate,
|
||||
component: component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
accessoryComponentView.frame = CGRect(origin: CGPoint(x: self.bounds.width - accessorySize.width - 4.0, y: floor((self.bounds.height - accessorySize.height) / 2.0)), size: accessorySize)
|
||||
} else if let accessoryComponentView = self.accessoryComponentView {
|
||||
self.accessoryComponentView = nil
|
||||
accessoryComponentView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
accessoryComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak accessoryComponentView] _ in
|
||||
accessoryComponentView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ constrainedSize: CGSize, _ expansionProgress: CGFloat, _ iconColor: UIColor, _ foregroundColor: UIColor, _ backgroundColor: UIColor, _ transition: ContainedViewLayoutTransition) -> (CGFloat, () -> Void) {
|
||||
let labelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let currentForegroundColor = self.foregroundColor
|
||||
@ -183,6 +212,10 @@ public class SearchBarPlaceholderNode: ASDisplayNode {
|
||||
transition.updateCornerRadius(node: strongSelf.backgroundNode, cornerRadius: cornerRadius)
|
||||
transition.updateAlpha(node: strongSelf.backgroundNode, alpha: outerAlpha)
|
||||
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: height)))
|
||||
|
||||
if let accessoryComponentView = strongSelf.accessoryComponentView {
|
||||
accessoryComponentView.frame = CGRect(origin: CGPoint(x: constrainedSize.width - accessoryComponentView.bounds.width - 4.0, y: floor((constrainedSize.height - accessoryComponentView.bounds.height) / 2.0)), size: accessoryComponentView.bounds.size)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,337 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import ReactionImageComponent
|
||||
import WebPBinding
|
||||
import FetchManagerImpl
|
||||
import ListMessageItem
|
||||
import ListSectionHeaderNode
|
||||
|
||||
private struct DownloadItem: Equatable {
|
||||
let resourceId: MediaResourceId
|
||||
let message: Message
|
||||
let priority: FetchManagerPriorityKey
|
||||
|
||||
static func ==(lhs: DownloadItem, rhs: DownloadItem) -> Bool {
|
||||
if lhs.resourceId != rhs.resourceId {
|
||||
return false
|
||||
}
|
||||
if lhs.message.id != rhs.message.id {
|
||||
return false
|
||||
}
|
||||
if lhs.priority != rhs.priority {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private final class DownloadsControllerArguments {
|
||||
let context: AccountContext
|
||||
|
||||
init(
|
||||
context: AccountContext
|
||||
) {
|
||||
self.context = context
|
||||
}
|
||||
}
|
||||
|
||||
private enum DownloadsControllerSection: Int32 {
|
||||
case items
|
||||
}
|
||||
|
||||
public final class DownloadsItemHeader: ListViewItemHeader {
|
||||
public let id: ListViewItemNode.HeaderId
|
||||
public let title: String
|
||||
public let stickDirection: ListViewItemHeaderStickDirection = .top
|
||||
public let stickOverInsets: Bool = true
|
||||
public let theme: PresentationTheme
|
||||
|
||||
public let height: CGFloat = 28.0
|
||||
|
||||
public init(id: ListViewItemNode.HeaderId, title: String, theme: PresentationTheme) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.theme = theme
|
||||
}
|
||||
|
||||
public func combinesWith(other: ListViewItemHeader) -> Bool {
|
||||
if let other = other as? DownloadsItemHeader, other.id == self.id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func node(synchronousLoad: Bool) -> ListViewItemHeaderNode {
|
||||
return DownloadsItemHeaderNode(title: self.title, theme: self.theme)
|
||||
}
|
||||
|
||||
public func updateNode(_ node: ListViewItemHeaderNode, previous: ListViewItemHeader?, next: ListViewItemHeader?) {
|
||||
(node as? DownloadsItemHeaderNode)?.update(title: self.title)
|
||||
}
|
||||
}
|
||||
|
||||
public final class DownloadsItemHeaderNode: ListViewItemHeaderNode {
|
||||
private var title: String
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)?
|
||||
|
||||
private let sectionHeaderNode: ListSectionHeaderNode
|
||||
|
||||
public init(title: String, theme: PresentationTheme) {
|
||||
self.title = title
|
||||
self.theme = theme
|
||||
|
||||
self.sectionHeaderNode = ListSectionHeaderNode(theme: theme)
|
||||
|
||||
super.init()
|
||||
|
||||
self.sectionHeaderNode.title = title
|
||||
self.sectionHeaderNode.action = nil
|
||||
|
||||
self.addSubnode(self.sectionHeaderNode)
|
||||
}
|
||||
|
||||
public func updateTheme(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
self.sectionHeaderNode.updateTheme(theme: theme)
|
||||
}
|
||||
|
||||
public func update(title: String) {
|
||||
self.sectionHeaderNode.title = title
|
||||
self.sectionHeaderNode.action = nil
|
||||
|
||||
if let (size, leftInset, rightInset) = self.validLayout {
|
||||
self.sectionHeaderNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)
|
||||
}
|
||||
}
|
||||
|
||||
override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
self.validLayout = (size, leftInset, rightInset)
|
||||
self.sectionHeaderNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.sectionHeaderNode.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)
|
||||
}
|
||||
|
||||
override public func animateRemoved(duration: Double) {
|
||||
self.alpha = 0.0
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: true)
|
||||
}
|
||||
}
|
||||
|
||||
private enum DownloadsControllerEntry: ItemListNodeEntry {
|
||||
enum StableId: Hashable {
|
||||
case item(MediaResourceId)
|
||||
}
|
||||
|
||||
case item(item: DownloadItem)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .item:
|
||||
return DownloadsControllerSection.items.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: StableId {
|
||||
switch self {
|
||||
case let .item(item):
|
||||
return .item(item.resourceId)
|
||||
}
|
||||
}
|
||||
|
||||
var sortId: FetchManagerPriorityKey {
|
||||
switch self {
|
||||
case let .item(item):
|
||||
return item.priority
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: DownloadsControllerEntry, rhs: DownloadsControllerEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .item(item):
|
||||
if case .item(item) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: DownloadsControllerEntry, rhs: DownloadsControllerEntry) -> Bool {
|
||||
return lhs.sortId < rhs.sortId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! DownloadsControllerArguments
|
||||
let _ = arguments
|
||||
switch self {
|
||||
case let .item(item):
|
||||
let listInteraction = ListMessageItemInteraction(openMessage: { message, mode -> Bool in
|
||||
return false
|
||||
}, openMessageContextMenu: { message, _, node, rect, gesture in
|
||||
}, toggleMessagesSelection: { messageId, selected in
|
||||
}, openUrl: { url, _, _, message in
|
||||
}, openInstantPage: { message, data in
|
||||
}, longTap: { action, message in
|
||||
}, getHiddenMedia: {
|
||||
return [:]
|
||||
})
|
||||
|
||||
let presentationData = arguments.context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), context: arguments.context, chatLocation: .peer(item.message.id.peerId), interaction: listInteraction, message: item.message, selection: .none, displayHeader: false, customHeader: nil/*DownloadsItemHeader(id: ListViewItemNode.HeaderId(space: 0, id: item.message.id.peerId), title: item.message.peers[item.message.id.peerId].flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) ?? "", theme: presentationData.theme)*/, hintIsLink: false, isGlobalSearchResult: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DownloadsControllerState: Equatable {
|
||||
var hasReaction: Bool = false
|
||||
}
|
||||
|
||||
private func downloadsControllerEntries(
|
||||
presentationData: PresentationData,
|
||||
items: [DownloadItem],
|
||||
state: DownloadsControllerState
|
||||
) -> [DownloadsControllerEntry] {
|
||||
var entries: [DownloadsControllerEntry] = []
|
||||
|
||||
var index = 0
|
||||
for item in items {
|
||||
entries.append(.item(
|
||||
item: item
|
||||
))
|
||||
index += 1
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func downloadsController(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil
|
||||
) -> ViewController {
|
||||
let statePromise = ValuePromise(DownloadsControllerState(), ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: DownloadsControllerState())
|
||||
let updateState: ((DownloadsControllerState) -> DownloadsControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
let _ = updateState
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let _ = dismissImpl
|
||||
|
||||
let actionsDisposable = DisposableSet()
|
||||
|
||||
let arguments = DownloadsControllerArguments(
|
||||
context: context
|
||||
)
|
||||
|
||||
let settings = context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings])
|
||||
|> map { preferencesView -> ReactionSettings in
|
||||
let reactionSettings: ReactionSettings
|
||||
if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) {
|
||||
reactionSettings = value
|
||||
} else {
|
||||
reactionSettings = .default
|
||||
}
|
||||
return reactionSettings
|
||||
}
|
||||
|
||||
let downloadItems: Signal<[DownloadItem], NoError> = (context.fetchManager as! FetchManagerImpl).entriesSummary
|
||||
|> mapToSignal { entries -> Signal<[DownloadItem], NoError> in
|
||||
var itemSignals: [Signal<DownloadItem?, NoError>] = []
|
||||
|
||||
for entry in entries {
|
||||
switch entry.id.locationKey {
|
||||
case let .messageId(id):
|
||||
itemSignals.append(context.account.postbox.transaction { transaction -> DownloadItem? in
|
||||
if let message = transaction.getMessage(id) {
|
||||
return DownloadItem(resourceId: entry.resourceReference.resource.id, message: message, priority: entry.priority)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return combineLatest(queue: .mainQueue(), itemSignals)
|
||||
|> map { items -> [DownloadItem] in
|
||||
return items.compactMap { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
|
||||
let signal = combineLatest(queue: .mainQueue(),
|
||||
presentationData,
|
||||
statePromise.get(),
|
||||
context.engine.stickers.availableReactions(),
|
||||
settings,
|
||||
downloadItems
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, state, availableReactions, settings, downloadItems -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
//TODO:localize
|
||||
let title: String = "Downloads"
|
||||
|
||||
let entries = downloadsControllerEntries(
|
||||
presentationData: presentationData,
|
||||
items: downloadItems,
|
||||
state: state
|
||||
)
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
title: .text(title),
|
||||
leftNavigationButton: nil,
|
||||
rightNavigationButton: nil,
|
||||
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
|
||||
animateChanges: false
|
||||
)
|
||||
let listState = ItemListNodeState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
entries: entries,
|
||||
style: .plain,
|
||||
animateChanges: true
|
||||
)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
|
||||
controller.didScrollWithOffset = { [weak controller] offset, transition, _ in
|
||||
guard let controller = controller else {
|
||||
return
|
||||
}
|
||||
controller.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ReactionChatPreviewItemNode {
|
||||
itemNode.standaloneReactionAnimation?.addRelativeContentOffset(CGPoint(x: 0.0, y: offset), transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dismissImpl = { [weak controller] in
|
||||
guard let controller = controller else {
|
||||
return
|
||||
}
|
||||
controller.dismiss()
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
|
@ -602,7 +602,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
||||
case .Local:
|
||||
state = .none
|
||||
local = true
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
state = .progress(color: statusForegroundColor, lineWidth: nil, value: 0.027, cancelEnabled: false, animateRotation: true)
|
||||
}
|
||||
strongSelf.statusNode.transitionToState(state, completion: {})
|
||||
|
@ -5,13 +5,6 @@ import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
|
||||
private final class NullActionClass: NSObject, CAAction {
|
||||
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
|
||||
}
|
||||
}
|
||||
|
||||
private let nullAction = NullActionClass()
|
||||
|
||||
public protocol SparseItemGridLayer: CALayer {
|
||||
func update(size: CGSize)
|
||||
func needsShimmer() -> Bool
|
||||
|
@ -1410,7 +1410,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
disposable.set(strongSelf.requestMediaChannelDescriptions(ssrcs: ssrcs, completion: completion))
|
||||
}
|
||||
return disposable
|
||||
}, audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: self.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash), rejoinNeeded: { [weak self] in
|
||||
}, rejoinNeeded: { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -1513,8 +1513,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
strongSelf.currentConnectionMode = .rtc
|
||||
strongSelf.genericCallContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false)
|
||||
strongSelf.genericCallContext?.setJoinResponse(payload: clientParams)
|
||||
case .broadcast:
|
||||
case let .broadcast(isExternalStream):
|
||||
strongSelf.currentConnectionMode = .broadcast
|
||||
strongSelf.genericCallContext?.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: isExternalStream))
|
||||
strongSelf.genericCallContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false)
|
||||
}
|
||||
|
||||
@ -2399,7 +2400,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
activeSpeakers: Set()
|
||||
)))
|
||||
|
||||
self.startDisposable.set((self.accountContext.engine.calls.createGroupCall(peerId: self.peerId, title: nil, scheduleDate: timestamp)
|
||||
self.startDisposable.set((self.accountContext.engine.calls.createGroupCall(peerId: self.peerId, title: nil, scheduleDate: timestamp, isExternalStream: false)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] callInfo in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -2668,7 +2669,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
|
||||
self.hasScreencast = true
|
||||
|
||||
let screencastCallContext = OngoingGroupCallContext(video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, audioStreamData: nil, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, preferX264: false)
|
||||
let screencastCallContext = OngoingGroupCallContext(video: self.screencastCapturer, requestMediaChannelDescriptions: { _, _ in EmptyDisposable }, rejoinNeeded: { }, outgoingAudioBitrateKbit: nil, videoContentType: .screencast, enableNoiseSuppression: false, preferX264: false)
|
||||
self.screencastCallContext = screencastCallContext
|
||||
|
||||
self.screencastJoinDisposable.set((screencastCallContext.joinPayload
|
||||
|
@ -142,6 +142,71 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me
|
||||
return nil
|
||||
}
|
||||
|
||||
public func findMediaResourceById(message: Message, resourceId: MediaResourceId) -> TelegramMediaResource? {
|
||||
for media in message.media {
|
||||
if let result = findMediaResourceById(media: media, resourceId: resourceId) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findMediaResourceById(media: Media, resourceId: MediaResourceId) -> TelegramMediaResource? {
|
||||
if let image = media as? TelegramMediaImage {
|
||||
for representation in image.representations {
|
||||
if representation.resource.id == resourceId {
|
||||
return representation.resource
|
||||
}
|
||||
}
|
||||
for representation in image.videoRepresentations {
|
||||
if representation.resource.id == resourceId {
|
||||
return representation.resource
|
||||
}
|
||||
}
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
if file.resource.id == resourceId {
|
||||
return file.resource
|
||||
}
|
||||
|
||||
for representation in file.previewRepresentations {
|
||||
if representation.resource.id == resourceId {
|
||||
return representation.resource
|
||||
}
|
||||
}
|
||||
} else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content {
|
||||
if let image = content.image, let result = findMediaResourceById(media: image, resourceId: resourceId) {
|
||||
return result
|
||||
}
|
||||
if let file = content.file, let result = findMediaResourceById(media: file, resourceId: resourceId) {
|
||||
return result
|
||||
}
|
||||
if let instantPage = content.instantPage {
|
||||
for pageMedia in instantPage.media.values {
|
||||
if let result = findMediaResourceById(media: pageMedia, resourceId: resourceId) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let game = media as? TelegramMediaGame {
|
||||
if let image = game.image, let result = findMediaResourceById(media: image, resourceId: resourceId) {
|
||||
return result
|
||||
}
|
||||
if let file = game.file, let result = findMediaResourceById(media: file, resourceId: resourceId) {
|
||||
return result
|
||||
}
|
||||
} else if let action = media as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case let .photoUpdated(image):
|
||||
if let image = image, let result = findMediaResourceById(media: image, resourceId: resourceId) {
|
||||
return result
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func findUpdatedMediaResource(media: Media, previousMedia: Media?, resource: MediaResource) -> TelegramMediaResource? {
|
||||
if let foundResource = findMediaResource(media: media, previousMedia: previousMedia, resource: resource) {
|
||||
return foundResource
|
||||
|
@ -60,6 +60,7 @@ public struct Namespaces {
|
||||
public static let RecentlyUsedHashtags: Int32 = 8
|
||||
public static let CloudThemes: Int32 = 9
|
||||
public static let CloudGreetingStickers: Int32 = 10
|
||||
public static let RecentDownloads: Int32 = 11
|
||||
}
|
||||
|
||||
public struct CachedItemCollection {
|
||||
|
@ -0,0 +1,252 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class RecentDownloadItem: Codable, Equatable {
|
||||
struct Id {
|
||||
var rawValue: MemoryBuffer
|
||||
|
||||
init(id: MessageId, resourceId: String) {
|
||||
let buffer = WriteBuffer()
|
||||
|
||||
var idId: Int32 = id.id
|
||||
buffer.write(&idId, length: 4)
|
||||
|
||||
var idNamespace: Int32 = id.namespace
|
||||
buffer.write(&idNamespace, length: 4)
|
||||
|
||||
var peerId: Int64 = id.peerId.toInt64()
|
||||
buffer.write(&peerId, length: 8)
|
||||
|
||||
let resourceIdData = resourceId.data(using: .utf8)!
|
||||
var resourceIdLength = Int32(resourceIdData.count)
|
||||
buffer.write(&resourceIdLength, length: 4)
|
||||
buffer.write(resourceIdData)
|
||||
|
||||
self.rawValue = buffer.makeReadBufferAndReset()
|
||||
}
|
||||
}
|
||||
|
||||
public let messageId: MessageId
|
||||
public let resourceId: String
|
||||
public let timestamp: Int32
|
||||
public let isSeen: Bool
|
||||
|
||||
public init(messageId: MessageId, resourceId: String, timestamp: Int32, isSeen: Bool) {
|
||||
self.messageId = messageId
|
||||
self.resourceId = resourceId
|
||||
self.timestamp = timestamp
|
||||
self.isSeen = isSeen
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
self.messageId = try container.decode(MessageId.self, forKey: "messageId")
|
||||
self.resourceId = try container.decode(String.self, forKey: "resourceId")
|
||||
self.timestamp = try container.decode(Int32.self, forKey: "timestamp")
|
||||
self.isSeen = try container.decodeIfPresent(Bool.self, forKey: "isSeen") ?? false
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
try container.encode(self.messageId, forKey: "messageId")
|
||||
try container.encode(self.resourceId, forKey: "resourceId")
|
||||
try container.encode(self.timestamp, forKey: "timestamp")
|
||||
try container.encode(self.isSeen, forKey: "isSeen")
|
||||
}
|
||||
|
||||
public static func ==(lhs: RecentDownloadItem, rhs: RecentDownloadItem) -> Bool {
|
||||
if lhs.messageId != rhs.messageId {
|
||||
return false
|
||||
}
|
||||
if lhs.resourceId != rhs.resourceId {
|
||||
return false
|
||||
}
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return false
|
||||
}
|
||||
if lhs.isSeen != rhs.isSeen {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func withSeen() -> RecentDownloadItem {
|
||||
return RecentDownloadItem(messageId: self.messageId, resourceId: self.resourceId, timestamp: self.timestamp, isSeen: true)
|
||||
}
|
||||
}
|
||||
|
||||
public final class RenderedRecentDownloadItem: Equatable {
|
||||
public let message: Message
|
||||
public let timestamp: Int32
|
||||
public let isSeen: Bool
|
||||
public let resourceId: String
|
||||
public let size: Int
|
||||
|
||||
public init(message: Message, timestamp: Int32, isSeen: Bool, resourceId: String, size: Int) {
|
||||
self.message = message
|
||||
self.timestamp = timestamp
|
||||
self.isSeen = isSeen
|
||||
self.resourceId = resourceId
|
||||
self.size = size
|
||||
}
|
||||
|
||||
public static func ==(lhs: RenderedRecentDownloadItem, rhs: RenderedRecentDownloadItem) -> Bool {
|
||||
if lhs.message.id != rhs.message.id {
|
||||
return false
|
||||
}
|
||||
if lhs.message.stableVersion != rhs.message.stableVersion {
|
||||
return false
|
||||
}
|
||||
if lhs.timestamp != rhs.timestamp {
|
||||
return false
|
||||
}
|
||||
if lhs.isSeen != rhs.isSeen {
|
||||
return false
|
||||
}
|
||||
if lhs.resourceId != rhs.resourceId {
|
||||
return false
|
||||
}
|
||||
if lhs.size != rhs.size {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public func recentDownloadItems(postbox: Postbox) -> Signal<[RenderedRecentDownloadItem], NoError> {
|
||||
let viewKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.RecentDownloads)
|
||||
return postbox.combinedView(keys: [viewKey])
|
||||
|> mapToSignal { views -> Signal<[RenderedRecentDownloadItem], NoError> in
|
||||
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
||||
return .single([])
|
||||
}
|
||||
|
||||
return combineLatest(postbox.transaction { transaction -> [RenderedRecentDownloadItem] in
|
||||
var result: [RenderedRecentDownloadItem] = []
|
||||
|
||||
for item in view.items {
|
||||
guard let item = item.contents.get(RecentDownloadItem.self) else {
|
||||
continue
|
||||
}
|
||||
guard let message = transaction.getMessage(item.messageId) else {
|
||||
continue
|
||||
}
|
||||
|
||||
var size: Int?
|
||||
for media in message.media {
|
||||
if let result = findMediaResourceById(media: media, resourceId: MediaResourceId(item.resourceId)) {
|
||||
size = result.size
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let size = size {
|
||||
result.append(RenderedRecentDownloadItem(message: message, timestamp: item.timestamp, isSeen: item.isSeen, resourceId: item.resourceId, size: size))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}, postbox.mediaBox.didRemoveResources)
|
||||
|> mapToSignal { items, _ -> Signal<[RenderedRecentDownloadItem], NoError> in
|
||||
var statusSignals: [Signal<Bool, NoError>] = []
|
||||
|
||||
for item in items {
|
||||
statusSignals.append(postbox.mediaBox.resourceStatus(MediaResourceId(item.resourceId), resourceSize: item.size)
|
||||
|> map { status -> Bool in
|
||||
switch status {
|
||||
case .Local:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged)
|
||||
}
|
||||
|
||||
return combineLatest(queue: .mainQueue(), statusSignals)
|
||||
|> map { statuses -> [RenderedRecentDownloadItem] in
|
||||
var result: [RenderedRecentDownloadItem] = []
|
||||
for i in 0 ..< items.count {
|
||||
if statuses[i] {
|
||||
result.append(items[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func addRecentDownloadItem(postbox: Postbox, item: RecentDownloadItem) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction -> Void in
|
||||
guard let entry = CodableEntry(item) else {
|
||||
return
|
||||
}
|
||||
transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentDownloads, item: OrderedItemListEntry(id: RecentDownloadItem.Id(id: item.messageId, resourceId: item.resourceId).rawValue, contents: entry), removeTailIfCountExceeds: 200)
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
public func markRecentDownloadItemsAsSeen(postbox: Postbox, items: [(messageId: MessageId, resourceId: String)]) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction -> Void in
|
||||
var unseenIds: [(messageId: MessageId, resourceId: String)] = []
|
||||
for item in items {
|
||||
guard let listItem = transaction.getOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentDownloads, itemId: RecentDownloadItem.Id(id: item.messageId, resourceId: item.resourceId).rawValue) else {
|
||||
continue
|
||||
}
|
||||
guard let listItemValue = listItem.contents.get(RecentDownloadItem.self), !listItemValue.isSeen else {
|
||||
continue
|
||||
}
|
||||
unseenIds.append(item)
|
||||
}
|
||||
|
||||
if unseenIds.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
let items = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.RecentDownloads)
|
||||
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.RecentDownloads, items: items.compactMap { entry -> OrderedItemListEntry? in
|
||||
guard let item = entry.contents.get(RecentDownloadItem.self) else {
|
||||
return nil
|
||||
}
|
||||
if unseenIds.contains(where: { $0.messageId == item.messageId && $0.resourceId == item.resourceId }) {
|
||||
guard let entry = CodableEntry(item.withSeen()) else {
|
||||
return nil
|
||||
}
|
||||
return OrderedItemListEntry(id: RecentDownloadItem.Id(id: item.messageId, resourceId: item.resourceId).rawValue, contents: entry)
|
||||
} else {
|
||||
return entry
|
||||
}
|
||||
})
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
|
||||
public func markAllRecentDownloadItemsAsSeen(postbox: Postbox) -> Signal<Never, NoError> {
|
||||
return postbox.transaction { transaction -> Void in
|
||||
let items = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.RecentDownloads)
|
||||
var hasUnseen = false
|
||||
for item in items {
|
||||
if let item = item.contents.get(RecentDownloadItem.self), !item.isSeen {
|
||||
hasUnseen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasUnseen {
|
||||
return
|
||||
}
|
||||
transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.RecentDownloads, items: items.compactMap { item -> OrderedItemListEntry? in
|
||||
guard let item = item.contents.get(RecentDownloadItem.self) else {
|
||||
return nil
|
||||
}
|
||||
guard let entry = CodableEntry(item.withSeen()) else {
|
||||
return nil
|
||||
}
|
||||
return OrderedItemListEntry(id: RecentDownloadItem.Id(id: item.messageId, resourceId: item.resourceId).rawValue, contents: entry)
|
||||
})
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
@ -143,7 +143,7 @@ public enum CreateGroupCallError {
|
||||
case scheduledTooLate
|
||||
}
|
||||
|
||||
func _internal_createGroupCall(account: Account, peerId: PeerId, title: String?, scheduleDate: Int32?) -> Signal<GroupCallInfo, CreateGroupCallError> {
|
||||
func _internal_createGroupCall(account: Account, peerId: PeerId, title: String?, scheduleDate: Int32?, isExternalStream: Bool) -> Signal<GroupCallInfo, CreateGroupCallError> {
|
||||
return account.postbox.transaction { transaction -> Api.InputPeer? in
|
||||
let callPeer = transaction.getPeer(peerId).flatMap(apiInputPeer)
|
||||
return callPeer
|
||||
@ -160,6 +160,9 @@ func _internal_createGroupCall(account: Account, peerId: PeerId, title: String?,
|
||||
if let _ = scheduleDate {
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
if isExternalStream {
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
return account.network.request(Api.functions.phone.createGroupCall(flags: flags, peer: inputPeer, randomId: Int32.random(in: Int32.min ... Int32.max), title: title, scheduleDate: scheduleDate))
|
||||
|> mapError { error -> CreateGroupCallError in
|
||||
if error.errorDescription == "ANONYMOUS_CALLS_DISABLED" {
|
||||
@ -433,7 +436,7 @@ public enum JoinGroupCallError {
|
||||
public struct JoinGroupCallResult {
|
||||
public enum ConnectionMode {
|
||||
case rtc
|
||||
case broadcast
|
||||
case broadcast(isExternalStream: Bool)
|
||||
}
|
||||
|
||||
public var callInfo: GroupCallInfo
|
||||
@ -589,12 +592,16 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?,
|
||||
let connectionMode: JoinGroupCallResult.ConnectionMode
|
||||
if let clientParamsData = parsedClientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: clientParamsData, options: [])) as? [String: Any] {
|
||||
if let stream = dict["stream"] as? Bool, stream {
|
||||
connectionMode = .broadcast
|
||||
var isExternalStream = false
|
||||
if let rtmp = dict["rtmp"] as? Bool, rtmp {
|
||||
isExternalStream = true
|
||||
}
|
||||
connectionMode = .broadcast(isExternalStream: isExternalStream)
|
||||
} else {
|
||||
connectionMode = .rtc
|
||||
}
|
||||
} else {
|
||||
connectionMode = .broadcast
|
||||
connectionMode = .broadcast(isExternalStream: false)
|
||||
}
|
||||
|
||||
return account.postbox.transaction { transaction -> JoinGroupCallResult in
|
||||
|
@ -1,5 +1,17 @@
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
|
||||
public struct EngineCallStreamState {
|
||||
public struct Channel {
|
||||
public var id: Int32
|
||||
public var scale: Int32
|
||||
public var latestTimestamp: Int64
|
||||
}
|
||||
|
||||
public var channels: [Channel]
|
||||
}
|
||||
|
||||
public extension TelegramEngine {
|
||||
final class Calls {
|
||||
@ -21,8 +33,8 @@ public extension TelegramEngine {
|
||||
return _internal_getCurrentGroupCall(account: self.account, callId: callId, accessHash: accessHash, peerId: peerId)
|
||||
}
|
||||
|
||||
public func createGroupCall(peerId: PeerId, title: String?, scheduleDate: Int32?) -> Signal<GroupCallInfo, CreateGroupCallError> {
|
||||
return _internal_createGroupCall(account: self.account, peerId: peerId, title: title, scheduleDate: scheduleDate)
|
||||
public func createGroupCall(peerId: PeerId, title: String?, scheduleDate: Int32?, isExternalStream: Bool) -> Signal<GroupCallInfo, CreateGroupCallError> {
|
||||
return _internal_createGroupCall(account: self.account, peerId: peerId, title: title, scheduleDate: scheduleDate, isExternalStream: isExternalStream)
|
||||
}
|
||||
|
||||
public func startScheduledGroupCall(peerId: PeerId, callId: Int64, accessHash: Int64) -> Signal<GroupCallInfo, StartScheduledGroupCallError> {
|
||||
@ -119,5 +131,28 @@ public extension TelegramEngine {
|
||||
}
|
||||
|> take(1)
|
||||
}
|
||||
|
||||
public func requestStreamState(callId: Int64, accessHash: Int64) -> Signal<EngineCallStreamState?, NoError> {
|
||||
return self.account.network.request(Api.functions.phone.getGroupCallStreamChannels(call: .inputGroupCall(id: callId, accessHash: accessHash)))
|
||||
|> mapToSignal { result -> Signal<EngineCallStreamState?, MTRpcError> in
|
||||
switch result {
|
||||
case let .groupCallStreamChannels(channels):
|
||||
let state = EngineCallStreamState(channels: channels.map { channel -> EngineCallStreamState.Channel in
|
||||
switch channel {
|
||||
case let .groupCallStreamChannel(channel, scale, lastTimestampMs):
|
||||
return EngineCallStreamState.Channel(id: channel, scale: scale, latestTimestamp: lastTimestampMs)
|
||||
}
|
||||
})
|
||||
/*if state.channels.isEmpty {
|
||||
return .fail(MTRpcError(errorCode: 500, errorDescription: "Generated")) |> delay(10.0, queue: .mainQueue())
|
||||
}*/
|
||||
return .single(state)
|
||||
}
|
||||
}
|
||||
//|> restartIfError
|
||||
|> `catch` { _ -> Signal<EngineCallStreamState?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,9 +60,10 @@ public final class EngineMediaResource: Equatable {
|
||||
}
|
||||
|
||||
public enum FetchStatus: Equatable {
|
||||
case Remote
|
||||
case Remote(progress: Float)
|
||||
case Local
|
||||
case Fetching(isActive: Bool, progress: Float)
|
||||
case Paused(progress: Float)
|
||||
}
|
||||
|
||||
public struct Id: Equatable, Hashable {
|
||||
@ -105,23 +106,27 @@ public extension EngineMediaResource.ResourceData {
|
||||
public extension EngineMediaResource.FetchStatus {
|
||||
init(_ status: MediaResourceStatus) {
|
||||
switch status {
|
||||
case .Remote:
|
||||
self = .Remote
|
||||
case let .Remote(progress):
|
||||
self = .Remote(progress: progress)
|
||||
case .Local:
|
||||
self = .Local
|
||||
case let .Fetching(isActive, progress):
|
||||
self = .Fetching(isActive: isActive, progress: progress)
|
||||
case let .Paused(progress):
|
||||
self = .Paused(progress: progress)
|
||||
}
|
||||
}
|
||||
|
||||
func _asStatus() -> MediaResourceStatus {
|
||||
switch self {
|
||||
case .Remote:
|
||||
return .Remote
|
||||
case let .Remote(progress):
|
||||
return .Remote(progress: progress)
|
||||
case .Local:
|
||||
return .Local
|
||||
case let .Fetching(isActive, progress):
|
||||
return .Fetching(isActive: isActive, progress: progress)
|
||||
case let .Paused(progress):
|
||||
return .Paused(progress: progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -683,7 +683,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
statusController?.dismiss()
|
||||
}
|
||||
strongSelf.present(statusController, in: .window(.root))
|
||||
strongSelf.createVoiceChatDisposable.set((strongSelf.context.engine.calls.createGroupCall(peerId: message.id.peerId, title: nil, scheduleDate: nil)
|
||||
strongSelf.createVoiceChatDisposable.set((strongSelf.context.engine.calls.createGroupCall(peerId: message.id.peerId, title: nil, scheduleDate: nil, isExternalStream: false)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] info in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
@ -1815,6 +1815,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
var messageIdsWithRefreshMedia: [MessageId] = []
|
||||
var messageIdsWithUnseenPersonalMention: [MessageId] = []
|
||||
var messageIdsWithUnseenReactions: [MessageId] = []
|
||||
var downloadableResourceIds: [(messageId: MessageId, resourceId: String)] = []
|
||||
|
||||
if indexRange.0 <= indexRange.1 {
|
||||
for i in (indexRange.0 ... indexRange.1) {
|
||||
@ -1875,6 +1876,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
downloadableResourceIds.append((message.id, telegramFile.resource.id.stringRepresentation))
|
||||
} else if let image = media as? TelegramMediaImage {
|
||||
if let representation = image.representations.last {
|
||||
downloadableResourceIds.append((message.id, representation.resource.id.stringRepresentation))
|
||||
}
|
||||
}
|
||||
}
|
||||
if contentRequiredValidation {
|
||||
@ -1889,6 +1895,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
if hasUnseenReactions {
|
||||
messageIdsWithUnseenReactions.append(message.id)
|
||||
}
|
||||
|
||||
if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.effectiveTopId == message.id {
|
||||
isTopReplyThreadMessageShownValue = true
|
||||
}
|
||||
@ -1909,6 +1916,15 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
for media in message.media {
|
||||
if let telegramFile = media as? TelegramMediaFile {
|
||||
downloadableResourceIds.append((message.id, telegramFile.resource.id.stringRepresentation))
|
||||
} else if let image = media as? TelegramMediaImage {
|
||||
if let representation = image.representations.last {
|
||||
downloadableResourceIds.append((message.id, representation.resource.id.stringRepresentation))
|
||||
}
|
||||
}
|
||||
}
|
||||
for attribute in message.attributes {
|
||||
if attribute is ViewCountMessageAttribute {
|
||||
if message.id.namespace == Namespaces.Message.Cloud {
|
||||
@ -2073,6 +2089,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
if !messageIdsWithPossibleReactions.isEmpty {
|
||||
self.messageWithReactionsProcessingManager.add(messageIdsWithPossibleReactions)
|
||||
}
|
||||
if !downloadableResourceIds.isEmpty {
|
||||
let _ = markRecentDownloadItemsAsSeen(postbox: self.context.account.postbox, items: downloadableResourceIds).start()
|
||||
}
|
||||
|
||||
self.currentEarlierPrefetchMessages = toEarlierMediaMessages
|
||||
self.currentLaterPrefetchMessages = toLaterMediaMessages
|
||||
|
@ -226,7 +226,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
|
||||
cancel()
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
|
||||
fetch()
|
||||
}
|
||||
@ -249,7 +249,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
|
||||
cancel()
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
|
||||
fetch()
|
||||
}
|
||||
@ -947,7 +947,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
} else {
|
||||
state = .none
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if isAudio && !isVoice {
|
||||
state = .play
|
||||
} else {
|
||||
@ -971,7 +971,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0))
|
||||
case .Local:
|
||||
streamingState = .none
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
streamingState = .download
|
||||
}
|
||||
} else {
|
||||
|
@ -675,7 +675,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
} else {
|
||||
state = .none
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
state = .download(messageTheme.mediaOverlayControlColors.foregroundColor)
|
||||
}
|
||||
default:
|
||||
@ -824,7 +824,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
} else {
|
||||
messageMediaFileCancelInteractiveFetch(context: item.context, messageId: item.message.id, file: file)
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if let file = self.media {
|
||||
self.fetchDisposable.set(messageMediaFileInteractiveFetched(context: item.context, message: item.message, file: file, userInitiated: true).start())
|
||||
}
|
||||
|
@ -321,7 +321,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
if let cancel = self.fetchControls.with({ return $0?.cancel }) {
|
||||
cancel()
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if let fetch = self.fetchControls.with({ return $0?.fetch }) {
|
||||
fetch(true)
|
||||
}
|
||||
@ -1213,7 +1213,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
progressRequired = true
|
||||
}
|
||||
}
|
||||
case .Remote, .Fetching:
|
||||
case .Remote, .Fetching, .Paused:
|
||||
if let webpage = webpage, let automaticDownload = self.automaticDownload, case .full = automaticDownload, case let .Loaded(content) = webpage.content {
|
||||
if content.type == "telegram_background" {
|
||||
progressRequired = true
|
||||
@ -1448,7 +1448,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition)
|
||||
badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: nil, muted: muted, active: false)
|
||||
}
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
state = .download(messageTheme.mediaOverlayControlColors.foregroundColor)
|
||||
if let file = self.media as? TelegramMediaFile, !file.isVideoSticker {
|
||||
do {
|
||||
|
@ -250,7 +250,7 @@ final class GridMessageItemNode: GridItemNode {
|
||||
statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
|
||||
case .Local:
|
||||
statusState = .none
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
statusState = .download(.white)
|
||||
}
|
||||
}
|
||||
@ -284,7 +284,7 @@ final class GridMessageItemNode: GridItemNode {
|
||||
mediaDownloadState = .compactFetching(progress: 0.0)
|
||||
case .Local:
|
||||
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
|
||||
mediaDownloadState = .compactRemote
|
||||
}
|
||||
@ -432,7 +432,7 @@ final class GridMessageItemNode: GridItemNode {
|
||||
messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file)
|
||||
case .Local:
|
||||
let _ = controllerInteraction.openMessage(message, .default)
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
self.fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, message: message, file: file, userInitiated: true).start())
|
||||
}
|
||||
}
|
||||
|
@ -461,7 +461,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
switch status {
|
||||
case let .Fetching(_, progress):
|
||||
state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(max(progress, 0.2)), cancelEnabled: false, animateRotation: true)
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
//state = .download(statusForegroundColor)
|
||||
state = .none
|
||||
case .Local:
|
||||
|
@ -678,13 +678,6 @@ private final class VisualMediaItem: SparseItemGrid.Item {
|
||||
}
|
||||
}
|
||||
|
||||
private final class NullActionClass: NSObject, CAAction {
|
||||
@objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
|
||||
}
|
||||
}
|
||||
|
||||
private let nullAction = NullActionClass()
|
||||
|
||||
private struct Month: Equatable {
|
||||
var packedValue: Int32
|
||||
|
||||
|
@ -404,7 +404,6 @@ private enum PeerInfoSettingsSection {
|
||||
case avatar
|
||||
case edit
|
||||
case proxy
|
||||
case downloads
|
||||
case savedMessages
|
||||
case recentCalls
|
||||
case devices
|
||||
@ -664,10 +663,6 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
|
||||
}
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
items[.shortcuts]!.append(PeerInfoScreenDisclosureItem(id: 0, text: "Downloads", icon: PresentationResourcesSettings.savedMessages, action: {
|
||||
interaction.openSettings(.downloads)
|
||||
}))
|
||||
items[.shortcuts]!.append(PeerInfoScreenDisclosureItem(id: 1, text: presentationData.strings.Settings_SavedMessages, icon: PresentationResourcesSettings.savedMessages, action: {
|
||||
interaction.openSettings(.savedMessages)
|
||||
}))
|
||||
@ -4118,6 +4113,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
return
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
let isExternalStream: Bool = true
|
||||
#else
|
||||
let isExternalStream: Bool = false
|
||||
#endif
|
||||
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = strongSelf.presentationData
|
||||
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
|
||||
@ -4134,7 +4135,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
let createSignal = strongSelf.context.engine.calls.createGroupCall(peerId: peerId, title: nil, scheduleDate: nil)
|
||||
let createSignal = strongSelf.context.engine.calls.createGroupCall(peerId: peerId, title: nil, scheduleDate: nil, isExternalStream: isExternalStream)
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
@ -5533,8 +5534,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
self.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil)
|
||||
case .proxy:
|
||||
self.controller?.push(proxySettingsController(context: self.context))
|
||||
case .downloads:
|
||||
self.controller?.push(downloadsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData))
|
||||
case .savedMessages:
|
||||
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController {
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(self.context.account.peerId)))
|
||||
|
@ -260,7 +260,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
messageMediaFileCancelInteractiveFetch(context: self.context, messageId: message.id, file: file)
|
||||
case .Local:
|
||||
self.interaction.openMessage(message)
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
self.fetchDisposable.set(messageMediaFileInteractiveFetched(context: self.context, message: message, file: file, userInitiated: true).start())
|
||||
}
|
||||
}
|
||||
@ -348,7 +348,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
statusState = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
|
||||
case .Local:
|
||||
statusState = .none
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
statusState = .download(.white)
|
||||
}
|
||||
}
|
||||
@ -382,7 +382,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
mediaDownloadState = .compactFetching(progress: 0.0)
|
||||
case .Local:
|
||||
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
|
||||
mediaDownloadState = .compactRemote
|
||||
}
|
||||
|
@ -1128,6 +1128,15 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
navigateToChatControllerImpl(params)
|
||||
}
|
||||
|
||||
public func openStorageUsage(context: AccountContext) {
|
||||
guard let navigationController = self.mainWindow?.viewController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = storageUsageController(context: context, isModal: true)
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
|
||||
public func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController) {
|
||||
var found = false
|
||||
for controller in navigationController.viewControllers.reversed() {
|
||||
|
@ -364,7 +364,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode {
|
||||
switch status {
|
||||
case let .Fetching(_, progress):
|
||||
state = RadialStatusNodeState.progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(max(progress, 0.2)), cancelEnabled: false, animateRotation: true)
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
state = .download(statusForegroundColor)
|
||||
case .Local:
|
||||
state = .none
|
||||
@ -378,7 +378,6 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode {
|
||||
} else {
|
||||
strongSelf.statusNode.transitionToState(.none, completion: { })
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -42,19 +42,29 @@ private final class NetworkBroadcastPartSource: BroadcastPartSource {
|
||||
private let engine: TelegramEngine
|
||||
private let callId: Int64
|
||||
private let accessHash: Int64
|
||||
private let isExternalStream: Bool
|
||||
private var dataSource: AudioBroadcastDataSource?
|
||||
|
||||
init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64) {
|
||||
init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64, isExternalStream: Bool) {
|
||||
self.queue = queue
|
||||
self.engine = engine
|
||||
self.callId = callId
|
||||
self.accessHash = accessHash
|
||||
self.isExternalStream = isExternalStream
|
||||
}
|
||||
|
||||
func requestTime(completion: @escaping (Int64) -> Void) -> Disposable {
|
||||
return engine.calls.serverTime().start(next: { result in
|
||||
completion(result)
|
||||
})
|
||||
if self.isExternalStream {
|
||||
return self.engine.calls.requestStreamState(callId: self.callId, accessHash: self.accessHash).start(next: { result in
|
||||
if let channel = result?.channels.first {
|
||||
completion(channel.latestTimestamp)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return self.engine.calls.serverTime().start(next: { result in
|
||||
completion(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func requestPart(timestampMilliseconds: Int64, durationMilliseconds: Int64, subject: BroadcastPartSubject, completion: @escaping (OngoingGroupCallBroadcastPart) -> Void, rejoinNeeded: @escaping () -> Void) -> Disposable {
|
||||
@ -146,11 +156,13 @@ public final class OngoingGroupCallContext {
|
||||
public var engine: TelegramEngine
|
||||
public var callId: Int64
|
||||
public var accessHash: Int64
|
||||
public var isExternalStream: Bool
|
||||
|
||||
public init(engine: TelegramEngine, callId: Int64, accessHash: Int64) {
|
||||
public init(engine: TelegramEngine, callId: Int64, accessHash: Int64, isExternalStream: Bool) {
|
||||
self.engine = engine
|
||||
self.callId = callId
|
||||
self.accessHash = accessHash
|
||||
self.isExternalStream = isExternalStream
|
||||
}
|
||||
}
|
||||
|
||||
@ -361,21 +373,14 @@ public final class OngoingGroupCallContext {
|
||||
|
||||
private var currentRequestedVideoChannels: [VideoChannel] = []
|
||||
|
||||
private var broadcastPartsSource: BroadcastPartSource?
|
||||
private let broadcastPartsSource = Atomic<BroadcastPartSource?>(value: nil)
|
||||
|
||||
init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, preferX264: Bool) {
|
||||
init(queue: Queue, inputDeviceId: String, outputDeviceId: String, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, preferX264: Bool) {
|
||||
self.queue = queue
|
||||
|
||||
var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)?
|
||||
var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)?
|
||||
|
||||
if let audioStreamData = audioStreamData {
|
||||
let broadcastPartsSource = NetworkBroadcastPartSource(queue: queue, engine: audioStreamData.engine, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash)
|
||||
self.broadcastPartsSource = broadcastPartsSource
|
||||
}
|
||||
|
||||
let broadcastPartsSource = self.broadcastPartsSource
|
||||
|
||||
let _videoContentType: OngoingGroupCallVideoContentType
|
||||
switch videoContentType {
|
||||
case .generic:
|
||||
@ -385,6 +390,8 @@ public final class OngoingGroupCallContext {
|
||||
case .none:
|
||||
_videoContentType = .none
|
||||
}
|
||||
|
||||
var getBroadcastPartsSource: (() -> BroadcastPartSource?)?
|
||||
|
||||
self.context = GroupCallThreadLocalContext(
|
||||
queue: ContextQueueImpl(queue: queue),
|
||||
@ -433,7 +440,7 @@ public final class OngoingGroupCallContext {
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
queue.async {
|
||||
disposable.set(broadcastPartsSource?.requestTime(completion: completion))
|
||||
disposable.set(getBroadcastPartsSource?()?.requestTime(completion: completion))
|
||||
}
|
||||
|
||||
return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable)
|
||||
@ -442,7 +449,7 @@ public final class OngoingGroupCallContext {
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
queue.async {
|
||||
disposable.set(broadcastPartsSource?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .audio, completion: completion, rejoinNeeded: {
|
||||
disposable.set(getBroadcastPartsSource?()?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .audio, completion: completion, rejoinNeeded: {
|
||||
rejoinNeeded()
|
||||
}))
|
||||
}
|
||||
@ -464,7 +471,7 @@ public final class OngoingGroupCallContext {
|
||||
@unknown default:
|
||||
mappedQuality = .thumbnail
|
||||
}
|
||||
disposable.set(broadcastPartsSource?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .video(channelId: channelId, quality: mappedQuality), completion: completion, rejoinNeeded: {
|
||||
disposable.set(getBroadcastPartsSource?()?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .video(channelId: channelId, quality: mappedQuality), completion: completion, rejoinNeeded: {
|
||||
rejoinNeeded()
|
||||
}))
|
||||
}
|
||||
@ -479,6 +486,11 @@ public final class OngoingGroupCallContext {
|
||||
|
||||
let queue = self.queue
|
||||
|
||||
let broadcastPartsSource = self.broadcastPartsSource
|
||||
getBroadcastPartsSource = {
|
||||
return broadcastPartsSource.with { $0 }
|
||||
}
|
||||
|
||||
networkStateUpdatedImpl = { [weak self] state in
|
||||
queue.async {
|
||||
guard let strongSelf = self else {
|
||||
@ -525,6 +537,13 @@ public final class OngoingGroupCallContext {
|
||||
self.context.setJoinResponsePayload(payload)
|
||||
}
|
||||
|
||||
func setAudioStreamData(audioStreamData: AudioStreamData?) {
|
||||
if let audioStreamData = audioStreamData {
|
||||
let broadcastPartsSource = NetworkBroadcastPartSource(queue: self.queue, engine: audioStreamData.engine, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash, isExternalStream: audioStreamData.isExternalStream)
|
||||
let _ = self.broadcastPartsSource.swap(broadcastPartsSource)
|
||||
}
|
||||
}
|
||||
|
||||
func addSsrcs(ssrcs: [UInt32]) {
|
||||
}
|
||||
|
||||
@ -874,10 +893,10 @@ public final class OngoingGroupCallContext {
|
||||
}
|
||||
}
|
||||
|
||||
public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, audioStreamData: AudioStreamData?, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, preferX264: Bool) {
|
||||
public init(inputDeviceId: String = "", outputDeviceId: String = "", video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, preferX264: Bool) {
|
||||
let queue = self.queue
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, audioStreamData: audioStreamData, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, preferX264: preferX264)
|
||||
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, preferX264: preferX264)
|
||||
})
|
||||
}
|
||||
|
||||
@ -916,17 +935,25 @@ public final class OngoingGroupCallContext {
|
||||
impl.switchAudioInput(deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
public func switchAudioOutput(_ deviceId: String) {
|
||||
self.impl.with { impl in
|
||||
impl.switchAudioOutput(deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
public func setJoinResponse(payload: String) {
|
||||
self.impl.with { impl in
|
||||
impl.setJoinResponse(payload: payload)
|
||||
}
|
||||
}
|
||||
|
||||
public func setAudioStreamData(audioStreamData: AudioStreamData?) {
|
||||
self.impl.with { impl in
|
||||
impl.setAudioStreamData(audioStreamData: audioStreamData)
|
||||
}
|
||||
}
|
||||
|
||||
public func addSsrcs(ssrcs: [UInt32]) {
|
||||
self.impl.with { impl in
|
||||
impl.addSsrcs(ssrcs: ssrcs)
|
||||
|
@ -251,7 +251,7 @@ func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyW
|
||||
switch status {
|
||||
case .Local:
|
||||
return 1.0
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
return 0.027
|
||||
case let .Fetching(_, progress):
|
||||
return max(progress, 0.1)
|
||||
|
@ -527,7 +527,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
switch fetchStatus {
|
||||
case .Local:
|
||||
videoNode.togglePlayPause()
|
||||
case .Remote:
|
||||
case .Remote, .Paused:
|
||||
if self.requiresDownload {
|
||||
self.fetchControls?.fetch()
|
||||
} else {
|
||||
|
Loading…
x
Reference in New Issue
Block a user