Handle HTTP Link header for favourites and followers/following

This commit is contained in:
Thomas Ricouard 2022-12-24 09:21:04 +01:00
parent a90a63bf1b
commit 8de2a8192b
5 changed files with 64 additions and 16 deletions

View file

@ -47,6 +47,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
@Published var title: String = "" @Published var title: String = ""
@Published var relationship: Relationshionship? @Published var relationship: Relationshionship?
@Published var favourites: [Status] = [] @Published var favourites: [Status] = []
private var favouritesNextPage: LinkHandler?
@Published var followedTags: [Tag] = [] @Published var followedTags: [Tag] = []
@Published var featuredTags: [FeaturedTag] = [] @Published var featuredTags: [FeaturedTag] = []
@Published var fields: [Account.Field] = [] @Published var fields: [Account.Field] = []
@ -108,7 +109,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
tabState = .statuses(statusesState: .loading) tabState = .statuses(statusesState: .loading)
statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil, tag: nil)) statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil, tag: nil))
if isCurrentUser { if isCurrentUser {
favourites = try await client.get(endpoint: Accounts.favourites) (favourites, favouritesNextPage) = try await client.getWithLink(endpoint: Accounts.favourites(sinceId: nil))
} }
reloadTabState() reloadTabState()
} catch { } 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)) let newStatuses: [Status] = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: lastId, tag: nil))
statuses.append(contentsOf: newStatuses) statuses.append(contentsOf: newStatuses)
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage)) 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 break
} }
} catch { } catch {
@ -157,7 +164,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
case .statuses: case .statuses:
tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage)) tabState = .statuses(statusesState: .display(statuses: statuses, nextPageState: .hasNextPage))
case .favourites: case .favourites:
tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .none)) tabState = .statuses(statusesState: .display(statuses: favourites, nextPageState: .hasNextPage))
case .followedTags: case .followedTags:
tabState = .followedTags(tags: followedTags) tabState = .followedTags(tags: followedTags)
} }

View file

@ -29,6 +29,8 @@ class AccountsListViewModel: ObservableObject {
@Published var state = State.loading @Published var state = State.loading
private var nextPageId: String?
init(accountId: String, mode: AccountsListMode) { init(accountId: String, mode: AccountsListMode) {
self.accountId = accountId self.accountId = accountId
self.mode = mode self.mode = mode
@ -38,14 +40,16 @@ class AccountsListViewModel: ObservableObject {
guard let client else { return } guard let client else { return }
do { do {
state = .loading state = .loading
let link: LinkHandler?
switch mode { switch mode {
case .followers: case .followers:
accounts = try await client.get(endpoint: Accounts.followers(id: accountId, (accounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId,
sinceId: nil)) sinceId: nil))
case .following: case .following:
accounts = try await client.get(endpoint: Accounts.following(id: accountId, (accounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId,
sinceId: nil)) sinceId: nil))
} }
nextPageId = link?.maxId
relationships = try await client.get(endpoint: relationships = try await client.get(endpoint:
Accounts.relationships(ids: accounts.map{ $0.id })) Accounts.relationships(ids: accounts.map{ $0.id }))
state = .display(accounts: accounts, state = .display(accounts: accounts,
@ -55,23 +59,25 @@ class AccountsListViewModel: ObservableObject {
} }
func fetchNextPage() async { func fetchNextPage() async {
guard let client else { return } guard let client, let nextPageId else { return }
do { do {
state = .display(accounts: accounts, relationships: relationships, nextPageState: .loadingNextPage) state = .display(accounts: accounts, relationships: relationships, nextPageState: .loadingNextPage)
let newAccounts: [Account] let newAccounts: [Account]
let link: LinkHandler?
switch mode { switch mode {
case .followers: case .followers:
newAccounts = try await client.get(endpoint: Accounts.followers(id: accountId, (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId,
sinceId: accounts.last?.id)) sinceId: nextPageId))
case .following: case .following:
newAccounts = try await client.get(endpoint: Accounts.following(id: accountId, (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId,
sinceId: accounts.last?.id)) sinceId: nextPageId))
} }
accounts.append(contentsOf: newAccounts) accounts.append(contentsOf: newAccounts)
let newRelationships: [Relationshionship] = let newRelationships: [Relationshionship] =
try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map{ $0.id })) try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map{ $0.id }))
relationships.append(contentsOf: newRelationships) relationships.append(contentsOf: newRelationships)
self.nextPageId = link?.maxId
state = .display(accounts: accounts, state = .display(accounts: accounts,
relationships: relationships, relationships: relationships,
nextPageState: .hasNextPage) nextPageState: .hasNextPage)

View file

@ -61,15 +61,29 @@ public class Client: ObservableObject, Equatable {
} }
return request return request
} }
private func makeGet(endpoint: Endpoint) -> URLRequest {
let url = makeURL(endpoint: endpoint)
return makeURLRequest(url: url, httpMethod: "GET")
}
public func get<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity { public func get<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity {
let url = makeURL(endpoint: endpoint) let (data, httpResponse) = try await urlSession.data(for: makeGet(endpoint: endpoint))
let request = makeURLRequest(url: url, httpMethod: "GET")
let (data, httpResponse) = try await urlSession.data(for: request)
logResponseOnError(httpResponse: httpResponse, data: data) logResponseOnError(httpResponse: httpResponse, data: data)
return try decoder.decode(Entity.self, from: data) return try decoder.decode(Entity.self, from: data)
} }
public func getWithLink<Entity: Decodable>(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<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity { public func post<Entity: Decodable>(endpoint: Endpoint) async throws -> Entity {
let url = makeURL(endpoint: endpoint) let url = makeURL(endpoint: endpoint)
let request = makeURLRequest(url: url, httpMethod: "POST") let request = makeURLRequest(url: url, httpMethod: "POST")

View file

@ -2,7 +2,7 @@ import Foundation
public enum Accounts: Endpoint { public enum Accounts: Endpoint {
case accounts(id: String) case accounts(id: String)
case favourites case favourites(sinceId: String?)
case followedTags case followedTags
case featuredTags(id: String) case featuredTags(id: String)
case verifyCredentials case verifyCredentials
@ -69,6 +69,9 @@ public enum Accounts: Endpoint {
case let .following(_, sinceId): case let .following(_, sinceId):
guard let sinceId else { return nil } guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)] 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: default:
return nil return nil
} }

View file

@ -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
}
}