Vernissage/Vernissage/Services/HomeTimelineService.swift

158 lines
6.6 KiB
Swift

//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftData
import PixelfedKit
import ClientKit
import ServicesKit
import Nuke
import OSLog
import EnvironmentKit
import Semaphore
/// Service responsible for managing home timeline.
@MainActor
public class HomeTimelineService {
public static let shared = HomeTimelineService()
private init() { }
private let maximumAmountOfDownloadedStatuses = 40
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
private let semaphore = AsyncSemaphore(value: 1)
public func amountOfNewStatuses(includeReblogs: Bool, hideStatusesWithoutAlt: Bool, modelContext: ModelContext) async -> Int {
await semaphore.wait()
defer { semaphore.signal() }
guard let accountData = AccountDataHandler.shared.getCurrentAccountData(modelContext: modelContext) else {
return 0
}
guard let accessToken = accountData.accessToken else {
return 0
}
// Get maximimum downloaded stauts id.
guard let lastSeenStatusId = self.getLastLoadedStatusId(accountId: accountData.id, modelContext: modelContext) else {
return 0
}
let client = PixelfedClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
var statuses: [Status] = []
var latestStatusId: String? = nil
var breakProcesssing = false;
// There can be more then 40 newest statuses, that's why we have to sometimes send more then one request.
while true {
do {
// Download statuses from the top or the list.
let downloadedStatuses = try await client.getHomeTimeline(maxId: latestStatusId,
limit: self.maximumAmountOfDownloadedStatuses,
includeReblogs: includeReblogs)
// Iterate througt the list until we go to already visible status by the user.
var temporaryList: [Status] = []
for downloadedStatus in downloadedStatuses.data {
guard downloadedStatus.id != lastSeenStatusId else {
breakProcesssing = true
break
}
temporaryList.append(downloadedStatus)
}
// Remove from the list duplicated statuses.
let visibleStatuses = self.getVisibleStatuses(accountId: accountData.id,
statuses: temporaryList,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
modelContext: modelContext)
// Add statuses to the list.
statuses.append(contentsOf: visibleStatuses)
// Break when we go to the already visible status.
if breakProcesssing {
break
}
// When we discovered more then 100 statuses we can break.
if statuses.count > 100 {
break
}
// Set status Id which should be used to download next portion of the statuses.
latestStatusId = downloadedStatuses.getMaxId()
} catch {
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadingNewStatuses")
break
}
}
// Start prefetching images.
self.prefetch(statuses: statuses)
// Return number of new statuses not visible yet on the timeline.
return statuses.count
}
public func getVisibleStatuses(accountId: String, statuses: [Status], hideStatusesWithoutAlt: Bool, modelContext: ModelContext) -> [Status] {
// We have to include in the counter only statuses with images.
let statusesWithImagesOnly = statuses.getStatusesWithImagesOnly()
let statusesFromOldestToNewest = statusesWithImagesOnly.reversed()
var visibleStatuses: [Status] = []
for status in statusesFromOldestToNewest {
// We have to hide statuses without ALT text.
if hideStatusesWithoutAlt && status.statusContainsAltText() == false {
continue
}
// We shouldn't add statuses that are boosted by muted accounts.
if AccountRelationshipHandler.shared.isBoostedStatusesMuted(accountId: accountId, status: status, modelContext: modelContext) {
continue
}
// We should add to timeline only statuses that has not been showned to the user already.
guard self.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, modelContext: modelContext) == false else {
continue
}
// Same rebloged status has been already visible in already processed (visible) portion of data.
if let reblog = status.reblog, visibleStatuses.contains(where: { $0.reblog?.id == reblog.id || $0.id == reblog.id }) {
continue
}
// Same rebloged (orginal) status will be added to visible in same portion of data.
if let reblog = status.reblog, statusesWithImagesOnly.contains(where: { $0.id == reblog.id }) {
continue
}
visibleStatuses.append(status)
}
// Return statuses from newest to oldest.
return visibleStatuses.reversed()
}
private func hasBeenAlreadyOnTimeline(accountId: String, status: Status, modelContext: ModelContext) -> Bool {
return ViewedStatusHandler.shared.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, modelContext: modelContext)
}
private func getLastLoadedStatusId(accountId: String, modelContext: ModelContext) -> String? {
let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext)
return accountData?.lastLoadedStatusId
}
private func prefetch(statuses: [Status]) {
let statusModels = statuses.toStatusModels()
imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls())
}
}