mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
542 lines
20 KiB
Swift
542 lines
20 KiB
Swift
import Foundation
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
|
|
|
|
public struct HistoryPreloadIndex: Hashable, Comparable, CustomStringConvertible {
|
|
public let index: ChatListIndex?
|
|
public let threadId: Int64?
|
|
public let hasUnread: Bool
|
|
public let isMuted: Bool
|
|
public let isPriority: Bool
|
|
|
|
public init(index: ChatListIndex?, threadId: Int64?, hasUnread: Bool, isMuted: Bool, isPriority: Bool) {
|
|
self.index = index
|
|
self.threadId = threadId
|
|
self.hasUnread = hasUnread
|
|
self.isMuted = isMuted
|
|
self.isPriority = isPriority
|
|
}
|
|
|
|
public static func <(lhs: HistoryPreloadIndex, rhs: HistoryPreloadIndex) -> Bool {
|
|
if lhs.isPriority != rhs.isPriority {
|
|
if lhs.isPriority {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
if lhs.isMuted != rhs.isMuted {
|
|
if lhs.isMuted {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
if lhs.hasUnread != rhs.hasUnread {
|
|
if lhs.hasUnread {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if lhs.index == rhs.index {
|
|
if let lhsThreadId = lhs.threadId, let rhsThreadId = rhs.threadId {
|
|
if lhsThreadId != rhsThreadId {
|
|
return lhsThreadId < rhsThreadId
|
|
}
|
|
}
|
|
}
|
|
|
|
if let lhsIndex = lhs.index, let rhsIndex = rhs.index {
|
|
return lhsIndex > rhsIndex
|
|
} else if lhs.index != nil {
|
|
return true
|
|
} else if rhs.index != nil {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
public var description: String {
|
|
return "index: \(String(describing: self.index)), hasUnread: \(self.hasUnread), isMuted: \(self.isMuted), isPriority: \(self.isPriority)"
|
|
}
|
|
}
|
|
|
|
private struct HistoryPreloadHole: Hashable, Comparable, CustomStringConvertible {
|
|
let preloadIndex: HistoryPreloadIndex
|
|
let hole: MessageOfInterestHole
|
|
|
|
static func <(lhs: HistoryPreloadHole, rhs: HistoryPreloadHole) -> Bool {
|
|
return lhs.preloadIndex < rhs.preloadIndex
|
|
}
|
|
|
|
var description: String {
|
|
return "(preloadIndex: \(self.preloadIndex), hole: \(self.hole))"
|
|
}
|
|
}
|
|
|
|
private final class HistoryPreloadEntry: Comparable {
|
|
var hole: HistoryPreloadHole
|
|
private var isStarted = false
|
|
private let disposable = MetaDisposable()
|
|
|
|
init(hole: HistoryPreloadHole) {
|
|
self.hole = hole
|
|
}
|
|
|
|
static func ==(lhs: HistoryPreloadEntry, rhs: HistoryPreloadEntry) -> Bool {
|
|
return lhs.hole == rhs.hole
|
|
}
|
|
|
|
static func <(lhs: HistoryPreloadEntry, rhs: HistoryPreloadEntry) -> Bool {
|
|
return lhs.hole < rhs.hole
|
|
}
|
|
|
|
func startIfNeeded(postbox: Postbox, accountPeerId: PeerId, download: Signal<Download, NoError>, queue: Queue) {
|
|
if !self.isStarted {
|
|
self.isStarted = true
|
|
|
|
let hole = self.hole.hole
|
|
|
|
Logger.shared.log("HistoryPreload", "start hole \(hole)")
|
|
|
|
let signal: Signal<Never, NoError> = .complete()
|
|
|> delay(0.3, queue: queue)
|
|
|> then(
|
|
download
|
|
|> take(1)
|
|
|> deliverOn(queue)
|
|
|> mapToSignal { download -> Signal<Never, NoError> in
|
|
switch hole.hole {
|
|
case let .peer(peerHole):
|
|
return fetchMessageHistoryHole(accountPeerId: accountPeerId, source: .download(download), postbox: postbox, peerInput: .direct(peerId: peerHole.peerId, threadId: nil), namespace: peerHole.namespace, direction: hole.direction, space: .everywhere, count: 60)
|
|
|> ignoreValues
|
|
}
|
|
}
|
|
)
|
|
self.disposable.set(signal.start())
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
self.disposable.dispose()
|
|
}
|
|
}
|
|
|
|
private final class HistoryPreloadViewContext {
|
|
var index: ChatListIndex?
|
|
var threadId: Int64?
|
|
var hasUnread: Bool?
|
|
var isMuted: Bool?
|
|
var isPriority: Bool
|
|
let disposable = MetaDisposable()
|
|
var hole: MessageOfInterestHole?
|
|
var media: [HolesViewMedia] = []
|
|
|
|
var preloadIndex: HistoryPreloadIndex {
|
|
return HistoryPreloadIndex(index: self.index, threadId: self.threadId, hasUnread: self.hasUnread ?? false, isMuted: self.isMuted ?? true, isPriority: self.isPriority)
|
|
}
|
|
|
|
var currentHole: HistoryPreloadHole? {
|
|
if let hole = self.hole {
|
|
return HistoryPreloadHole(preloadIndex: self.preloadIndex, hole: hole)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
init(index: ChatListIndex?, threadId: Int64?, hasUnread: Bool?, isMuted: Bool?, isPriority: Bool) {
|
|
self.index = index
|
|
self.threadId = threadId
|
|
self.hasUnread = hasUnread
|
|
self.isMuted = isMuted
|
|
self.isPriority = isPriority
|
|
}
|
|
|
|
deinit {
|
|
disposable.dispose()
|
|
}
|
|
}
|
|
|
|
private enum ChatHistoryPreloadEntity: Hashable {
|
|
case peer(peerId: PeerId, threadId: Int64?)
|
|
}
|
|
|
|
private struct ChatHistoryPreloadIndex {
|
|
let index: ChatListIndex
|
|
let entity: ChatHistoryPreloadEntity
|
|
}
|
|
|
|
public final class ChatHistoryPreloadMediaItem: Comparable {
|
|
public let preloadIndex: HistoryPreloadIndex
|
|
public let media: HolesViewMedia
|
|
|
|
init(preloadIndex: HistoryPreloadIndex, media: HolesViewMedia) {
|
|
self.preloadIndex = preloadIndex
|
|
self.media = media
|
|
}
|
|
|
|
public static func ==(lhs: ChatHistoryPreloadMediaItem, rhs: ChatHistoryPreloadMediaItem) -> Bool {
|
|
if lhs.preloadIndex != rhs.preloadIndex {
|
|
return false
|
|
}
|
|
if lhs.media != rhs.media {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public static func <(lhs: ChatHistoryPreloadMediaItem, rhs: ChatHistoryPreloadMediaItem) -> Bool {
|
|
if lhs.preloadIndex != rhs.preloadIndex {
|
|
return lhs.preloadIndex > rhs.preloadIndex
|
|
}
|
|
return lhs.media.index < rhs.media.index
|
|
}
|
|
}
|
|
|
|
private final class AdditionalPreloadPeerIdsContext {
|
|
private let queue: Queue
|
|
|
|
private var subscribers: [PeerId: Bag<Void>] = [:]
|
|
private var additionalPeerIdsValue = ValuePromise<Set<PeerId>>(Set(), ignoreRepeated: true)
|
|
|
|
var additionalPeerIds: Signal<Set<PeerId>, NoError> {
|
|
return self.additionalPeerIdsValue.get()
|
|
}
|
|
|
|
init(queue: Queue) {
|
|
self.queue = queue
|
|
}
|
|
|
|
deinit {
|
|
assert(self.queue.isCurrent())
|
|
}
|
|
|
|
func add(peerId: PeerId) -> Disposable {
|
|
let bag: Bag<Void>
|
|
if let current = self.subscribers[peerId] {
|
|
bag = current
|
|
} else {
|
|
bag = Bag()
|
|
self.subscribers[peerId] = bag
|
|
}
|
|
let wasEmpty = bag.isEmpty
|
|
let index = bag.add(Void())
|
|
|
|
if wasEmpty {
|
|
self.additionalPeerIdsValue.set(Set(self.subscribers.keys))
|
|
}
|
|
let queue = self.queue
|
|
return ActionDisposable { [weak self, weak bag] in
|
|
queue.async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if let current = strongSelf.subscribers[peerId], let bag = bag, current === bag {
|
|
current.remove(index)
|
|
if current.isEmpty {
|
|
strongSelf.subscribers.removeValue(forKey: peerId)
|
|
strongSelf.additionalPeerIdsValue.set(Set(strongSelf.subscribers.keys))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct ChatHistoryPreloadItem : Equatable, Hashable {
|
|
public let index: ChatListIndex
|
|
public let threadId: Int64?
|
|
public let isMuted: Bool
|
|
public let hasUnread: Bool
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(index.hashValue)
|
|
if let threadId = threadId {
|
|
hasher.combine(threadId)
|
|
}
|
|
}
|
|
|
|
public init(index: ChatListIndex, threadId: Int64?, isMuted: Bool, hasUnread: Bool) {
|
|
self.index = index
|
|
self.threadId = threadId
|
|
self.isMuted = isMuted
|
|
self.hasUnread = hasUnread
|
|
}
|
|
}
|
|
|
|
final class ChatHistoryPreloadManager {
|
|
private let queue = Queue()
|
|
|
|
private let postbox: Postbox
|
|
private let accountPeerId: PeerId
|
|
private let network: Network
|
|
private let download = Promise<Download>()
|
|
|
|
private var canPreloadHistoryDisposable: Disposable?
|
|
private var canPreloadHistoryValue = false
|
|
|
|
private let automaticChatListDisposable = MetaDisposable()
|
|
|
|
private var views: [ChatHistoryPreloadEntity: HistoryPreloadViewContext] = [:]
|
|
|
|
private var entries: [HistoryPreloadEntry] = []
|
|
|
|
private var orderedMediaValue: [ChatHistoryPreloadMediaItem] = []
|
|
private let orderedMediaPromise = ValuePromise<[ChatHistoryPreloadMediaItem]>([])
|
|
var orderedMedia: Signal<[ChatHistoryPreloadMediaItem], NoError> {
|
|
return self.orderedMediaPromise.get()
|
|
}
|
|
|
|
private let additionalPreloadPeerIdsContext: QueueLocalObject<AdditionalPreloadPeerIdsContext>
|
|
private let preloadItemsSignal: Signal<[ChatHistoryPreloadItem], NoError>
|
|
|
|
init(postbox: Postbox, network: Network, accountPeerId: PeerId, networkState: Signal<AccountNetworkState, NoError>, preloadItemsSignal: Signal<[ChatHistoryPreloadItem], NoError>) {
|
|
self.postbox = postbox
|
|
self.network = network
|
|
self.accountPeerId = accountPeerId
|
|
self.download.set(network.background())
|
|
self.preloadItemsSignal = preloadItemsSignal
|
|
|
|
let queue = Queue.mainQueue()
|
|
self.additionalPreloadPeerIdsContext = QueueLocalObject(queue: queue, generate: {
|
|
AdditionalPreloadPeerIdsContext(queue: queue)
|
|
})
|
|
|
|
self.canPreloadHistoryDisposable = (networkState
|
|
|> map { state -> Bool in
|
|
switch state {
|
|
case .online:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
|> deliverOn(self.queue)).start(next: { [weak self] value in
|
|
guard let strongSelf = self, strongSelf.canPreloadHistoryValue != value else {
|
|
return
|
|
}
|
|
|
|
strongSelf.canPreloadHistoryValue = value
|
|
if value {
|
|
for i in 0 ..< min(3, strongSelf.entries.count) {
|
|
strongSelf.entries[i].startIfNeeded(postbox: strongSelf.postbox, accountPeerId: strongSelf.accountPeerId, download: strongSelf.download.get() |> take(1), queue: strongSelf.queue)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.canPreloadHistoryDisposable?.dispose()
|
|
}
|
|
|
|
func addAdditionalPeerId(peerId: PeerId) -> Disposable {
|
|
let disposable = MetaDisposable()
|
|
self.additionalPreloadPeerIdsContext.with { context in
|
|
disposable.set(context.add(peerId: peerId))
|
|
}
|
|
return disposable
|
|
}
|
|
|
|
func start() {
|
|
let additionalPreloadPeerIdsContext = self.additionalPreloadPeerIdsContext
|
|
let additionalPeerIds = Signal<Set<PeerId>, NoError> { subscriber in
|
|
let disposable = MetaDisposable()
|
|
additionalPreloadPeerIdsContext.with { context in
|
|
disposable.set(context.additionalPeerIds.start(next: { value in
|
|
subscriber.putNext(value)
|
|
}))
|
|
}
|
|
return disposable
|
|
}
|
|
|
|
self.automaticChatListDisposable.set((combineLatest(queue: .mainQueue(), self.preloadItemsSignal, additionalPeerIds)
|
|
|> delay(1.0, queue: .mainQueue())
|
|
|> deliverOnMainQueue).start(next: { [weak self] loadItems, additionalPeerIds in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
/*#if DEBUG
|
|
if "".isEmpty {
|
|
return
|
|
}
|
|
#endif*/
|
|
|
|
var indices: [(ChatHistoryPreloadIndex, Bool, Bool)] = []
|
|
for item in loadItems {
|
|
indices.append((ChatHistoryPreloadIndex(index: item.index, entity: .peer(peerId: item.index.messageIndex.id.peerId, threadId: item.threadId)), item.hasUnread, item.isMuted))
|
|
}
|
|
|
|
strongSelf.update(indices: indices, additionalPeerIds: additionalPeerIds)
|
|
}))
|
|
}
|
|
|
|
private func update(indices: [(ChatHistoryPreloadIndex, Bool, Bool)], additionalPeerIds: Set<PeerId>) {
|
|
self.queue.async {
|
|
var validEntityIds = Set(indices.map { $0.0.entity })
|
|
for peerId in additionalPeerIds {
|
|
validEntityIds.insert(.peer(peerId: peerId, threadId: nil))
|
|
}
|
|
|
|
var removedEntityIds: [ChatHistoryPreloadEntity] = []
|
|
for (entityId, view) in self.views {
|
|
if !validEntityIds.contains(entityId) {
|
|
removedEntityIds.append(entityId)
|
|
if let hole = view.currentHole {
|
|
self.update(from: hole, to: nil)
|
|
}
|
|
}
|
|
}
|
|
for entityId in removedEntityIds {
|
|
self.views.removeValue(forKey: entityId)
|
|
}
|
|
|
|
var combinedIndices: [(ChatHistoryPreloadIndex, Bool, Bool, Bool)] = []
|
|
var existingPeerIds = Set<PeerId>()
|
|
for (index, hasUnread, isMuted) in indices {
|
|
existingPeerIds.insert(index.index.messageIndex.id.peerId)
|
|
combinedIndices.append((index, hasUnread, isMuted, additionalPeerIds.contains(index.index.messageIndex.id.peerId)))
|
|
}
|
|
for peerId in additionalPeerIds {
|
|
if !existingPeerIds.contains(peerId) {
|
|
combinedIndices.append((ChatHistoryPreloadIndex(index: ChatListIndex.absoluteLowerBound, entity: .peer(peerId: peerId, threadId: nil)), false, true, true))
|
|
}
|
|
}
|
|
|
|
for (index, hasUnread, isMuted, isPriority) in combinedIndices {
|
|
if let view = self.views[index.entity] {
|
|
if view.index != index.index || view.hasUnread != hasUnread || view.isMuted != isMuted {
|
|
let previousHole = view.currentHole
|
|
view.index = index.index
|
|
view.hasUnread = hasUnread
|
|
view.isMuted = isMuted
|
|
|
|
let updatedHole = view.currentHole
|
|
if previousHole != updatedHole {
|
|
self.update(from: previousHole, to: updatedHole)
|
|
}
|
|
}
|
|
} else {
|
|
var threadId: Int64?
|
|
switch index.entity {
|
|
case let .peer(_, threadIdValue):
|
|
threadId = threadIdValue
|
|
}
|
|
let view = HistoryPreloadViewContext(index: index.index, threadId: threadId, hasUnread: hasUnread, isMuted: isMuted, isPriority: isPriority)
|
|
self.views[index.entity] = view
|
|
let key: PostboxViewKey
|
|
switch index.entity {
|
|
case let .peer(peerId, threadId):
|
|
key = .messageOfInterestHole(location: .peer(peerId: peerId, threadId: threadId), namespace: Namespaces.Message.Cloud, count: 70)
|
|
}
|
|
view.disposable.set((self.postbox.combinedView(keys: [key])
|
|
|> deliverOn(self.queue)).start(next: { [weak self] next in
|
|
if let strongSelf = self, let value = next.views[key] as? MessageOfInterestHolesView {
|
|
if let view = strongSelf.views[index.entity] {
|
|
let previousHole = view.currentHole
|
|
view.hole = value.closestHole
|
|
|
|
var mediaUpdated = false
|
|
if view.media.count != value.closestLaterMedia.count {
|
|
mediaUpdated = true
|
|
} else {
|
|
for i in 0 ..< view.media.count {
|
|
if view.media[i] != value.closestLaterMedia[i] {
|
|
mediaUpdated = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if mediaUpdated {
|
|
view.media = value.closestLaterMedia
|
|
strongSelf.updateMedia()
|
|
}
|
|
|
|
let updatedHole = view.currentHole
|
|
|
|
let holeIsUpdated = previousHole != updatedHole
|
|
|
|
switch index.entity {
|
|
case let .peer(peerId, threadId):
|
|
Logger.shared.log("HistoryPreload", "view \(peerId) (threadId: \(String(describing: threadId)) hole \(String(describing: updatedHole)) isUpdated: \(holeIsUpdated)")
|
|
}
|
|
|
|
if previousHole != updatedHole {
|
|
strongSelf.update(from: previousHole, to: updatedHole)
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateMedia() {
|
|
var result: [ChatHistoryPreloadMediaItem] = []
|
|
for (_, view) in self.views {
|
|
for media in view.media {
|
|
result.append(ChatHistoryPreloadMediaItem(preloadIndex: view.preloadIndex, media: media))
|
|
}
|
|
}
|
|
result.sort()
|
|
if result != self.orderedMediaValue {
|
|
self.orderedMediaValue = result
|
|
self.orderedMediaPromise.set(result)
|
|
}
|
|
}
|
|
|
|
private func update(from previousHole: HistoryPreloadHole?, to updatedHole: HistoryPreloadHole?) {
|
|
assert(self.queue.isCurrent())
|
|
let isHoleUpdated = previousHole != updatedHole
|
|
|
|
Logger.shared.log("HistoryPreload", "update from \(String(describing: previousHole)) to \(String(describing: updatedHole)), isUpdated: \(isHoleUpdated)")
|
|
|
|
if !isHoleUpdated {
|
|
return
|
|
}
|
|
|
|
var skipUpdated = false
|
|
if let previousHole = previousHole {
|
|
for i in (0 ..< self.entries.count).reversed() {
|
|
if self.entries[i].hole == previousHole {
|
|
if let updatedHole = updatedHole, updatedHole.hole == self.entries[i].hole.hole {
|
|
self.entries[i].hole = updatedHole
|
|
skipUpdated = true
|
|
} else {
|
|
self.entries.remove(at: i)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if let updatedHole = updatedHole, !skipUpdated {
|
|
var found = false
|
|
for i in 0 ..< self.entries.count {
|
|
if self.entries[i].hole == updatedHole {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
self.entries.append(HistoryPreloadEntry(hole: updatedHole))
|
|
self.entries.sort()
|
|
}
|
|
}
|
|
|
|
if self.canPreloadHistoryValue {
|
|
Logger.shared.log("HistoryPreload", "will start")
|
|
for i in 0 ..< min(3, self.entries.count) {
|
|
self.entries[i].startIfNeeded(postbox: self.postbox, accountPeerId: self.accountPeerId, download: self.download.get() |> take(1), queue: self.queue)
|
|
}
|
|
} else {
|
|
Logger.shared.log("HistoryPreload", "will not start, canPreloadHistoryValue = false")
|
|
}
|
|
}
|
|
}
|