#45 Improvements on home timeline.
Changes: - downloading 40 statuses - fixing duplicates - downloading only by user push to refresh
This commit is contained in:
parent
815172ad76
commit
00da250b21
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue