diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 61063982c6..cdbdbcf3c1 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -323,6 +323,7 @@ D05D8B742195CD890064586F /* SetupTwoStepVerificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05D8B732195CD890064586F /* SetupTwoStepVerificationController.swift */; }; D05D8B762195CD930064586F /* SetupTwoStepVerificationControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05D8B752195CD930064586F /* SetupTwoStepVerificationControllerNode.swift */; }; D05D8B782195E0050064586F /* SetupTwoStepVerificationContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05D8B772195E0050064586F /* SetupTwoStepVerificationContentNode.swift */; }; + D06350AE2229A7F800FA2B32 /* InChatPrefetchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06350AD2229A7F800FA2B32 /* InChatPrefetchManager.swift */; }; D0642EFC1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0642EFB1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift */; }; D064EF871F69A06F00AC0398 /* MessageContentKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = D064EF861F69A06F00AC0398 /* MessageContentKind.swift */; }; D0671F232143BDA6000A8AE7 /* TwoStepVerificationEmptyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0671F222143BDA6000A8AE7 /* TwoStepVerificationEmptyItem.swift */; }; @@ -1701,6 +1702,7 @@ D0613FC71E5F8AB100202CDB /* ChannelInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoController.swift; sourceTree = ""; }; D0613FCC1E60482300202CDB /* ChannelMembersController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMembersController.swift; sourceTree = ""; }; D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertToSupergroupController.swift; sourceTree = ""; }; + D06350AD2229A7F800FA2B32 /* InChatPrefetchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InChatPrefetchManager.swift; sourceTree = ""; }; D0642EFB1F3E1E7B00792790 /* ChatHistoryNavigationButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryNavigationButtons.swift; sourceTree = ""; }; D064EF861F69A06F00AC0398 /* MessageContentKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContentKind.swift; sourceTree = ""; }; D0671F222143BDA6000A8AE7 /* TwoStepVerificationEmptyItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationEmptyItem.swift; sourceTree = ""; }; @@ -4521,6 +4523,7 @@ D0F69E441D6B8B850046BCD6 /* History Navigation */, D044A0FA20BDC40C00326FAC /* CachedChannelAdmins.swift */, D01848E721A03BDA00B6DEBD /* ChatSearchState.swift */, + D06350AD2229A7F800FA2B32 /* InChatPrefetchManager.swift */, ); name = Chat; sourceTree = ""; @@ -5816,6 +5819,7 @@ D0477D1B1F617E5800412B44 /* UniversalVideoNode.swift in Sources */, D0E9BA081F0446A300F079A4 /* BotCheckoutPaymentShippingOptionSheetController.swift in Sources */, D0EC6DDC1EB9F58900EBF1C3 /* ChatTextInputPanelNode.swift in Sources */, + D06350AE2229A7F800FA2B32 /* InChatPrefetchManager.swift in Sources */, D0EB41F51F30D26A00838FE6 /* LegacySuggestionContext.swift in Sources */, D0EC6DDD1EB9F58900EBF1C3 /* ChatTextInputMediaRecordingButton.swift in Sources */, D0F0AAE61EC21B68005EE2A5 /* CallControllerButton.swift in Sources */, diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 685c282427..745d97850d 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -1544,7 +1544,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal strongSelf.automaticMediaDownloadSettings = downloadSettings strongSelf.controllerInteraction?.automaticMediaDownloadSettings = downloadSettings if strongSelf.isNodeLoaded { - strongSelf.chatDisplayNode.updateAutomaticMediaDownloadSettings() + strongSelf.chatDisplayNode.updateAutomaticMediaDownloadSettings(downloadSettings) } } }) @@ -1964,7 +1964,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal stationaryItemRange = (maxInsertedItem + 1, Int.max) } - mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, animateIn: false), updateSizeAndInsets) + mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, peerType: transition.peerType, networkType: transition.networkType, animateIn: false), updateSizeAndInsets) }) if let mappedTransition = mappedTransition { diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index fabc082786..e557c20620 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -1474,12 +1474,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - func updateAutomaticMediaDownloadSettings() { + func updateAutomaticMediaDownloadSettings(_ settings: MediaAutoDownloadSettings) { self.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { itemNode.updateAutomaticMediaDownloadSettings() } } + self.historyNode.prefetchManager.updateAutoDownloadSettings(settings) } func playFirstMediaWithSound() { diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index 11f74bb3cd..c6ebc94a09 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -118,6 +118,8 @@ struct ChatHistoryListViewTransition { let cachedDataMessages: [MessageId: Message]? let readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? let scrolledToIndex: MessageHistoryAnchorIndex? + let peerType: MediaAutoDownloadPeerType + let networkType: MediaAutoDownloadNetworkType let animateIn: Bool } @@ -252,7 +254,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca } private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, animateIn: transition.animateIn) + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn) } private final class ChatHistoryTransactionOpaqueState { @@ -363,6 +365,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { private let messageProcessingManager = ChatMessageThrottledProcessingManager() private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager() private let messageMentionProcessingManager = ChatMessageThrottledProcessingManager(delay: 0.2) + let prefetchManager: InChatPrefetchManager + private var currentEarlierPrefetchMessages: [(Message, Media)] = [] + private var currentLaterPrefetchMessages: [(Message, Media)] = [] + private var currentPrefetchDirectionIsToLater: Bool = true private var maxVisibleMessageIndexReported: MessageIndex? var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)? @@ -416,6 +422,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.chatPresentationDataPromise = Promise(ChatPresentationData(theme: ChatPresentationThemeData(theme: self.currentPresentationData.theme, wallpaper: self.currentPresentationData.chatWallpaper), fontSize: self.currentPresentationData.fontSize, strings: self.currentPresentationData.strings, dateTimeFormat: self.currentPresentationData.dateTimeFormat, nameDisplayOrder: self.currentPresentationData.nameDisplayOrder, disableAnimations: self.currentPresentationData.disableAnimations)) + self.prefetchManager = InChatPrefetchManager(context: context) + super.init() self.dynamicBounceEnabled = !self.currentPresentationData.disableAnimations @@ -632,6 +640,21 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.chatHistoryLocation.set(ChatHistoryLocationInput(content: .Initial(count: 60), id: 0)) } + self.generalScrollDirectionUpdated = { [weak self] direction in + guard let strongSelf = self else { + return + } + let prefetchDirectionIsToLater = direction == .up + if strongSelf.currentPrefetchDirectionIsToLater != prefetchDirectionIsToLater { + strongSelf.currentPrefetchDirectionIsToLater = prefetchDirectionIsToLater + if strongSelf.currentPrefetchDirectionIsToLater { + strongSelf.prefetchManager.updateMessages(strongSelf.currentLaterPrefetchMessages, directionIsToLater: strongSelf.currentPrefetchDirectionIsToLater) + } else { + strongSelf.prefetchManager.updateMessages(strongSelf.currentEarlierPrefetchMessages, directionIsToLater: strongSelf.currentPrefetchDirectionIsToLater) + } + } + } + self.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in if let strongSelf = self { if let historyView = (opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView { @@ -639,13 +662,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex) let readIndexRange = (0, historyView.filteredEntries.count - 1 - visible.firstIndex) - /*if !visible.firstIndexFullyVisible { - readIndexRange.1 -= 1 - }*/ + + let toEarlierRange = (0, historyView.filteredEntries.count - 1 - visible.firstIndex - 1) + let toLaterRange = (historyView.filteredEntries.count - 1 - visible.lastIndex + 1, historyView.filteredEntries.count - 1) var messageIdsWithViewCount: [MessageId] = [] var messageIdsWithUnsupportedMedia: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = [] + var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = [] + var messagesWithPreloadableMediaToLater: [(Message, Media)] = [] + for i in (indexRange.0 ... indexRange.1) { switch historyView.filteredEntries[i] { case let .MessageEntry(message, _, _, _, _, _): @@ -704,6 +730,69 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } + func addMediaToPrefetch(_ message: Message, _ media: Media, _ messages: inout [(Message, Media)]) -> Bool { + if media is TelegramMediaImage || media is TelegramMediaFile { + messages.append((message, media)) + } + if messages.count >= 3 { + return false + } else { + return true + } + } + + var toEarlierMediaMessages: [(Message, Media)] = [] + if toEarlierRange.0 <= toEarlierRange.1 { + outer: for i in (toEarlierRange.0 ... toEarlierRange.1).reversed() { + switch historyView.filteredEntries[i] { + case let .MessageEntry(message, _, _, _, _, _): + for media in message.media { + if !addMediaToPrefetch(message, media, &toEarlierMediaMessages) { + break outer + } + } + case let .MessageGroupEntry(_, messages, _): + for (message, _, _, _) in messages { + var stop = false + for media in message.media { + if !addMediaToPrefetch(message, media, &toEarlierMediaMessages) { + stop = true + } + } + if stop { + break outer + } + } + default: + break + } + } + } + + var toLaterMediaMessages: [(Message, Media)] = [] + if toLaterRange.0 <= toLaterRange.1 { + outer: for i in (toLaterRange.0 ... toLaterRange.1) { + switch historyView.filteredEntries[i] { + case let .MessageEntry(message, _, _, _, _, _): + for media in message.media { + if !addMediaToPrefetch(message, media, &toLaterMediaMessages) { + break outer + } + } + case let .MessageGroupEntry(_, messages, _): + for (message, _, _, _) in messages { + for media in message.media { + if !addMediaToPrefetch(message, media, &toLaterMediaMessages) { + break outer + } + } + } + default: + break + } + } + } + if !messageIdsWithViewCount.isEmpty { strongSelf.messageProcessingManager.add(messageIdsWithViewCount) } @@ -714,6 +803,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention) } + strongSelf.currentEarlierPrefetchMessages = toEarlierMediaMessages + strongSelf.currentLaterPrefetchMessages = toLaterMediaMessages + if strongSelf.currentPrefetchDirectionIsToLater { + strongSelf.prefetchManager.updateMessages(toLaterMediaMessages, directionIsToLater: strongSelf.currentPrefetchDirectionIsToLater) + } else { + strongSelf.prefetchManager.updateMessages(toEarlierMediaMessages, directionIsToLater: strongSelf.currentPrefetchDirectionIsToLater) + } + if readIndexRange.0 <= readIndexRange.1 { let (maxIncomingIndex, maxOverallIndex) = maxMessageIndexForEntries(historyView, indexRange: readIndexRange) @@ -978,6 +1075,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { preconditionFailure() } + strongSelf.prefetchManager.updateOptions(InChatPrefetchOptions(networkType: transition.networkType, peerType: transition.peerType)) + if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData))) diff --git a/TelegramUI/FetchManager.swift b/TelegramUI/FetchManager.swift index 95752615aa..e87d2001a1 100644 --- a/TelegramUI/FetchManager.swift +++ b/TelegramUI/FetchManager.swift @@ -27,9 +27,15 @@ private struct FetchManagerLocationEntryId: Hashable { } } +enum FetchManagerForegroundDirection { + case toEarlier + case toLater +} + enum FetchManagerPriority: Comparable { case userInitiated - case backgroundPrefetch(MessageIndex) + case foregroundPrefetch(direction: FetchManagerForegroundDirection, localOrder: MessageIndex) + case backgroundPrefetch(locationOrder: HistoryPreloadIndex, localOrder: MessageIndex) static func <(lhs: FetchManagerPriority, rhs: FetchManagerPriority) -> Bool { switch lhs { @@ -37,15 +43,44 @@ enum FetchManagerPriority: Comparable { switch rhs { case .userInitiated: return false + case .foregroundPrefetch: + return true case .backgroundPrefetch: return true } - case let .backgroundPrefetch(lhsIndex): + case let .foregroundPrefetch(lhsDirection, lhsLocalOrder): switch rhs { case .userInitiated: return false - case let .backgroundPrefetch(rhsIndex): - return lhsIndex > rhsIndex + case let .foregroundPrefetch(rhsDirection, rhsLocalOrder): + if lhsDirection == rhsDirection { + switch lhsDirection { + case .toEarlier: + return lhsLocalOrder > rhsLocalOrder + case .toLater: + return lhsLocalOrder < rhsLocalOrder + } + } else { + if lhsDirection == .toEarlier { + return true + } else { + return false + } + } + case .backgroundPrefetch: + return true + } + case let .backgroundPrefetch(lhsLocationOrder, lhsLocalOrder): + switch rhs { + case .userInitiated: + return false + case .foregroundPrefetch: + return false + case let .backgroundPrefetch(rhsLocationOrder, rhsLocalOrder): + if lhsLocationOrder != rhsLocationOrder { + return lhsLocationOrder < rhsLocationOrder + } + return lhsLocalOrder > rhsLocalOrder } } } @@ -231,6 +266,7 @@ private final class FetchManagerCategoryContext { } parsedRanges = resultRanges } + activeContext.disposable?.dispose() activeContext.disposable = (fetchedMediaResource(postbox: self.postbox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated) |> mapToSignal { type -> Signal in if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType { @@ -307,14 +343,22 @@ private final class FetchManagerCategoryContext { self.topEntryIdAndPriority = topEntryIdAndPriority } - if let topEntryId = self.topEntryIdAndPriority?.0, self.activeContexts[topEntryId] == nil { + if let topEntryId = self.topEntryIdAndPriority?.0 { if let entry = self.entries[topEntryId] { - let activeContext = FetchManagerActiveContext(userInitiated: entry.userInitiated) let ranges = entry.combinedRanges - activeContext.ranges = ranges let parsedRanges: [(Range, MediaBoxFetchPriority)]? - if ranges.count == 1 && ranges.min() == 0 && ranges.max() == Int(Int32.max) { + + var count = 0 + var isCompleteRange = false + for range in ranges.rangeView { + count += 1 + if range.lowerBound == 0 && range.upperBound == Int(Int32.max) { + isCompleteRange = true + } + } + + if count == 1 && isCompleteRange { parsedRanges = nil } else { var resultRanges: [(Range, MediaBoxFetchPriority)] = [] @@ -324,26 +368,45 @@ private final class FetchManagerCategoryContext { parsedRanges = resultRanges } - self.activeContexts[topEntryId] = activeContext - let entryCompleted = self.entryCompleted - let storeManager = self.storeManager - activeContext.disposable = (fetchedMediaResource(postbox: self.postbox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated) - |> mapToSignal { type -> Signal in - if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType { - return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType) - |> introduceError(FetchResourceError.self) - |> mapToSignal { _ -> Signal in - return .complete() - } - |> then(.single(type)) - } - return .single(type) + let activeContext: FetchManagerActiveContext + var restart = false + if let current = self.activeContexts[topEntryId] { + activeContext = current + restart = activeContext.ranges != ranges + } else { + activeContext = FetchManagerActiveContext(userInitiated: entry.userInitiated) + self.activeContexts[topEntryId] = activeContext + restart = true } - |> deliverOnMainQueue).start(next: { _ in - entryCompleted(topEntryId) + + if restart { + activeContext.ranges = ranges - }) - return true + let entryCompleted = self.entryCompleted + let storeManager = self.storeManager + activeContext.disposable?.dispose() + if ranges.isEmpty { + } else { + activeContext.disposable = (fetchedMediaResource(postbox: self.postbox, reference: entry.resourceReference, ranges: parsedRanges, statsCategory: entry.statsCategory, reportResultStatus: true, continueInBackground: entry.userInitiated) + |> mapToSignal { type -> Signal in + if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType { + return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType) + |> introduceError(FetchResourceError.self) + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(type)) + } + return .single(type) + } + |> deliverOnMainQueue).start(next: { _ in + entryCompleted(topEntryId) + }) + } + return true + } else { + return false + } } else { assertionFailure() return false diff --git a/TelegramUI/InChatPrefetchManager.swift b/TelegramUI/InChatPrefetchManager.swift new file mode 100644 index 0000000000..d560ac1bb7 --- /dev/null +++ b/TelegramUI/InChatPrefetchManager.swift @@ -0,0 +1,138 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class PrefetchMediaContext { + let fetchDisposable = MetaDisposable() + + init() { + } +} + +struct InChatPrefetchOptions: Equatable { + let networkType: MediaAutoDownloadNetworkType + let peerType: MediaAutoDownloadPeerType +} + +final class InChatPrefetchManager { + private let context: AccountContext + private var settings: MediaAutoDownloadSettings + private var options: InChatPrefetchOptions? + + private var messages: [(Message, Media)] = [] + private var directionIsToLater: Bool = true + + private var contexts: [MediaId: PrefetchMediaContext] = [:] + + init(context: AccountContext) { + self.context = context + self.settings = context.sharedContext.currentAutomaticMediaDownloadSettings.with { $0 } + } + + func updateAutoDownloadSettings(_ settings: MediaAutoDownloadSettings) { + if self.settings != settings { + self.settings = settings + self.update() + } + } + + func updateOptions(_ options: InChatPrefetchOptions) { + if self.options != options { + self.options = options + self.update() + } + } + + func updateMessages(_ messages: [(Message, Media)], directionIsToLater: Bool) { + self.messages = messages + self.directionIsToLater = directionIsToLater + self.update() + } + + private func update() { + guard let options = self.options else { + return + } + + var validIds = Set() + for (message, media) in self.messages { + guard let id = media.id else { + continue + } + if validIds.contains(id) { + continue + } + + var mediaResource: MediaResource? + + var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none + + if let telegramImage = media as? TelegramMediaImage { + mediaResource = largestRepresentationForPhoto(telegramImage)?.resource + if shouldDownloadMediaAutomatically(settings: self.settings, peerType: options.peerType, networkType: options.networkType, authorPeerId: nil, contactsPeerIds: [], media: telegramImage) { + automaticDownload = .full + } + } else if let telegramFile = media as? TelegramMediaFile { + mediaResource = telegramFile.resource + if shouldDownloadMediaAutomatically(settings: self.settings, peerType: options.peerType, networkType: options.networkType, authorPeerId: nil, contactsPeerIds: [], media: telegramFile) { + automaticDownload = .full + } else if shouldPredownloadMedia(settings: self.settings, peerType: options.peerType, networkType: options.networkType, media: telegramFile) { + automaticDownload = .prefetch + } + } + + if case .none = automaticDownload { + continue + } + guard let resource = mediaResource else { + continue + } + + validIds.insert(id) + let context: PrefetchMediaContext + if let current = self.contexts[id] { + context = current + } else { + context = PrefetchMediaContext() + self.contexts[id] = context + + let priority: FetchManagerPriority = .foregroundPrefetch(direction: self.directionIsToLater ? .toLater : .toEarlier, localOrder: MessageIndex(message)) + + if case .full = automaticDownload { + if let image = media as? TelegramMediaImage { + context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.context.fetchManager, messageId: message.id, messageReference: MessageReference(message), image: image, resource: resource, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil).start()) + } else if let _ = media as? TelegramMediaWebFile { + //strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start()) + } else if let file = media as? TelegramMediaFile { + let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.context.fetchManager, messageId: message.id, messageReference: MessageReference(message), file: file, userInitiated: false, priority: priority) + context.fetchDisposable.set(fetchSignal.start()) + } + } else if case .prefetch = automaticDownload, message.id.peerId.namespace != Namespaces.Peer.SecretChat { + if let file = media as? TelegramMediaFile, let fileSize = file.size { + let fetchHeadRange: Range = 0 ..< 2 * 1024 * 1024 + let fetchTailRange: Range = fileSize - 256 * 1024 ..< Int(Int32.max) + + var ranges = IndexSet() + ranges.insert(integersIn: fetchHeadRange) + ranges.insert(integersIn: fetchTailRange) + + let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.context.fetchManager, messageId: message.id, messageReference: MessageReference(message), file: file, ranges: ranges, userInitiated: false, priority: priority) + context.fetchDisposable.set(fetchSignal.start()) + } + } + } + } + var removeIds: [MediaId] = [] + for key in self.contexts.keys { + if !validIds.contains(key) { + removeIds.append(key) + } + } + for id in removeIds { + if let context = self.contexts.removeValue(forKey: id) { + context.fetchDisposable.dispose() + } + } + } +} diff --git a/TelegramUI/PrefetchManager.swift b/TelegramUI/PrefetchManager.swift index a6cc71c32e..f593aea8f7 100644 --- a/TelegramUI/PrefetchManager.swift +++ b/TelegramUI/PrefetchManager.swift @@ -4,11 +4,9 @@ import Postbox import TelegramCore private final class PrefetchMediaContext { - let media: HolesViewMedia let fetchDisposable = MetaDisposable() - init(media: HolesViewMedia) { - self.media = media + init() { } } @@ -48,36 +46,39 @@ private final class PrefetchManagerImpl { self.listDisposable?.dispose() } - private func updateOrderedPreloadMedia(_ orderedPreloadMedia: [HolesViewMedia], automaticDownloadSettings: MediaAutoDownloadSettings, networkType: MediaAutoDownloadNetworkType) { + private func updateOrderedPreloadMedia(_ orderedPreloadMedia: [ChatHistoryPreloadMediaItem], automaticDownloadSettings: MediaAutoDownloadSettings, networkType: MediaAutoDownloadNetworkType) { var validIds = Set() for mediaItem in orderedPreloadMedia { - guard let id = mediaItem.media.id else { + guard let id = mediaItem.media.media.id else { + continue + } + if validIds.contains(id) { continue } var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none let peerType: MediaAutoDownloadPeerType - if mediaItem.authorIsContact { + if mediaItem.media.authorIsContact { peerType = .contact - } else if let channel = mediaItem.peer as? TelegramChannel { + } else if let channel = mediaItem.media.peer as? TelegramChannel { if case .group = channel.info { peerType = .group } else { peerType = .channel } - } else if mediaItem.peer is TelegramGroup { + } else if mediaItem.media.peer is TelegramGroup { peerType = .group } else { peerType = .otherPrivate } var mediaResource: MediaResource? - if let telegramImage = mediaItem.media as? TelegramMediaImage { + if let telegramImage = mediaItem.media.media as? TelegramMediaImage { mediaResource = largestRepresentationForPhoto(telegramImage)?.resource if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: peerType, networkType: networkType, authorPeerId: nil, contactsPeerIds: [], media: telegramImage) { automaticDownload = .full } - } else if let telegramFile = mediaItem.media as? TelegramMediaFile { + } else if let telegramFile = mediaItem.media.media as? TelegramMediaFile { mediaResource = telegramFile.resource if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: peerType, networkType: networkType, authorPeerId: nil, contactsPeerIds: [], media: telegramFile) { automaticDownload = .full @@ -98,21 +99,23 @@ private final class PrefetchManagerImpl { if let current = self.contexts[id] { context = current } else { - context = PrefetchMediaContext(media: mediaItem) + context = PrefetchMediaContext() self.contexts[id] = context - let media = mediaItem.media + let media = mediaItem.media.media + + let priority: FetchManagerPriority = .backgroundPrefetch(locationOrder: mediaItem.preloadIndex, localOrder: mediaItem.media.index) if case .full = automaticDownload { if let image = media as? TelegramMediaImage { - context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.index.id, messageReference: MessageReference(peer: mediaItem.peer, id: mediaItem.index.id, timestamp: mediaItem.index.timestamp, incoming: true, secret: false), image: image, resource: resource, userInitiated: false, priority: .backgroundPrefetch(mediaItem.index), storeToDownloadsPeerType: nil).start()) + context.fetchDisposable.set(messageMediaImageInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), image: image, resource: resource, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil).start()) } else if let _ = media as? TelegramMediaWebFile { //strongSelf.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: context.account, image: image).start()) } else if let file = media as? TelegramMediaFile { - let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.index.id, messageReference: MessageReference(peer: mediaItem.peer, id: mediaItem.index.id, timestamp: mediaItem.index.timestamp, incoming: true, secret: false), file: file, userInitiated: false, priority: .backgroundPrefetch(mediaItem.index)) + let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), file: file, userInitiated: false, priority: priority) context.fetchDisposable.set(fetchSignal.start()) } - } else if case .prefetch = automaticDownload, mediaItem.peer.id.namespace != Namespaces.Peer.SecretChat { + } else if case .prefetch = automaticDownload, mediaItem.media.peer.id.namespace != Namespaces.Peer.SecretChat { if let file = media as? TelegramMediaFile, let fileSize = file.size { let fetchHeadRange: Range = 0 ..< 2 * 1024 * 1024 let fetchTailRange: Range = fileSize - 256 * 1024 ..< Int(Int32.max) @@ -121,7 +124,7 @@ private final class PrefetchManagerImpl { ranges.insert(integersIn: fetchHeadRange) ranges.insert(integersIn: fetchTailRange) - let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.index.id, messageReference: MessageReference(peer: mediaItem.peer, id: mediaItem.index.id, timestamp: mediaItem.index.timestamp, incoming: true, secret: false), file: file, ranges: ranges, userInitiated: false, priority: .backgroundPrefetch(mediaItem.index)) + let fetchSignal = messageMediaFileInteractiveFetched(fetchManager: self.fetchManager, messageId: mediaItem.media.index.id, messageReference: MessageReference(peer: mediaItem.media.peer, id: mediaItem.media.index.id, timestamp: mediaItem.media.index.timestamp, incoming: true, secret: false), file: file, ranges: ranges, userInitiated: false, priority: priority) context.fetchDisposable.set(fetchSignal.start()) } } diff --git a/TelegramUI/SharedAccountContext.swift b/TelegramUI/SharedAccountContext.swift index 84c78b4129..4988970a9c 100644 --- a/TelegramUI/SharedAccountContext.swift +++ b/TelegramUI/SharedAccountContext.swift @@ -361,6 +361,7 @@ public final class SharedAccountContext { assertionFailure() } self.activeAccountsValue!.accounts.append((account.id, account, accountRecord.2)) + account.resetStateManagement() hadUpdates = true } else { let _ = accountManager.transaction({ transaction in