Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2022-02-21 10:48:29 +03:00
commit 2dd23729b5
83 changed files with 2492 additions and 839 deletions

View File

@ -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>

View File

@ -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>
}

View File

@ -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)
}

View File

@ -35,6 +35,7 @@ swift_library(
"//submodules/ManagedFile:ManagedFile",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AnimationCompression:AnimationCompression",
"//submodules/Components/MetalImageView:MetalImageView",
],
visibility = [
"//visibility:public",

View File

@ -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

View File

@ -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()

View File

@ -42,9 +42,9 @@ final class AnimationRendererPool {
}
private func putBack(renderer: AnimationRenderer) {
#if DEBUG
/*#if DEBUG
self.items.append(renderer)
#endif
#endif*/
}
}

View File

@ -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
})
}*/
}
}
}

View File

@ -54,6 +54,7 @@ swift_library(
],
deps = [
":DctHuffman",
"//submodules/Components/MetalImageView:MetalImageView",
],
visibility = [
"//visibility:public",

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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",

View File

@ -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() {

View File

@ -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? {

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -0,0 +1,3 @@
import Foundation
import UIKit

View 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
}
}
}

View 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",
],
)

View File

@ -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)
}
}

View 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",
],
)

View File

@ -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
}
}

View 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",
],
)

View File

@ -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)
}
}

View File

@ -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

View File

@ -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()

View File

@ -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")
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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)
}
})

View File

@ -299,7 +299,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
default:
break
}
case .Remote:
case .Remote, .Paused:
if case .tap = gesture {
self.fetchControls?.fetch(true)
}

View File

@ -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()

View File

@ -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()
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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()
})
})

View File

@ -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

View File

@ -16,6 +16,7 @@ swift_library(
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ActivityIndicator:ActivityIndicator",
"//submodules/AppBundle:AppBundle",
"//submodules/ComponentFlow:ComponentFlow",
],
visibility = [
"//visibility:public",

View File

@ -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()

View File

@ -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)
}
}
})
}

View File

@ -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
}

View File

@ -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: {})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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())
}

View File

@ -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 {

View File

@ -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())
}
}

View File

@ -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:

View File

@ -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

View File

@ -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)))

View File

@ -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
}

View File

@ -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() {

View File

@ -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: { })
}
}
})
}

View File

@ -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)

View File

@ -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)

View File

@ -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 {