From 8de2a8192b4fc9954378a0886b62c4203b445350 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sat, 24 Dec 2022 09:21:04 +0100 Subject: [PATCH] Handle HTTP Link header for favourites and followers/following --- .../Account/AccountDetailViewModel.swift | 13 +++++++--- .../AccountsLIst/AccountsListViewModel.swift | 24 ++++++++++++------- Packages/Network/Sources/Network/Client.swift | 20 +++++++++++++--- .../Sources/Network/Endpoint/Accounts.swift | 5 +++- .../Network/Sources/Network/LinkHandler.swift | 18 ++++++++++++++ 5 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 Packages/Network/Sources/Network/LinkHandler.swift diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index 948cea21..3e64e024 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -47,6 +47,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { @Published var title: String = "" @Published var relationship: Relationshionship? @Published var favourites: [Status] = [] + private var favouritesNextPage: LinkHandler? @Published var followedTags: [Tag] = [] @Published var featuredTags: [FeaturedTag] = [] @Published var fields: [Account.Field] = [] @@ -108,7 +109,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { tabState = .statuses(statusesState: .loading) statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil, tag: nil)) if isCurrentUser { - favourites = try await client.get(endpoint: Accounts.favourites) + (favourites, favouritesNextPage) = try await client.getWithLink(endpoint: Accounts.favourites(sinceId: nil)) } reloadTabState() } catch { @@ -126,7 +127,13 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { let newStatuses: [Status] = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: lastId, tag: nil)) statuses.append(contentsOf: newStatuses) tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage)) - case .favourites, .followedTags: + case .favourites: + guard let nextPageId = favouritesNextPage?.maxId else { return } + let newFavourites: [Status] + (newFavourites, favouritesNextPage) = try await client.getWithLink(endpoint: Accounts.favourites(sinceId: nextPageId)) + favourites.append(contentsOf: newFavourites) + tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .hasNextPage)) + case .followedTags: break } } catch { @@ -157,7 +164,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { case .statuses: tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage)) case .favourites: - tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .none)) + tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .hasNextPage)) case .followedTags: tabState = .followedTags(tags: followedTags) } diff --git a/Packages/Account/Sources/Account/AccountsLIst/AccountsListViewModel.swift b/Packages/Account/Sources/Account/AccountsLIst/AccountsListViewModel.swift index 97a936a8..59a2f854 100644 --- a/Packages/Account/Sources/Account/AccountsLIst/AccountsListViewModel.swift +++ b/Packages/Account/Sources/Account/AccountsLIst/AccountsListViewModel.swift @@ -29,6 +29,8 @@ class AccountsListViewModel: ObservableObject { @Published var state = State.loading + private var nextPageId: String? + init(accountId: String, mode: AccountsListMode) { self.accountId = accountId self.mode = mode @@ -38,14 +40,16 @@ class AccountsListViewModel: ObservableObject { guard let client else { return } do { state = .loading + let link: LinkHandler? switch mode { case .followers: - accounts = try await client.get(endpoint: Accounts.followers(id: accountId, - sinceId: nil)) + (accounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId, + sinceId: nil)) case .following: - accounts = try await client.get(endpoint: Accounts.following(id: accountId, - sinceId: nil)) + (accounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId, + sinceId: nil)) } + nextPageId = link?.maxId relationships = try await client.get(endpoint: Accounts.relationships(ids: accounts.map{ $0.id })) state = .display(accounts: accounts, @@ -55,23 +59,25 @@ class AccountsListViewModel: ObservableObject { } func fetchNextPage() async { - guard let client else { return } + guard let client, let nextPageId else { return } do { state = .display(accounts: accounts, relationships: relationships, nextPageState: .loadingNextPage) let newAccounts: [Account] + let link: LinkHandler? switch mode { case .followers: - newAccounts = try await client.get(endpoint: Accounts.followers(id: accountId, - sinceId: accounts.last?.id)) + (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId, + sinceId: nextPageId)) case .following: - newAccounts = try await client.get(endpoint: Accounts.following(id: accountId, - sinceId: accounts.last?.id)) + (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId, + sinceId: nextPageId)) } accounts.append(contentsOf: newAccounts) let newRelationships: [Relationshionship] = try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map{ $0.id })) relationships.append(contentsOf: newRelationships) + self.nextPageId = link?.maxId state = .display(accounts: accounts, relationships: relationships, nextPageState: .hasNextPage) diff --git a/Packages/Network/Sources/Network/Client.swift b/Packages/Network/Sources/Network/Client.swift index c5336c25..b6ae0abb 100644 --- a/Packages/Network/Sources/Network/Client.swift +++ b/Packages/Network/Sources/Network/Client.swift @@ -61,15 +61,29 @@ public class Client: ObservableObject, Equatable { } return request } + + private func makeGet(endpoint: Endpoint) -> URLRequest { + let url = makeURL(endpoint: endpoint) + return makeURLRequest(url: url, httpMethod: "GET") + } public func get(endpoint: Endpoint) async throws -> Entity { - let url = makeURL(endpoint: endpoint) - let request = makeURLRequest(url: url, httpMethod: "GET") - let (data, httpResponse) = try await urlSession.data(for: request) + let (data, httpResponse) = try await urlSession.data(for: makeGet(endpoint: endpoint)) logResponseOnError(httpResponse: httpResponse, data: data) return try decoder.decode(Entity.self, from: data) } + public func getWithLink(endpoint: Endpoint) async throws -> (Entity, LinkHandler?) { + let (data, httpResponse) = try await urlSession.data(for: makeGet(endpoint: endpoint)) + var linkHandler: LinkHandler? + if let response = httpResponse as? HTTPURLResponse, + let link = response.allHeaderFields["Link"] as? String{ + linkHandler = .init(rawLink: link) + } + logResponseOnError(httpResponse: httpResponse, data: data) + return (try decoder.decode(Entity.self, from: data), linkHandler) + } + public func post(endpoint: Endpoint) async throws -> Entity { let url = makeURL(endpoint: endpoint) let request = makeURLRequest(url: url, httpMethod: "POST") diff --git a/Packages/Network/Sources/Network/Endpoint/Accounts.swift b/Packages/Network/Sources/Network/Endpoint/Accounts.swift index a4c3af1f..12bb3c4e 100644 --- a/Packages/Network/Sources/Network/Endpoint/Accounts.swift +++ b/Packages/Network/Sources/Network/Endpoint/Accounts.swift @@ -2,7 +2,7 @@ import Foundation public enum Accounts: Endpoint { case accounts(id: String) - case favourites + case favourites(sinceId: String?) case followedTags case featuredTags(id: String) case verifyCredentials @@ -69,6 +69,9 @@ public enum Accounts: Endpoint { case let .following(_, sinceId): guard let sinceId else { return nil } return [.init(name: "max_id", value: sinceId)] + case let .favourites(sinceId): + guard let sinceId else { return nil } + return [.init(name: "max_id", value: sinceId)] default: return nil } diff --git a/Packages/Network/Sources/Network/LinkHandler.swift b/Packages/Network/Sources/Network/LinkHandler.swift new file mode 100644 index 00000000..73dcf736 --- /dev/null +++ b/Packages/Network/Sources/Network/LinkHandler.swift @@ -0,0 +1,18 @@ +import Foundation +import RegexBuilder + +public struct LinkHandler { + public let rawLink: String + + public var maxId: String? { + do { + let regex = try Regex("max_id=[0-9]+") + if let match = rawLink.firstMatch(of: regex) { + return match.output.first?.substring?.replacingOccurrences(of: "max_id=", with: "") + } + } catch { + return nil + } + return nil + } +}