#45 Improvements on home timeline.

Changes:
 - downloading 40 statuses
 - fixing duplicates
 - downloading only by user push to refresh
This commit is contained in:
Marcin Czachurski 2023-04-19 15:32:25 +02:00
parent 815172ad76
commit 00da250b21
5 changed files with 126 additions and 41 deletions

View File

@ -44,4 +44,37 @@ extension StatusData {
self.visibility = status.visibility.rawValue
}
}
func updateFrom(_ status: Status) {
if let reblog = status.reblog {
self.updateFrom(reblog)
self.rebloggedAccountAvatar = status.account.avatar
self.rebloggedAccountDisplayName = status.account.displayName
self.rebloggedAccountId = status.account.id
self.rebloggedAccountUsername = status.account.acct
} else {
self.accountAvatar = status.account.avatar
self.accountDisplayName = status.account.displayName
self.accountUsername = status.account.acct
self.applicationName = status.application?.name
self.applicationWebsite = status.application?.website
self.bookmarked = status.bookmarked
self.content = status.content.htmlValue
self.favourited = status.favourited
self.favouritesCount = Int32(status.favouritesCount)
self.inReplyToAccount = status.inReplyToAccount
self.inReplyToId = status.inReplyToId
self.muted = status.muted
self.pinned = status.pinned
self.reblogged = status.reblogged
self.reblogsCount = Int32(status.reblogsCount)
self.repliesCount = Int32(status.repliesCount)
self.sensitive = status.sensitive
self.spoilerText = status.spoilerText
self.uri = status.uri
self.url = status.url
self.visibility = status.visibility.rawValue
}
}
}

View File

