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,
minId: String? = nil,
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)
}
@ -22,7 +22,7 @@ extension Client {
maxId: String? = nil,
sinceId: 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,
remote: remote,
onlyMedia: true,
@ -38,7 +38,7 @@ extension Client {
maxId: String? = nil,
sinceId: 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,
local: local,
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 {
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" : {
"comment" : "Information message when loading account failed",
"localizations" : {

View File

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

View File

@ -7,7 +7,7 @@
import Foundation
/// 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).
public let data: T
@ -20,3 +20,29 @@ public struct Linkable<T> where T: Codable {
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,
limit: Int? = nil,
includeReblogs: Bool? = nil,
timeoutInterval: Double? = nil) async throws -> [Status] {
timeoutInterval: Double? = nil) async throws -> Linkable<[Status]> {
let request = try Self.request(
for: baseURL,
@ -22,7 +22,7 @@ public extension PixelfedClientAuthenticated {
timeoutInterval: timeoutInterval
)
return try await downloadJson([Status].self, request: request)
return try await downloadJsonWithLink([Status].self, request: request)
}
func getPublicTimeline(local: Bool? = nil,
@ -31,7 +31,7 @@ public extension PixelfedClientAuthenticated {
maxId: EntityId? = nil,
sinceId: EntityId? = nil,
minId: EntityId? = nil,
limit: Limit? = nil) async throws -> [Status] {
limit: Limit? = nil) async throws -> Linkable<[Status]> {
let request = try Self.request(
for: baseURL,
@ -39,7 +39,7 @@ public extension PixelfedClientAuthenticated {
withBearerToken: token
)
return try await downloadJson([Status].self, request: request)
return try await downloadJsonWithLink([Status].self, request: request)
}
func getTagTimeline(tag: String,
@ -49,7 +49,7 @@ public extension PixelfedClientAuthenticated {
maxId: EntityId? = nil,
sinceId: EntityId? = nil,
minId: EntityId? = nil,
limit: Int? = nil) async throws -> [Status] {
limit: Int? = nil) async throws -> Linkable<[Status]> {
let request = try Self.request(
for: baseURL,
@ -57,7 +57,7 @@ public extension PixelfedClientAuthenticated {
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 {

View File

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

View File

@ -43,27 +43,49 @@ public class HomeTimelineService {
let client = PixelfedClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
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.
while true {
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,
includeReblogs: includeReblogs)
guard let firstStatus = downloadedStatuses.first else {
break
// 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: downloadedStatuses,
statuses: temporaryList,
hideStatusesWithoutAlt: hideStatusesWithoutAlt,
modelContext: modelContext)
// Add statuses to the list.
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 {
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadingNewStatuses")
break

View File

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

View File

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

View File

@ -75,7 +75,7 @@ struct ImagesGrid: View {
do {
let statusesFromApi = try await self.loadStatuses()
let statusesWithImages = statusesFromApi.getStatusesWithImagesOnly()
let statusesWithImages = statusesFromApi.data.getStatusesWithImagesOnly()
let photoUrls = self.getPhotoUrls(statuses: statusesWithImages)
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 {
case .hashtag(let name):
return try await self.client.publicTimeline?.getTagStatuses(
tag: name,
local: true,
limit: 10) ?? []
limit: 10) ?? Linkable(data: [])
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 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
}
@ -49,11 +49,11 @@ public class StatusFetcher {
let accountData = AccountDataHandler.shared.getAccountData(accountId: accountId, modelContext: modelContext)
guard let timelineCache = accountData?.timelineCache,
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()]
}
let widgetEntries = await self.prepare(statuses: statusesFromCache, length: length)
let widgetEntries = await self.prepare(statuses: statusesFromCache.data, length: length)
return widgetEntries
}