From ac8e9a0b9c12f5a3dc84ca94be18ba2bec6391bb Mon Sep 17 00:00:00 2001 From: Ali <> Date: Fri, 18 Feb 2022 20:12:38 +0400 Subject: [PATCH] Download manager improvements --- .../AccountContext/Sources/FetchManager.swift | 3 + .../Sources/FetchMediaUtils.swift | 2 +- submodules/ChatListUI/BUILD | 1 + .../Sources/ChatListController.swift | 262 +++++++++++++++--- .../Sources/ChatListSearchContainerNode.swift | 43 ++- .../Sources/ChatListSearchListPaneNode.swift | 113 ++++++-- .../Sources/ChatListSearchMediaNode.swift | 8 +- submodules/CheckNode/Sources/CheckNode.swift | 7 - .../Source/Components/LazyList.swift | 3 + .../Source/Components/ZStack.swift | 49 ++++ .../Sources/LottieAnimationComponent.swift | 2 +- .../ProgressIndicatorComponent/BUILD | 19 ++ .../Sources/ProgressIndicatorComponent.swift | 113 ++++++++ .../Sources/DebugController.swift | 6 +- .../Sources/FetchManagerImpl.swift | 219 +++++++++++---- .../ChatItemGalleryFooterContentNode.swift | 2 +- .../Items/ChatAnimationGalleryItem.swift | 2 +- .../Items/ChatDocumentGalleryItem.swift | 2 +- .../Items/ChatExternalFileGalleryItem.swift | 2 +- .../Sources/Items/ChatImageGalleryItem.swift | 2 +- .../Items/UniversalVideoGalleryItem.swift | 4 +- .../Sources/HashtagSearchController.swift | 4 +- .../Sources/InstantPageImageNode.swift | 2 +- .../InstantPagePlayableVideoNode.swift | 2 +- .../Sources/ListMessageFileItemNode.swift | 34 ++- .../Sources/PhotoResources.swift | 6 +- submodules/Postbox/Sources/MediaBox.swift | 16 +- submodules/Postbox/Sources/MediaBoxFile.swift | 10 +- .../Postbox/Sources/MediaResourceStatus.swift | 29 +- .../Sources/OrderedItemListTable.swift | 10 +- .../Sources/ReactionContextNode.swift | 5 + .../Sources/SaveToCameraRoll.swift | 2 + .../Sources/SearchBarPlaceholderNode.swift | 5 +- .../Sources/Themes/WallpaperGalleryItem.swift | 2 +- .../Sources/SparseItemGrid.swift | 7 - .../Network/FetchedMediaResource.swift | 65 +++++ .../SyncCore_RecentDownloadItem.swift | 57 +++- .../Animations/anim_search_downloaded.json | 2 +- .../Animations/anim_search_downloading.json | 2 +- .../TelegramUI/Sources/ChatController.swift | 2 +- .../Sources/ChatHistoryListNode.swift | 19 ++ .../ChatMessageInteractiveFileNode.swift | 8 +- ...atMessageInteractiveInstantVideoNode.swift | 4 +- .../ChatMessageInteractiveMediaNode.swift | 6 +- .../TelegramUI/Sources/GridMessageItem.swift | 6 +- ...ListContextResultsChatInputPanelItem.swift | 2 +- .../Panes/PeerInfoVisualMediaPaneNode.swift | 7 - .../Sources/PeerInfo/PeerInfoScreen.swift | 8 +- .../Sources/PeerInfoGifPaneNode.swift | 6 +- ...ListContextResultsChatInputPanelItem.swift | 3 +- 50 files changed, 942 insertions(+), 253 deletions(-) create mode 100644 submodules/ComponentFlow/Source/Components/LazyList.swift create mode 100644 submodules/ComponentFlow/Source/Components/ZStack.swift create mode 100644 submodules/Components/ProgressIndicatorComponent/BUILD create mode 100644 submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift diff --git a/submodules/AccountContext/Sources/FetchManager.swift b/submodules/AccountContext/Sources/FetchManager.swift index f53df05fac..b0dd2de54f 100644 --- a/submodules/AccountContext/Sources/FetchManager.swift +++ b/submodules/AccountContext/Sources/FetchManager.swift @@ -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 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 } diff --git a/submodules/AccountContext/Sources/FetchMediaUtils.swift b/submodules/AccountContext/Sources/FetchMediaUtils.swift index 2a11ec0d06..b693b63b07 100644 --- a/submodules/AccountContext/Sources/FetchMediaUtils.swift +++ b/submodules/AccountContext/Sources/FetchMediaUtils.swift @@ -96,7 +96,7 @@ public func messageMediaFileStatus(context: AccountContext, messageId: MessageId public func messageMediaImageStatus(context: AccountContext, messageId: MessageId, image: TelegramMediaImage) -> Signal { 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) } diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 7bdc8060b7..52115e69e3 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -65,6 +65,7 @@ swift_library( "//submodules/FetchManagerImpl:FetchManagerImpl", "//submodules/ComponentFlow:ComponentFlow", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", + "//submodules/Components/ProgressIndicatorComponent:ProgressIndicatorComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index a932b76cdf..bcc2d1fb7c 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -28,6 +28,7 @@ import PasswordSetupUI import FetchManagerImpl import ComponentFlow import LottieAnimationComponent +import ProgressIndicatorComponent private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { @@ -155,6 +156,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var tabContainerData: ([ChatListFilterTabEntry], Bool)? private var activeDownloadsDisposable: Disposable? + private var clearUnseenDownloadsTimer: SwiftSignalKit.Timer? private var didSetupTabs = false @@ -473,16 +475,152 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.searchContentNode?.updateExpansionProgress(0.0) self.navigationBar?.setContentNode(self.searchContentNode, animated: false) - enum State { + enum State: Equatable { case empty - case downloading + case downloading(Double) case hasUnseen } - var stateSignal: Signal = (combineLatest(queue: .mainQueue(), (context.fetchManager as! FetchManagerImpl).entriesSummary, recentDownloadItems(postbox: context.account.postbox)) + 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(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 = (combineLatest(queue: .mainQueue(), entriesWithFetchStatuses, recentDownloadItems(postbox: context.account.postbox)) |> map { entries, recentDownloadItems -> State in if !entries.isEmpty { - return .downloading + 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 { @@ -498,57 +636,89 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController |> distinctUntilChanged |> deliverOnMainQueue) - if !"".isEmpty { + /*if !"".isEmpty { stateSignal = Signal.single(.downloading) |> then(Signal.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 .downloading: - strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button( - content: AnyComponent(LottieAnimationComponent( - 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 - ), - size: CGSize(width: 24.0, height: 24.0) - )), - insets: UIEdgeInsets(), - action: { - guard let strongSelf = self else { - return - } - strongSelf.activateSearch(filter: .downloads, query: nil) - } - ))) + 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: - strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: AnyComponent(Button( - content: AnyComponent(LottieAnimationComponent( - 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 - ), + 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([ + 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 { @@ -557,7 +727,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.activateSearch(filter: .downloads, query: nil) } ))) - case .empty: + } else { strongSelf.searchContentNode?.placeholderNode.setAccessoryComponent(component: nil) } }) diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index aee208e7ea..f6170af27f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -46,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?, ChatListSearchPaneKey, String?) -> 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? - 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, String?) -> 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?) { + 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?) { self.openPeer = openPeer self.openDisabledPeer = openDisabledPeer self.openMessage = openMessage @@ -218,8 +218,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo return state.withUpdatedSelectedMessageIds(selectedMessageIds) } } - }, messageContextAction: { [weak self] message, node, rect, gesture, paneKey, downloadResourceId in - self?.messageContextAction(message, node: node, rect: rect, gesture: gesture, paneKey: paneKey, downloadResourceId: downloadResourceId) + }, 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 @@ -580,7 +580,7 @@ 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 @@ -768,7 +768,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo let _ = self.paneContainerNode.scrollToTop() } - private func messageContextAction(_ message: EngineMessage, node: ASDisplayNode?, rect: CGRect?, gesture anyRecognizer: UIGestureRecognizer?, paneKey: ChatListSearchPaneKey, downloadResourceId: String?) { + 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 } @@ -777,8 +777,8 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if paneKey == .downloads { let isCachedValue: Signal - if let downloadResourceId = downloadResourceId { - isCachedValue = self.context.account.postbox.mediaBox.resourceStatus(MediaResourceId(downloadResourceId), resourceSize: nil) + 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: @@ -821,27 +821,44 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo items.append(.action(ContextMenuActionItem(text: "Delete from Cache", textColor: .primary, icon: { _ in return nil }, action: { _, f in - guard let strongSelf = self, let downloadResourceId = downloadResourceId else { + guard let strongSelf = self, let downloadResource = downloadResource else { f(.default) return } - let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources([MediaResourceId(downloadResourceId)], notify: true) + 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 downloadResourceId = downloadResourceId else { + guard let strongSelf = self, let downloadResource = downloadResource else { f(.default) return } - let _ = strongSelf - let _ = downloadResourceId + strongSelf.context.fetchManager.cancelInteractiveFetches(resourceId: downloadResource.id) f(.default) }))) diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 45ea043416..811935d1df 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -246,7 +246,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case let .downloading(lhsKey): switch rhs { case let .downloading(rhsKey): - return lhsKey > rhsKey + return lhsKey < rhsKey case .index: return false case .downloaded: @@ -277,7 +277,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { 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, MessageOrderingKey, String?, MessageSection) + 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 { @@ -286,7 +286,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return .localPeerId(peer.id) case let .globalPeer(peer, _, _, _, _, _, _, _): return .globalPeerId(peer.peer.id) - case let .message(message, _, _, _, _, _, _, _, _, section): + case let .message(message, _, _, _, _, _, _, _, _, section, _): return .messageId(message.id, section) case .addContact: return .addContact @@ -307,8 +307,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } else { return false } - case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection): - if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection) = 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 } @@ -336,12 +336,18 @@ public enum ChatListSearchEntry: Comparable, Identifiable { if lhsKey != rhsKey { return false } - if lhsResourceId != rhsResourceId { + 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 @@ -381,8 +387,8 @@ public enum ChatListSearchEntry: Comparable, Identifiable { case .message, .addContact: return true } - case let .message(_, _, _, _, _, _, _, lhsKey, _, _): - if case let .message(_, _, _, _, _, _, _, rhsKey, _, _) = rhs { + case let .message(_, _, _, _, _, _, _, lhsKey, _, _, _): + if case let .message(_, _, _, _, _, _, _, rhsKey, _, _, _) = rhs { return lhsKey < rhsKey } else if case .addContact = rhs { return true @@ -394,7 +400,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } } - 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, String?) -> Void)?, openStorageSettings: @escaping () -> 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 @@ -542,12 +548,20 @@ public enum ChatListSearchEntry: Comparable, Identifiable { peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture) } }) - case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader, orderingKey, _, _): + case let .message(message, peer, readState, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused): let header: ChatListSearchItemHeader switch orderingKey { case .downloading: //TODO:localize - header = ChatListSearchItemHeader(type: .downloading, theme: presentationData.theme, strings: presentationData.strings, actionTitle: "Pause All", action: {}) + 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: { @@ -590,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 @@ -614,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, 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, String?) -> Void)?, openStorageSettings: @escaping () -> 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, key: key, tagMask: tagMask, interaction: interaction, listInteraction: listInteraction, peerContextAction: peerContextAction, toggleExpandLocalResults: toggleExpandLocalResults, toggleExpandGlobalResults: toggleExpandGlobalResults, searchPeer: searchPeer, searchQuery: searchQuery, searchOptions: searchOptions, messageContextAction: messageContextAction, openStorageSettings: openStorageSettings), 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), 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) } @@ -695,6 +709,7 @@ 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 { @@ -706,6 +721,9 @@ private struct DownloadItem: Equatable { if lhs.priority != rhs.priority { return false } + if lhs.isPaused != rhs.isPaused { + return false + } return true } } @@ -918,7 +936,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { 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 DownloadItem(resourceId: entry.resourceReference.resource.id, message: message, priority: entry.priority, isPaused: entry.isPaused) } return nil }) @@ -967,7 +985,16 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { |> map { presentationData -> ([ChatListSearchEntry], Bool)? in var entries: [ChatListSearchEntry] = [] var existingMessageIds = Set() + + 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 } @@ -987,9 +1014,15 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { peer = EngineRenderedPeer(peer: EnginePeer(channelPeer)) } } - entries.append(.message(message, peer, nil, presentationData, 1, nil, false, .downloading(item.priority), item.resourceId.stringRepresentation, .downloading)) + + 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 { + 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() @@ -1014,7 +1047,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { peer = EngineRenderedPeer(peer: EnginePeer(channelPeer)) } } - entries.append(.message(message, peer, nil, presentationData, 1, nil, false, .downloaded(timestamp: item.timestamp, index: message.index), item.resourceId, .recentlyDownloaded)) + 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) } @@ -1310,7 +1343,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { peer = EngineRenderedPeer(peer: EnginePeer(channelPeer)) } } - entries.append(.message(message, peer, nil, presentationData, 1, nil, true, .index(message.index), nil, .generic)) + entries.append(.message(message, peer, nil, presentationData, 1, nil, true, .index(message.index), nil, .generic, false)) index += 1 } @@ -1333,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, .index(message.index), nil, .generic)) + 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 } } @@ -1501,12 +1534,12 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return } - var fetchResourceId: String? + var fetchResourceId: (id: String, size: Int, isFirstInList: Bool)? for entry in currentEntries { switch entry { - case let .message(m, _, _, _, _, _, _, _, resourceId, _): + case let .message(m, _, _, _, _, _, _, _, resource, _, _): if m.id == message.id { - fetchResourceId = resourceId + fetchResourceId = resource } default: break @@ -1606,20 +1639,46 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { return state } }, searchPeer: { peer in - }, searchQuery: strongSelf.searchQueryValue, searchOptions: strongSelf.searchOptionsValue, messageContextAction: { message, node, rect, gesture, paneKey, downloadResourceId in - interaction.messageContextAction(message, node, rect, gesture, paneKey, downloadResourceId) + }, 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) } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift index 2c7854862d..5494b6d70b 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift @@ -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 diff --git a/submodules/CheckNode/Sources/CheckNode.swift b/submodules/CheckNode/Sources/CheckNode.swift index ac23bcfb93..8e4c61fcf6 100644 --- a/submodules/CheckNode/Sources/CheckNode.swift +++ b/submodules/CheckNode/Sources/CheckNode.swift @@ -291,13 +291,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 diff --git a/submodules/ComponentFlow/Source/Components/LazyList.swift b/submodules/ComponentFlow/Source/Components/LazyList.swift new file mode 100644 index 0000000000..e0f73bbae7 --- /dev/null +++ b/submodules/ComponentFlow/Source/Components/LazyList.swift @@ -0,0 +1,3 @@ +import Foundation +import UIKit + diff --git a/submodules/ComponentFlow/Source/Components/ZStack.swift b/submodules/ComponentFlow/Source/Components/ZStack.swift new file mode 100644 index 0000000000..c79ef1a937 --- /dev/null +++ b/submodules/ComponentFlow/Source/Components/ZStack.swift @@ -0,0 +1,49 @@ +import Foundation +import UIKit + +public final class ZStack: CombinedComponent { + public typealias EnvironmentType = ChildEnvironment + + private let items: [AnyComponentWithIdentity] + + public init(_ items: [AnyComponentWithIdentity]) { + self.items = items + } + + public static func ==(lhs: ZStack, rhs: ZStack) -> 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 + } + } +} diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index ec9759559f..e5b90f72b0 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -64,7 +64,7 @@ public final class LottieAnimationComponent: Component { view.backgroundColor = .clear view.isOpaque = false - view.logHierarchyKeypaths() + //view.logHierarchyKeypaths() for (key, value) in component.animation.colors { let colorCallback = LOTColorValueCallback(color: value.cgColor) diff --git a/submodules/Components/ProgressIndicatorComponent/BUILD b/submodules/Components/ProgressIndicatorComponent/BUILD new file mode 100644 index 0000000000..df967f4f4f --- /dev/null +++ b/submodules/Components/ProgressIndicatorComponent/BUILD @@ -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", + ], +) diff --git a/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift b/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift new file mode 100644 index 0000000000..b942ac6926 --- /dev/null +++ b/submodules/Components/ProgressIndicatorComponent/Sources/ProgressIndicatorComponent.swift @@ -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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 5715148260..ad1d2f8303 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -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() diff --git a/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift b/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift index 3fcb4a7b25..d12be7953c 100644 --- a/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift +++ b/submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift @@ -91,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 @@ -98,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 @@ -251,48 +256,19 @@ 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) - } - } - } - }*/ } } @@ -303,6 +279,9 @@ private final class FetchManagerCategoryContext { 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 { @@ -409,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 { @@ -434,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) @@ -464,6 +453,87 @@ private final class FetchManagerCategoryContext { 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) { let statusContext: FetchManagerStatusContext if let current = self.statusContexts[id] { @@ -471,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 } } @@ -494,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 { @@ -505,6 +577,7 @@ private extension FetchManagerEntrySummary { self.mediaReference = entry.mediaReference self.resourceReference = entry.resourceReference self.priority = priority + self.isPaused = entry.isPaused } } @@ -610,7 +683,9 @@ public final class FetchManagerImpl: FetchManager { } strongSelf.hasUserInitiatedEntriesValue.set(hasActiveUserInitiatedEntries) - strongSelf.entriesSummaryValue.set(activeEntries.sorted(by: { $0.priority < $1.priority })) + let sortedEntries = activeEntries.sorted(by: { $0.priority < $1.priority }) + + strongSelf.entriesSummaryValue.set(sortedEntries) } }) self.categoryContexts[key] = context @@ -629,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? @@ -639,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 } @@ -647,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() } @@ -679,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() } - } + }*/ } }) }) @@ -708,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 { let queue = self.queue return Signal { [weak self] subscriber in diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 068ab0b8a3..c96f9227da 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -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) } diff --git a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift index 386a27af17..cb8cb9d308 100644 --- a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift @@ -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 diff --git a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift index 4379cf5aca..b44a9336c5 100644 --- a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift @@ -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 diff --git a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift index bd7f697d09..aab16e3fa9 100644 --- a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift @@ -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 diff --git a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift index b9da1334b7..4f8de99440 100644 --- a/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift @@ -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 diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 63137ec516..e37bb44121 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -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 { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index ec24779e52..537e40c918 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -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, .index($0.index), nil, .generic) }) + 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 @@ -98,7 +98,7 @@ public final class HashtagSearchController: TelegramBaseController { 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, openStorageSettings: {}) + }, searchQuery: "", searchOptions: nil, messageContextAction: nil, openStorageSettings: {}, toggleAllPaused: {}) strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime) } }) diff --git a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift index 559bf1e7f5..f7ceffea21 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift @@ -299,7 +299,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { default: break } - case .Remote: + case .Remote, .Paused: if case .tap = gesture { self.fetchControls?.fetch(true) } diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift index ddf4c72a5d..86b0921dce 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift @@ -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() diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 57b0b3a08e..0fc90e78f8 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -607,9 +607,17 @@ 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) + } } }) } @@ -841,7 +849,7 @@ public final class ListMessageFileItemNode: ListMessageNode { }) } - transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel))) + transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel))) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel)) if let backgroundNode = strongSelf.backgroundNode { @@ -858,7 +866,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 @@ -997,7 +1005,7 @@ public final class ListMessageFileItemNode: ListMessageNode { if isAudio || isInstantVideo { iconStatusState = .play } - case .Remote: + case .Remote, .Paused: if isAudio || isInstantVideo { iconStatusState = .play } @@ -1083,7 +1091,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)))" } @@ -1096,8 +1104,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 @@ -1116,7 +1124,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 { @@ -1197,7 +1209,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() } @@ -1237,7 +1249,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() } diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 0d7e667934..619b54c7c6 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -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 diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 11de411af0..5d0d6ea91f 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -409,7 +409,7 @@ public final class MediaBox { } 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 @@ -783,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() } @@ -1363,15 +1367,17 @@ public final class MediaBox { if notify { for id in ids { if let context = self.statusContexts[id] { - context.status = .Remote + context.status = .Remote(progress: 0.0) for f in context.subscribers.copyItems() { - f(.Remote) + f(.Remote(progress: 0.0)) } } } } - self.didRemoveResourcesPipe.putNext(Void()) + self.dataQueue.justDispatch { + self.didRemoveResourcesPipe.putNext(Void()) + } subscriber.putCompletion() } diff --git a/submodules/Postbox/Sources/MediaBoxFile.swift b/submodules/Postbox/Sources/MediaBoxFile.swift index 66259bdc81..c9871455e3 100644 --- a/submodules/Postbox/Sources/MediaBoxFile.swift +++ b/submodules/Postbox/Sources/MediaBoxFile.swift @@ -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 diff --git a/submodules/Postbox/Sources/MediaResourceStatus.swift b/submodules/Postbox/Sources/MediaResourceStatus.swift index 189a992326..33c8a8cdd9 100644 --- a/submodules/Postbox/Sources/MediaResourceStatus.swift +++ b/submodules/Postbox/Sources/MediaResourceStatus.swift @@ -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) } diff --git a/submodules/Postbox/Sources/OrderedItemListTable.swift b/submodules/Postbox/Sources/OrderedItemListTable.swift index 0754986fd3..a42f716082 100644 --- a/submodules/Postbox/Sources/OrderedItemListTable.swift +++ b/submodules/Postbox/Sources/OrderedItemListTable.swift @@ -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 diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 582210fafd..9d5681fa61 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -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() }) }) diff --git a/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift b/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift index 3defd26afc..b9d5eb9af7 100644 --- a/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift +++ b/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift @@ -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 diff --git a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift index 8d092ee8df..9b557c036d 100644 --- a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift @@ -127,7 +127,10 @@ public class SearchBarPlaceholderNode: ASDisplayNode { 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.removeFromSuperview() + 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() + }) } } diff --git a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift index 25f5f63d6b..3fa36563e1 100644 --- a/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift +++ b/submodules/SettingsUI/Sources/Themes/WallpaperGalleryItem.swift @@ -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: {}) diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 0649f310eb..b50ce121fb 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index 065ce3e7ab..e5d3e9943a 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentDownloadItem.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentDownloadItem.swift index 819fef3af1..f8a57864bc 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentDownloadItem.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentDownloadItem.swift @@ -83,12 +83,14 @@ public final class RenderedRecentDownloadItem: Equatable { 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) { + 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 { @@ -107,6 +109,9 @@ public final class RenderedRecentDownloadItem: Equatable { if lhs.resourceId != rhs.resourceId { return false } + if lhs.size != rhs.size { + return false + } return true } } @@ -129,7 +134,18 @@ public func recentDownloadItems(postbox: Postbox) -> Signal<[RenderedRecentDownl guard let message = transaction.getMessage(item.messageId) else { continue } - result.append(RenderedRecentDownloadItem(message: message, timestamp: item.timestamp, isSeen: item.isSeen, resourceId: item.resourceId)) + + 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 @@ -138,7 +154,7 @@ public func recentDownloadItems(postbox: Postbox) -> Signal<[RenderedRecentDownl var statusSignals: [Signal] = [] for item in items { - statusSignals.append(postbox.mediaBox.resourceStatus(MediaResourceId(item.resourceId), resourceSize: nil) + statusSignals.append(postbox.mediaBox.resourceStatus(MediaResourceId(item.resourceId), resourceSize: item.size) |> map { status -> Bool in switch status { case .Local: @@ -174,6 +190,41 @@ public func addRecentDownloadItem(postbox: Postbox, item: RecentDownloadItem) -> |> ignoreValues } +public func markRecentDownloadItemsAsSeen(postbox: Postbox, items: [(messageId: MessageId, resourceId: String)]) -> Signal { + 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 { return postbox.transaction { transaction -> Void in let items = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.RecentDownloads) diff --git a/submodules/TelegramUI/Resources/Animations/anim_search_downloaded.json b/submodules/TelegramUI/Resources/Animations/anim_search_downloaded.json index 4ec20c5474..03885f36d5 100644 --- a/submodules/TelegramUI/Resources/Animations/anim_search_downloaded.json +++ b/submodules/TelegramUI/Resources/Animations/anim_search_downloaded.json @@ -1 +1 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":30,"w":24,"h":24,"nm":"ic_downloaded","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Oval","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[16.667,16.667,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[18.333,18.333,100]},{"t":30,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Fill 2","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[0,0,100]},{"t":20,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arrow2","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[12,0,0],"to":[0,2,0],"ti":[0,-2,0]},{"t":30,"s":[12,12,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.367,0],[0,-0.367],[0,0],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0.26,0.26],[0,0],[-0.26,0.26],[-0.26,-0.26],[0,0],[0,0]],"o":[[0.367,0],[0,0],[0,0],[0.26,-0.26],[0.26,0.26],[0,0],[-0.26,0.26],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0,0],[0,-0.367]],"v":[[0,-4.165],[0.665,-3.5],[0.665,1.895],[2.53,0.03],[3.47,0.03],[3.47,0.97],[0.47,3.97],[-0.47,3.97],[-3.47,0.97],[-3.47,0.03],[-2.53,0.03],[-0.665,1.895],[-0.665,-3.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Union","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Mask2","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Arrow3","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[12,0,0],"to":[0,2,0],"ti":[0,-2,0]},{"t":30,"s":[12,12,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.367,0],[0,-0.367],[0,0],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0.26,0.26],[0,0],[-0.26,0.26],[-0.26,-0.26],[0,0],[0,0]],"o":[[0.367,0],[0,0],[0,0],[0.26,-0.26],[0.26,0.26],[0,0],[-0.26,0.26],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0,0],[0,-0.367]],"v":[[0,-4.165],[0.665,-3.5],[0.665,1.895],[2.53,0.03],[3.47,0.03],[3.47,0.97],[0.47,3.97],[-0.47,3.97],[-3.47,0.97],[-3.47,0.03],[-2.53,0.03],[-0.665,1.895],[-0.665,-3.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Union","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Fill","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[0,0,100]},{"t":20,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Mask1","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Arrow1","parent":3,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,72,0],"to":[0,12,0],"ti":[0,-12,0]},{"t":30,"s":[0,144,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.367,0],[0,-0.367],[0,0],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0.26,0.26],[0,0],[-0.26,0.26],[-0.26,-0.26],[0,0],[0,0]],"o":[[0.367,0],[0,0],[0,0],[0.26,-0.26],[0.26,0.26],[0,0],[-0.26,0.26],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0,0],[0,-0.367]],"v":[[0,-4.165],[0.665,-3.5],[0.665,1.895],[2.53,0.03],[3.47,0.03],[3.47,0.97],[0.47,3.97],[-0.47,3.97],[-3.47,0.97],[-3.47,0.03],[-2.53,0.03],[-0.665,1.895],[-0.665,-3.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Union","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0}],"markers":[]} \ No newline at end of file +{"v":"5.8.1","fr":60,"ip":0,"op":30,"w":24,"h":24,"nm":"ic_downloaded","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Oval","sr":1,"ks":{"o":{"a":0,"k":15,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[16.667,16.667,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[18.333,18.333,100]},{"t":30,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Fill 2","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[0,0,100]},{"t":20,"s":[108,108,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arrow2","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[12,0,0],"to":[0,2,0],"ti":[0,-2,0]},{"t":30,"s":[12,12,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.367,0],[0,-0.367],[0,0],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0.26,0.26],[0,0],[-0.26,0.26],[-0.26,-0.26],[0,0],[0,0]],"o":[[0.367,0],[0,0],[0,0],[0.26,-0.26],[0.26,0.26],[0,0],[-0.26,0.26],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0,0],[0,-0.367]],"v":[[0,-4.165],[0.665,-3.5],[0.665,1.895],[2.53,0.03],[3.47,0.03],[3.47,0.97],[0.47,3.97],[-0.47,3.97],[-3.47,0.97],[-3.47,0.03],[-2.53,0.03],[-0.665,1.895],[-0.665,-3.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Union","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Mask2","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[92,92,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Arrow3","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[12,0,0],"to":[0,2,0],"ti":[0,-2,0]},{"t":30,"s":[12,12,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.367,0],[0,-0.367],[0,0],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0.26,0.26],[0,0],[-0.26,0.26],[-0.26,-0.26],[0,0],[0,0]],"o":[[0.367,0],[0,0],[0,0],[0.26,-0.26],[0.26,0.26],[0,0],[-0.26,0.26],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0,0],[0,-0.367]],"v":[[0,-4.165],[0.665,-3.5],[0.665,1.895],[2.53,0.03],[3.47,0.03],[3.47,0.97],[0.47,3.97],[-0.47,3.97],[-3.47,0.97],[-3.47,0.03],[-2.53,0.03],[-0.665,1.895],[-0.665,-3.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Union","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Fill","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[0,0,100]},{"t":20,"s":[108,108,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Mask1","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[92,92,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Arrow1","parent":3,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,72,0],"to":[0,12,0],"ti":[0,-12,0]},{"t":30,"s":[0,144,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.367,0],[0,-0.367],[0,0],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0.26,0.26],[0,0],[-0.26,0.26],[-0.26,-0.26],[0,0],[0,0]],"o":[[0.367,0],[0,0],[0,0],[0.26,-0.26],[0.26,0.26],[0,0],[-0.26,0.26],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0,0],[0,-0.367]],"v":[[0,-4.165],[0.665,-3.5],[0.665,1.895],[2.53,0.03],[3.47,0.03],[3.47,0.97],[0.47,3.97],[-0.47,3.97],[-3.47,0.97],[-3.47,0.03],[-2.53,0.03],[-0.665,1.895],[-0.665,-3.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Union","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_search_downloading.json b/submodules/TelegramUI/Resources/Animations/anim_search_downloading.json index 2768eb001e..4d64a95a2b 100644 --- a/submodules/TelegramUI/Resources/Animations/anim_search_downloading.json +++ b/submodules/TelegramUI/Resources/Animations/anim_search_downloading.json @@ -1 +1 @@ -{"v":"5.8.1","fr":60,"ip":0,"op":30,"w":24,"h":24,"nm":"ic_downloading","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Oval","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Mask2","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arrow2","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[12,0,0],"to":[0,2,0],"ti":[0,-2,0]},{"t":30,"s":[12,12,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.367,0],[0,-0.367],[0,0],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0.26,0.26],[0,0],[-0.26,0.26],[-0.26,-0.26],[0,0],[0,0]],"o":[[0.367,0],[0,0],[0,0],[0.26,-0.26],[0.26,0.26],[0,0],[-0.26,0.26],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0,0],[0,-0.367]],"v":[[0,-4.165],[0.665,-3.5],[0.665,1.895],[2.53,0.03],[3.47,0.03],[3.47,0.97],[0.47,3.97],[-0.47,3.97],[-3.47,0.97],[-3.47,0.03],[-2.53,0.03],[-0.665,1.895],[-0.665,-3.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Union","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Mask1","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Arrow1","parent":3,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,72,0],"to":[0,12,0],"ti":[0,-12,0]},{"t":30,"s":[0,144,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.367,0],[0,-0.367],[0,0],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0.26,0.26],[0,0],[-0.26,0.26],[-0.26,-0.26],[0,0],[0,0]],"o":[[0.367,0],[0,0],[0,0],[0.26,-0.26],[0.26,0.26],[0,0],[-0.26,0.26],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0,0],[0,-0.367]],"v":[[0,-4.165],[0.665,-3.5],[0.665,1.895],[2.53,0.03],[3.47,0.03],[3.47,0.97],[0.47,3.97],[-0.47,3.97],[-3.47,0.97],[-3.47,0.03],[-2.53,0.03],[-0.665,1.895],[-0.665,-3.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Union","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0}],"markers":[]} \ No newline at end of file +{"v":"5.8.1","fr":60,"ip":0,"op":30,"w":24,"h":24,"nm":"ic_downloading","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Oval","sr":1,"ks":{"o":{"a":0,"k":15,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Mask2","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[92,92,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arrow2","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[12,0,0],"to":[0,2,0],"ti":[0,-2,0]},{"t":30,"s":[12,12,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.367,0],[0,-0.367],[0,0],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0.26,0.26],[0,0],[-0.26,0.26],[-0.26,-0.26],[0,0],[0,0]],"o":[[0.367,0],[0,0],[0,0],[0.26,-0.26],[0.26,0.26],[0,0],[-0.26,0.26],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0,0],[0,-0.367]],"v":[[0,-4.165],[0.665,-3.5],[0.665,1.895],[2.53,0.03],[3.47,0.03],[3.47,0.97],[0.47,3.97],[-0.47,3.97],[-3.47,0.97],[-3.47,0.03],[-2.53,0.03],[-0.665,1.895],[-0.665,-3.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Union","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Mask1","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[92,92,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Arrow1","parent":3,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,72,0],"to":[0,12,0],"ti":[0,-12,0]},{"t":30,"s":[0,144,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.367,0],[0,-0.367],[0,0],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0.26,0.26],[0,0],[-0.26,0.26],[-0.26,-0.26],[0,0],[0,0]],"o":[[0.367,0],[0,0],[0,0],[0.26,-0.26],[0.26,0.26],[0,0],[-0.26,0.26],[0,0],[-0.26,-0.26],[0.26,-0.26],[0,0],[0,0],[0,-0.367]],"v":[[0,-4.165],[0.665,-3.5],[0.665,1.895],[2.53,0.03],[3.47,0.03],[3.47,0.97],[0.47,3.97],[-0.47,3.97],[-3.47,0.97],[-3.47,0.03],[-2.53,0.03],[-0.665,1.895],[-0.665,-3.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Union","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":31,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b7419b80eb..7a3ed8b3d2 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -676,7 +676,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 diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index d9cd232219..0b902a0b54 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift index 1fd6b23263..85bd993b9e 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveFileNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift index 411a175a66..e447871341 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -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()) } diff --git a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift index 02a58fbcb9..14b6960db0 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInteractiveMediaNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/GridMessageItem.swift b/submodules/TelegramUI/Sources/GridMessageItem.swift index e8c3a52717..b5aa53490b 100644 --- a/submodules/TelegramUI/Sources/GridMessageItem.swift +++ b/submodules/TelegramUI/Sources/GridMessageItem.swift @@ -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()) } } diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index 6d75431d50..4b4bb489e9 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -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: diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index 04f2464ecb..726a0a5d43 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 6cf0ec57a5..1fb69b8e8c 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -4113,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 { [weak self] subscriber in @@ -4129,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() diff --git a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift index 16367af592..86728907f7 100644 --- a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift index 7cc0ce5527..02a03e6dc8 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift @@ -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: { }) } - } }) }