Improve counter on home timeline

This commit is contained in:
Marcin Czachurski 2023-11-20 15:23:42 +01:00
parent 2a44115bd8
commit facc2caf5f
12 changed files with 169 additions and 79 deletions

View File

@ -13,7 +13,7 @@ extension Client {
sinceId: String? = nil, sinceId: String? = nil,
minId: String? = nil, minId: String? = nil,
limit: Int = 40, limit: Int = 40,
includeReblogs: Bool? = nil) async throws -> [Status] { includeReblogs: Bool? = nil) async throws -> Linkable<[Status]> {
return try await pixelfedClient.getHomeTimeline(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit, includeReblogs: includeReblogs) return try await pixelfedClient.getHomeTimeline(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit, includeReblogs: includeReblogs)
} }
@ -22,7 +22,7 @@ extension Client {
maxId: String? = nil, maxId: String? = nil,
sinceId: String? = nil, sinceId: String? = nil,
minId: String? = nil, minId: String? = nil,
limit: Int = 40) async throws -> [Status] { limit: Int = 40) async throws -> Linkable<[Status]> {
return try await pixelfedClient.getPublicTimeline(local: local, return try await pixelfedClient.getPublicTimeline(local: local,
remote: remote, remote: remote,
onlyMedia: true, onlyMedia: true,
@ -38,7 +38,7 @@ extension Client {
maxId: String? = nil, maxId: String? = nil,
sinceId: String? = nil, sinceId: String? = nil,
minId: String? = nil, minId: String? = nil,
limit: Int = 40) async throws -> [Status] { limit: Int = 40) async throws -> Linkable<[Status]> {
return try await pixelfedClient.getTagTimeline(tag: tag, return try await pixelfedClient.getTagTimeline(tag: tag,
local: local, local: local,
remote: remote, remote: remote,

View File

@ -64,7 +64,7 @@ class AccountDataHandler {
} }
} }
func update(lastSeenStatusId: String?, lastLoadedStatusId: String?, statuses: [Status]? = nil, applicationState: ApplicationState, modelContext: ModelContext) throws { func update(lastSeenStatusId: String?, lastLoadedStatusId: String?, statuses: Linkable<[Status]>? = nil, applicationState: ApplicationState, modelContext: ModelContext) throws {
guard let accountId = applicationState.account?.id else { guard let accountId = applicationState.account?.id else {
return return
} }

View File

@ -237,6 +237,34 @@
} }
} }
}, },
"+99" : {
"localizations" : {
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "+99"
}
},
"eu" : {
"stringUnit" : {
"state" : "translated",
"value" : "+99"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "+99"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "+99"
}
}
}
},
"accounts.error.loadingAccountsFailed" : { "accounts.error.loadingAccountsFailed" : {
"comment" : "Information message when loading account failed", "comment" : "Information message when loading account failed",
"localizations" : { "localizations" : {

View File

@ -8,7 +8,7 @@ import Foundation
import RegexBuilder import RegexBuilder
/// Link returned in header for paging feature/ /// Link returned in header for paging feature/
public struct Link { public struct Link: Codable {
/// Raw value of header link. /// Raw value of header link.
public let rawLink: String public let rawLink: String

View File

@ -7,7 +7,7 @@
import Foundation import Foundation
/// Some of endpoint returns JSON data and additional information in header, like link for paging functionality. /// Some of endpoint returns JSON data and additional information in header, like link for paging functionality.
public struct Linkable<T> where T: Codable { public struct Linkable<T> : Codable where T: Codable {
/// Data retunred in HTTP reponse body (mostly JSON data/entities). /// Data retunred in HTTP reponse body (mostly JSON data/entities).
public let data: T public let data: T
@ -20,3 +20,29 @@ public struct Linkable<T> where T: Codable {
self.link = link self.link = link
} }
} }
public extension Linkable<[Status]> {
func getMinId() -> String? {
if let link = self.link {
return link.minId
}
if let firstItemId = self.data.first?.id {
return firstItemId
}
return nil
}
func getMaxId() -> String? {
if let link = self.link {
return link.maxId
}
if let lastItemId = self.data.last?.id {
return lastItemId
}
return nil
}
}

View File

@ -13,7 +13,7 @@ public extension PixelfedClientAuthenticated {
minId: EntityId? = nil, minId: EntityId? = nil,
limit: Int? = nil, limit: Int? = nil,
includeReblogs: Bool? = nil, includeReblogs: Bool? = nil,
timeoutInterval: Double? = nil) async throws -> [Status] { timeoutInterval: Double? = nil) async throws -> Linkable<[Status]> {
let request = try Self.request( let request = try Self.request(
for: baseURL, for: baseURL,
@ -22,7 +22,7 @@ public extension PixelfedClientAuthenticated {
timeoutInterval: timeoutInterval timeoutInterval: timeoutInterval
) )
return try await downloadJson([Status].self, request: request) return try await downloadJsonWithLink([Status].self, request: request)
} }
func getPublicTimeline(local: Bool? = nil, func getPublicTimeline(local: Bool? = nil,
@ -31,7 +31,7 @@ public extension PixelfedClientAuthenticated {
maxId: EntityId? = nil, maxId: EntityId? = nil,
sinceId: EntityId? = nil, sinceId: EntityId? = nil,
minId: EntityId? = nil, minId: EntityId? = nil,
limit: Limit? = nil) async throws -> [Status] { limit: Limit? = nil) async throws -> Linkable<[Status]> {
let request = try Self.request( let request = try Self.request(
for: baseURL, for: baseURL,
@ -39,7 +39,7 @@ public extension PixelfedClientAuthenticated {
withBearerToken: token withBearerToken: token
) )
return try await downloadJson([Status].self, request: request) return try await downloadJsonWithLink([Status].self, request: request)
} }
func getTagTimeline(tag: String, func getTagTimeline(tag: String,
@ -49,7 +49,7 @@ public extension PixelfedClientAuthenticated {
maxId: EntityId? = nil, maxId: EntityId? = nil,
sinceId: EntityId? = nil, sinceId: EntityId? = nil,
minId: EntityId? = nil, minId: EntityId? = nil,
limit: Int? = nil) async throws -> [Status] { limit: Int? = nil) async throws -> Linkable<[Status]> {
let request = try Self.request( let request = try Self.request(
for: baseURL, for: baseURL,
@ -57,7 +57,7 @@ public extension PixelfedClientAuthenticated {
withBearerToken: token withBearerToken: token
) )
return try await downloadJson([Status].self, request: request) return try await downloadJsonWithLink([Status].self, request: request)
} }
func setMarkers(_ markers: [Pixelfed.Markers.Timeline: EntityId]) async throws -> Markers { func setMarkers(_ markers: [Pixelfed.Markers.Timeline: EntityId]) async throws -> Markers {

View File

@ -1209,7 +1209,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.0.1; MARKETING_VERSION = 2.0.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -1243,7 +1243,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.0.1; MARKETING_VERSION = 2.0.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -1276,7 +1276,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.0.1; MARKETING_VERSION = 2.0.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -1308,7 +1308,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.0.1; MARKETING_VERSION = 2.0.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -1474,7 +1474,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2.0.1; MARKETING_VERSION = 2.0.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -1517,7 +1517,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2.0.1; MARKETING_VERSION = 2.0.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage; PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";

View File

@ -43,27 +43,49 @@ public class HomeTimelineService {
let client = PixelfedClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken) let client = PixelfedClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
var statuses: [Status] = [] var statuses: [Status] = []
var newestStatusId = lastSeenStatusId 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. // There can be more then 40 newest statuses, that's why we have to sometimes send more then one request.
while true { while true {
do { do {
let downloadedStatuses = try await client.getHomeTimeline(minId: newestStatusId, // Download statuses from the top or the list.
let downloadedStatuses = try await client.getHomeTimeline(maxId: latestStatusId,
limit: self.maximumAmountOfDownloadedStatuses, limit: self.maximumAmountOfDownloadedStatuses,
includeReblogs: includeReblogs) includeReblogs: includeReblogs)
guard let firstStatus = downloadedStatuses.first else { // Iterate througt the list until we go to already visible status by the user.
break 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, let visibleStatuses = self.getVisibleStatuses(accountId: accountData.id,
statuses: downloadedStatuses, statuses: temporaryList,
hideStatusesWithoutAlt: hideStatusesWithoutAlt, hideStatusesWithoutAlt: hideStatusesWithoutAlt,
modelContext: modelContext) modelContext: modelContext)
// Add statuses to the list.
statuses.append(contentsOf: visibleStatuses) statuses.append(contentsOf: visibleStatuses)
newestStatusId = firstStatus.id // 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 { } catch {
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadingNewStatuses") ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadingNewStatuses")
break break

View File

@ -143,8 +143,14 @@ struct HomeTimelineView: View {
HStack { HStack {
Image(systemName: "arrow.up") Image(systemName: "arrow.up")
.fontWeight(.light) .fontWeight(.light)
Text("\(self.applicationState.amountOfNewStatuses)")
.fontWeight(.semibold) if self.applicationState.amountOfNewStatuses < 100 {
Text("\(self.applicationState.amountOfNewStatuses)")
.fontWeight(.semibold)
} else {
Text("+99")
.fontWeight(.semibold)
}
} }
.padding(.vertical, 12) .padding(.vertical, 12)
.padding(.horizontal, 18) .padding(.horizontal, 18)
@ -183,28 +189,28 @@ struct HomeTimelineView: View {
// Download statuses from API (which are older then last visible status). // Download statuses from API (which are older then last visible status).
let statuses = try await self.loadFromCacheOrApi(timelineCache: accountData.timelineCache) let statuses = try await self.loadFromCacheOrApi(timelineCache: accountData.timelineCache)
if statuses.isEmpty { if statuses.data.isEmpty {
self.allItemsLoaded = true self.allItemsLoaded = true
return return
} }
// Remember last status id returned by API. // Remember last status id returned by API.
self.lastStatusId = statuses.last?.id self.lastStatusId = statuses.getMaxId()
// Get only visible statuses. // Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId, let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses, statuses: statuses.data,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt, hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext) modelContext: modelContext)
// Remeber first status returned by API in user context (when it's newer then remembered). // Remeber first status returned by API in user context (when it's newer then remembered).
try AccountDataHandler.shared.update(lastSeenStatusId: nil, try AccountDataHandler.shared.update(lastSeenStatusId: nil,
lastLoadedStatusId: statuses.first?.id, lastLoadedStatusId: statuses.getMinId(),
applicationState: self.applicationState, applicationState: self.applicationState,
modelContext: modelContext) modelContext: modelContext)
// Append statuses to viewed. // Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext) try ViewedStatusHandler.shared.append(contentsOf: statuses.data, accountId: accountId, modelContext: modelContext)
// Map to view models. // Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) }) let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
@ -222,24 +228,24 @@ struct HomeTimelineView: View {
// Download statuses from API. // Download statuses from API.
let statuses = try await self.loadFromApi(maxId: lastStatusId) let statuses = try await self.loadFromApi(maxId: lastStatusId)
if statuses.isEmpty { if statuses.data.isEmpty {
self.allItemsLoaded = true self.allItemsLoaded = true
return return
} }
// Now we have new last status. // Now we have new last status.
if let lastStatusId = statuses.last?.id { if let lastStatusId = statuses.getMaxId() {
self.lastStatusId = lastStatusId self.lastStatusId = lastStatusId
} }
// Get only visible statuses. // Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId, let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses, statuses: statuses.data,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt, hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext) modelContext: modelContext)
// Append statuses to viewed. // Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext) try ViewedStatusHandler.shared.append(contentsOf: statuses.data, accountId: accountId, modelContext: modelContext)
// Map to view models. // Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) }) let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
@ -260,29 +266,29 @@ struct HomeTimelineView: View {
// Download statuses from API. // Download statuses from API.
let statuses = try await self.loadFromApi() let statuses = try await self.loadFromApi()
if statuses.isEmpty { if statuses.data.isEmpty {
self.allItemsLoaded = true self.allItemsLoaded = true
return return
} }
// Remember last status id returned by API. // Remember last status id returned by API.
self.lastStatusId = statuses.last?.id self.lastStatusId = statuses.getMaxId()
// Get only visible statuses. // Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId, let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses, statuses: statuses.data,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt, hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext) modelContext: modelContext)
// Remeber first status returned by API in user context (when it's newer then remembered). // Remeber first status returned by API in user context (when it's newer then remembered).
try AccountDataHandler.shared.update(lastSeenStatusId: self.statusViewModels.first?.id, try AccountDataHandler.shared.update(lastSeenStatusId: self.statusViewModels.first?.id,
lastLoadedStatusId: statuses.first?.id, lastLoadedStatusId: statuses.getMinId(),
statuses: statuses, statuses: statuses,
applicationState: self.applicationState, applicationState: self.applicationState,
modelContext: modelContext) modelContext: modelContext)
// Append statuses to viewed. // Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext) try ViewedStatusHandler.shared.append(contentsOf: statuses.data, accountId: accountId, modelContext: modelContext)
// Map to view models. // Map to view models.
let statusModels = visibleStatuses.map({ StatusModel(status: $0) }) let statusModels = visibleStatuses.map({ StatusModel(status: $0) })
@ -297,24 +303,22 @@ struct HomeTimelineView: View {
self.applicationState.amountOfNewStatuses = 0 self.applicationState.amountOfNewStatuses = 0
} }
private func loadFromCacheOrApi(timelineCache: String?) async throws -> [Status] { private func loadFromCacheOrApi(timelineCache: String?) async throws -> Linkable<[Status]> {
if let timelineCache, let timelineCacheData = timelineCache.data(using: .utf8) { if let timelineCache, let timelineCacheData = timelineCache.data(using: .utf8),
let statusesFromCache = try? JSONDecoder().decode([Status].self, from: timelineCacheData) let statusesFromCache = try? JSONDecoder().decode(Linkable<[Status]>.self, from: timelineCacheData) {
if let statusesFromCache { return statusesFromCache
return statusesFromCache
}
} }
return try await self.loadFromApi() return try await self.loadFromApi()
} }
private func loadFromApi(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil) async throws -> [Status] { private func loadFromApi(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil) async throws -> Linkable<[Status]> {
return try await self.client.publicTimeline?.getHomeTimeline( return try await self.client.publicTimeline?.getHomeTimeline(
maxId: maxId, maxId: maxId,
sinceId: sinceId, sinceId: sinceId,
minId: minId, minId: minId,
limit: self.defaultLimit, limit: self.defaultLimit,
includeReblogs: self.applicationState.showReboostedStatuses) ?? [] includeReblogs: self.applicationState.showReboostedStatuses) ?? Linkable(data: [])
} }
private func calculateOffset() { private func calculateOffset() {

View File

@ -182,29 +182,29 @@ struct StatusesView: View {
let statuses = try await self.loadFromApi() let statuses = try await self.loadFromApi()
if statuses.isEmpty { if statuses.data.isEmpty {
self.allItemsLoaded = true self.allItemsLoaded = true
return return
} }
// Remember last status id returned by API. // Remember last status id returned by API.
self.lastStatusId = statuses.last?.id self.lastStatusId = statuses.getMaxId()
// Get only visible statuses. // Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId, let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses, statuses: statuses.data,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt, hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext) modelContext: modelContext)
if self.listType == .home { if self.listType == .home {
// Remeber first status returned by API in user context (when it's newer then remembered). // Remeber first status returned by API in user context (when it's newer then remembered).
try AccountDataHandler.shared.update(lastSeenStatusId: nil, try AccountDataHandler.shared.update(lastSeenStatusId: nil,
lastLoadedStatusId: statuses.first?.id, lastLoadedStatusId: statuses.getMinId(),
applicationState: self.applicationState, applicationState: self.applicationState,
modelContext: modelContext) modelContext: modelContext)
// Append statuses to viewed. // Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext) try ViewedStatusHandler.shared.append(contentsOf: statuses.data, accountId: accountId, modelContext: modelContext)
} }
// Map to view models. // Map to view models.
@ -221,25 +221,25 @@ struct StatusesView: View {
if let lastStatusId = self.lastStatusId, let accountId = self.applicationState.account?.id { if let lastStatusId = self.lastStatusId, let accountId = self.applicationState.account?.id {
let statuses = try await self.loadFromApi(maxId: lastStatusId) let statuses = try await self.loadFromApi(maxId: lastStatusId)
if statuses.isEmpty { if statuses.data.isEmpty {
self.allItemsLoaded = true self.allItemsLoaded = true
return return
} }
// Now we have new last status. // Now we have new last status.
if let lastStatusId = statuses.last?.id { if let lastStatusId = statuses.getMaxId() {
self.lastStatusId = lastStatusId self.lastStatusId = lastStatusId
} }
// Get only visible statuses. // Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId, let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses, statuses: statuses.data,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt, hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext) modelContext: modelContext)
if self.listType == .home { if self.listType == .home {
// Append statuses to viewed. // Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext) try ViewedStatusHandler.shared.append(contentsOf: statuses.data, accountId: accountId, modelContext: modelContext)
} }
// Map to view models. // Map to view models.
@ -260,29 +260,29 @@ struct StatusesView: View {
let statuses = try await self.loadFromApi() let statuses = try await self.loadFromApi()
if statuses.isEmpty { if statuses.data.isEmpty {
self.allItemsLoaded = true self.allItemsLoaded = true
return return
} }
// Remember last status id returned by API. // Remember last status id returned by API.
self.lastStatusId = statuses.last?.id self.lastStatusId = statuses.getMaxId()
// Get only visible statuses. // Get only visible statuses.
let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId, let visibleStatuses = HomeTimelineService.shared.getVisibleStatuses(accountId: accountId,
statuses: statuses, statuses: statuses.data,
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt, hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
modelContext: modelContext) modelContext: modelContext)
if self.listType == .home { if self.listType == .home {
// Remeber first status returned by API in user context (when it's newer then remembered). // Remeber first status returned by API in user context (when it's newer then remembered).
try AccountDataHandler.shared.update(lastSeenStatusId: self.statusViewModels.first?.id, try AccountDataHandler.shared.update(lastSeenStatusId: self.statusViewModels.first?.id,
lastLoadedStatusId: statuses.first?.id, lastLoadedStatusId: statuses.getMinId(),
applicationState: self.applicationState, applicationState: self.applicationState,
modelContext: modelContext) modelContext: modelContext)
// Append statuses to viewed. // Append statuses to viewed.
try ViewedStatusHandler.shared.append(contentsOf: statuses, accountId: accountId, modelContext: modelContext) try ViewedStatusHandler.shared.append(contentsOf: statuses.data, accountId: accountId, modelContext: modelContext)
} }
// Map to view models. // Map to view models.
@ -296,7 +296,7 @@ struct StatusesView: View {
self.statusViewModels = statusModels self.statusViewModels = statusModels
} }
private func loadFromApi(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil) async throws -> [Status] { private func loadFromApi(maxId: String? = nil, sinceId: String? = nil, minId: String? = nil) async throws -> Linkable<[Status]> {
switch self.listType { switch self.listType {
case .home: case .home:
return try await self.client.publicTimeline?.getHomeTimeline( return try await self.client.publicTimeline?.getHomeTimeline(
@ -304,40 +304,44 @@ struct StatusesView: View {
sinceId: sinceId, sinceId: sinceId,
minId: minId, minId: minId,
limit: self.defaultLimit, limit: self.defaultLimit,
includeReblogs: self.applicationState.showReboostedStatuses) ?? [] includeReblogs: self.applicationState.showReboostedStatuses) ?? Linkable(data: [])
case .local: case .local:
return try await self.client.publicTimeline?.getStatuses( return try await self.client.publicTimeline?.getStatuses(
local: true, local: true,
maxId: maxId, maxId: maxId,
sinceId: sinceId, sinceId: sinceId,
minId: minId, minId: minId,
limit: self.defaultLimit) ?? [] limit: self.defaultLimit) ?? Linkable(data: [])
case .federated: case .federated:
return try await self.client.publicTimeline?.getStatuses( return try await self.client.publicTimeline?.getStatuses(
remote: true, remote: true,
maxId: maxId, maxId: maxId,
sinceId: sinceId, sinceId: sinceId,
minId: minId, minId: minId,
limit: self.defaultLimit) ?? [] limit: self.defaultLimit) ?? Linkable(data: [])
case .favourites: case .favourites:
return try await self.client.accounts?.favourites( let favourites = try await self.client.accounts?.favourites(
maxId: maxId, maxId: maxId,
sinceId: sinceId, sinceId: sinceId,
minId: minId, minId: minId,
limit: self.defaultLimit) ?? [] limit: self.defaultLimit) ?? []
return Linkable(data: favourites)
case .bookmarks: case .bookmarks:
return try await self.client.accounts?.bookmarks( let bookmarks = try await self.client.accounts?.bookmarks(
maxId: maxId, maxId: maxId,
sinceId: sinceId, sinceId: sinceId,
minId: minId, minId: minId,
limit: self.defaultLimit) ?? [] limit: self.defaultLimit) ?? []
return Linkable(data: bookmarks)
case .hashtag(let tag): case .hashtag(let tag):
let hashtagsFromApi = try await self.client.search?.search(query: tag, resultsType: .hashtags) let hashtagsFromApi = try await self.client.search?.search(query: tag, resultsType: .hashtags)
guard let hashtagsFromApi = hashtagsFromApi, hashtagsFromApi.hashtags.isEmpty == false else { guard let hashtagsFromApi = hashtagsFromApi, hashtagsFromApi.hashtags.isEmpty == false else {
ToastrService.shared.showError(title: LocalizedStringResource("global.error.hashtagNotExists"), imageSystemName: "exclamationmark.octagon") ToastrService.shared.showError(title: LocalizedStringResource("global.error.hashtagNotExists"), imageSystemName: "exclamationmark.octagon")
dismiss() dismiss()
return [] return Linkable(data: [])
} }
return try await self.client.publicTimeline?.getTagStatuses( return try await self.client.publicTimeline?.getTagStatuses(
@ -345,7 +349,7 @@ struct StatusesView: View {
maxId: maxId, maxId: maxId,
sinceId: sinceId, sinceId: sinceId,
minId: minId, minId: minId,
limit: self.defaultLimit) ?? [] limit: self.defaultLimit) ?? Linkable(data: [])
} }
} }

View File

@ -75,7 +75,7 @@ struct ImagesGrid: View {
do { do {
let statusesFromApi = try await self.loadStatuses() let statusesFromApi = try await self.loadStatuses()
let statusesWithImages = statusesFromApi.getStatusesWithImagesOnly() let statusesWithImages = statusesFromApi.data.getStatusesWithImagesOnly()
let photoUrls = self.getPhotoUrls(statuses: statusesWithImages) let photoUrls = self.getPhotoUrls(statuses: statusesWithImages)
self.prefetch(photoUrls: photoUrls) self.prefetch(photoUrls: photoUrls)
@ -119,15 +119,21 @@ struct ImagesGrid: View {
} }
} }
private func loadStatuses() async throws -> [Status] { private func loadStatuses() async throws -> Linkable<[Status]> {
switch self.gridType { switch self.gridType {
case .hashtag(let name): case .hashtag(let name):
return try await self.client.publicTimeline?.getTagStatuses( return try await self.client.publicTimeline?.getTagStatuses(
tag: name, tag: name,
local: true, local: true,
limit: 10) ?? [] limit: 10) ?? Linkable(data: [])
case .account(let accountId, _, _): case .account(let accountId, _, _):
return try await self.client.accounts?.statuses(createdBy: accountId, onlyMedia: true, limit: 10) ?? [] let accountStatuses = try await self.client.accounts?.statuses(
createdBy: accountId,
onlyMedia: true,
limit: 10
) ?? []
return Linkable(data: accountStatuses)
} }
} }

View File

@ -33,7 +33,7 @@ public class StatusFetcher {
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken) let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(limit: 20, includeReblogs: defaultSettings.showReboostedStatuses, timeoutInterval: 5.0) let statuses = try await client.getHomeTimeline(limit: 20, includeReblogs: defaultSettings.showReboostedStatuses, timeoutInterval: 5.0)
let widgetEntries = await self.prepare(statuses: statuses, length: length) let widgetEntries = await self.prepare(statuses: statuses.data, length: length)
return widgetEntries return widgetEntries
} }
@ -49,11 +49,11 @@ public class StatusFetcher {
let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext) let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext)
guard let timelineCache = accountData?.timelineCache, guard let timelineCache = accountData?.timelineCache,
let timelineCacheData = timelineCache.data(using: .utf8), let timelineCacheData = timelineCache.data(using: .utf8),
let statusesFromCache = try? JSONDecoder().decode([Status].self, from: timelineCacheData) else { let statusesFromCache = try? JSONDecoder().decode(Linkable<[Status]>.self, from: timelineCacheData) else {
return [self.placeholder()] return [self.placeholder()]
} }
let widgetEntries = await self.prepare(statuses: statusesFromCache, length: length) let widgetEntries = await self.prepare(statuses: statusesFromCache.data, length: length)
return widgetEntries return widgetEntries
} }