@ -12,8 +12,8 @@ class StatusDataHandler {
public static let shared = StatusDataHandler()
private init() { }
func getAllStatuses(accountId: String) -> [StatusData] {
let context = CoreDataHandler.shared.container.viewContext
func getAllStatuses(accountId: String, viewContext: NSManagedObjectContext? = nil) -> [StatusData] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
@ -28,6 +28,26 @@ class StatusDataHandler {
}
}
func getAllOlderStatuses(accountId: String, statusId: String, viewContext: NSManagedObjectContext? = nil) -> [StatusData] {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let predicate1 = NSPredicate(format: "id < %@", statusId)
let predicate2 = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [predicate1, predicate2])
do {
return try context.fetch(fetchRequest)
} catch {
CoreDataError.shared.handle(error, message: "Error during fetching status (getStatusData).")
return []
}
}
func getStatusData(accountId: String, statusId: String, viewContext: NSManagedObjectContext? = nil) -> StatusData? {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
let fetchRequest = StatusData.fetchRequest()
@ -84,13 +104,13 @@ class StatusDataHandler {
}
}
func remove(accountId: String, statusId: String) {
func remove(accountId: String, statusId: String, viewContext: NSManagedObjectContext? = nil) {
let status = self.getStatusData(accountId: accountId, statusId: statusId)
guard let status else {
return
}
let context = CoreDataHandler.shared.container.viewContext
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
context.delete(status)
do {
@ -100,8 +120,8 @@ class StatusDataHandler {
}
}
func remove(accountId: String, statuses: [StatusData]) {
let context = CoreDataHandler.shared.container.viewContext
func remove(accountId: String, statuses: [StatusData], viewContext: NSManagedObjectContext? = nil) {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
for status in statuses {
context.delete(status)

View File

@ -1187,7 +1187,7 @@
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 122;
CURRENT_PROJECT_VERSION = 123;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist;
@ -1215,7 +1215,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 122;
CURRENT_PROJECT_VERSION = 123;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist;
@ -1242,7 +1242,7 @@
CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 122;
CURRENT_PROJECT_VERSION = 123;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageShare/Info.plist;
@ -1269,7 +1269,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 122;
CURRENT_PROJECT_VERSION = 123;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageShare/Info.plist;
@ -1418,7 +1418,7 @@
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 122;
CURRENT_PROJECT_VERSION = 123;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;
@ -1459,7 +1459,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 122;
CURRENT_PROJECT_VERSION = 123;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;

View File

@ -15,6 +15,8 @@ public class HomeTimelineService {
public static let shared = HomeTimelineService()
private init() { }
private let defaultAmountOfDownloadedStatuses = 40
public func loadOnBottom(for account: AccountModel) async throws -> Int {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
@ -36,7 +38,7 @@ public class HomeTimelineService {
return newStatuses.count
}
public func loadOnTop(for account: AccountModel) async throws -> String? {
public func refreshTimeline(for account: AccountModel) async throws -> String? {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
@ -113,7 +115,7 @@ public class HomeTimelineService {
// There can be more then 40 newest statuses, that's why we have to sometimes send more then one request.
while true {
do {
let downloadedStatuses = try await client.getHomeTimeline(minId: newestStatusId, limit: 40)
let downloadedStatuses = try await client.getHomeTimeline(minId: newestStatusId, limit: self.defaultAmountOfDownloadedStatuses)
guard let firstStatus = downloadedStatuses.first else {
break
}
@ -139,38 +141,59 @@ public class HomeTimelineService {
// Retrieve statuses from API.
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(limit: 40)
let statuses = try await client.getHomeTimeline(limit: self.defaultAmountOfDownloadedStatuses)
// Retrieve all statuses from database.
let dbStatuses = StatusDataHandler.shared.getAllStatuses(accountId: account.id)
let lastSeenStatusId = dbStatuses.last?.rebloggedStatusId ?? dbStatuses.last?.id
// Retrieve newest visible status (last visible by user).
let dbNewestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: account.id, viewContext: backgroundContext)
let lastSeenStatusId = dbNewestStatus?.rebloggedStatusId ?? dbNewestStatus?.id
// Remove statuses that are not in 40 downloaded once.
var dbStatusesToRemove: [StatusData] = []
for dbStatus in dbStatuses where !statuses.contains(where: { status in status.id == dbStatus.id }) {
dbStatusesToRemove.append(dbStatus)
}
if !dbStatusesToRemove.isEmpty {
StatusDataHandler.shared.remove(accountId: account.id, statuses: dbStatusesToRemove)
}
// Update existing one.
for dbStatus in dbStatuses {
if let status = statuses.first(where: { item in item.id == dbStatus.id}) {
dbStatus.favourited = status.favourited
// Update all existing statuses in database.
for status in statuses {
if let dbStatus = StatusDataHandler.shared.getStatusData(accountId: account.id, statusId: status.id, viewContext: backgroundContext) {
dbStatus.updateFrom(status)
}
}
// Add statuses which are not existing in database, but has been downloaded via API.
var statusesToAdd: [Status] = []
for status in statuses where !dbStatuses.contains(where: { statusData in statusData.id == status.id }) {
for status in statuses where StatusDataHandler.shared.getStatusData(accountId: account.id,
statusId: status.id,
viewContext: backgroundContext) == nil {
statusesToAdd.append(status)
}
// Collection with statuses to remove from database.
var dbStatusesToRemove: [StatusData] = []
// Find statuses to delete (older then the last one from API).
if let lastStatus = statuses.last {
let dbOlderStatuses = StatusDataHandler.shared.getAllOlderStatuses(accountId: account.id,
statusId: lastStatus.id,
viewContext: backgroundContext)
if !dbOlderStatuses.isEmpty {
dbStatusesToRemove.append(contentsOf: dbOlderStatuses)
}
}
// Find statuses to delete (duplicates).
var existingStatusIds: [String] = []
let allDbStatuses = StatusDataHandler.shared.getAllStatuses(accountId: account.id, viewContext: backgroundContext)
for dbStatus in allDbStatuses {
if existingStatusIds.contains(where: { $0 == dbStatus.id }) {
dbStatusesToRemove.append(dbStatus)
} else {
existingStatusIds.append(dbStatus.id)
}
}
// Delete statuses from database.
if !dbStatusesToRemove.isEmpty {
StatusDataHandler.shared.remove(accountId: account.id, statuses: dbStatusesToRemove, viewContext: backgroundContext)
}
// Save statuses in database.
if !statusesToAdd.isEmpty {
_ = try await self.save(statuses: statusesToAdd, for: account, on: backgroundContext)
_ = try await self.add(statusesToAdd, for: account, on: backgroundContext)
}
return lastSeenStatusId
@ -187,15 +210,15 @@ public class HomeTimelineService {
// Retrieve statuses from API.
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 20)
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: self.defaultAmountOfDownloadedStatuses)
// Save statuses in database.
return try await self.save(statuses: statuses, for: account, on: backgroundContext)
return try await self.add(statuses, for: account, on: backgroundContext)
}
private func save(statuses: [Status],
for account: AccountModel,
on backgroundContext: NSManagedObjectContext
private func add(_ statuses: [Status],
for account: AccountModel,
on backgroundContext: NSManagedObjectContext
) async throws -> [Status] {
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else {

View File

@ -101,7 +101,7 @@ struct HomeFeedView: View {
private func refreshData() async {
do {
if let account = self.applicationState.account {
if let lastSeenStatusId = try await HomeTimelineService.shared.loadOnTop(for: account) {
if let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account) {
try await HomeTimelineService.shared.save(lastSeenStatusId: lastSeenStatusId, for: account)
self.applicationState.lastSeenStatusId = lastSeenStatusId
@ -115,8 +115,17 @@ struct HomeFeedView: View {
private func loadData() async {
do {
// We have to load data automatically only when the database is empty.
guard self.dbStatuses.isEmpty else {
withAnimation {
self.state = .loaded
}
return
}
if let account = self.applicationState.account {
_ = try await HomeTimelineService.shared.loadOnTop(for: account)
_ = try await HomeTimelineService.shared.refreshTimeline(for: account)
}
self.applicationState.amountOfNewStatuses = 0