Timeline: Fetch up to 10 new pages on pull to refresh

This commit is contained in:
Thomas Ricouard 2022-12-28 19:10:13 +01:00
parent 7cf233c974
commit 2b733e6b10
10 changed files with 71 additions and 28 deletions

View file

@ -51,7 +51,7 @@ struct TimelineTab: View {
Button {
self.timeline = timeline
} label: {
Text(timeline.title())
Label(timeline.title(), systemImage: timeline.iconName() ?? "")
}
}
} label: {

View file

@ -16,7 +16,7 @@ public struct Poll: Codable {
public let expired: Bool
public let multiple: Bool
public let votesCount: Int
public let votersCount: Int
public let votersCount: Int?
public let voted: Bool
public let ownVotes: [Int]
public let options: [Option]

View file

@ -64,9 +64,9 @@ public enum Accounts: Endpoint {
case let .familiarFollowers(withAccount):
return [.init(name: "id[]", value: withAccount)]
case let .followers(_, maxId):
return makePaginationParam(sinceId: nil, maxId: maxId)
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
case let .following(_, maxId):
return makePaginationParam(sinceId: nil, maxId: maxId)
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
case let .favourites(sinceId):
guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)]

View file

@ -6,11 +6,13 @@ public protocol Endpoint {
}
extension Endpoint {
func makePaginationParam(sinceId: String?, maxId: String?) -> [URLQueryItem]? {
func makePaginationParam(sinceId: String?, maxId: String?, mindId: String?) -> [URLQueryItem]? {
if let sinceId {
return [.init(name: "since_id", value: sinceId)]
} else if let maxId {
return [.init(name: "max_id", value: maxId)]
} else if let mindId {
return [.init(name: "min_id", value: mindId)]
}
return nil
}

View file

@ -15,7 +15,7 @@ public enum Notifications: Endpoint {
public func queryItems() -> [URLQueryItem]? {
switch self {
case .notifications(let sinceId, let maxId, let types):
var params = makePaginationParam(sinceId: sinceId, maxId: maxId) ?? []
var params = makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: nil) ?? []
if let types {
for type in types {
params.append(.init(name: "types[]", value: type))

View file

@ -76,9 +76,9 @@ public enum Statuses: Endpoint {
}
return params
case let .rebloggedBy(_, maxId):
return makePaginationParam(sinceId: nil, maxId: maxId)
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
case let .favouritedBy(_, maxId):
return makePaginationParam(sinceId: nil, maxId: maxId)
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
default:
return nil
}

View file

@ -1,8 +1,8 @@
import Foundation
public enum Timelines: Endpoint {
case pub(sinceId: String?, maxId: String?)
case home(sinceId: String?, maxId: String?)
case pub(sinceId: String?, maxId: String?, minId: String?, local: Bool)
case home(sinceId: String?, maxId: String?, minId: String?)
case hashtag(tag: String, maxId: String?)
public func path() -> String {
@ -18,12 +18,14 @@ public enum Timelines: Endpoint {
public func queryItems() -> [URLQueryItem]? {
switch self {
case .pub(let sinceId, let maxId):
return makePaginationParam(sinceId: sinceId, maxId: maxId)
case .home(let sinceId, let maxId):
return makePaginationParam(sinceId: sinceId, maxId: maxId)
case .pub(let sinceId, let maxId, let minId, let local):
var params = makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: minId) ?? []
params.append(.init(name: "local", value: local ? "true" : "false"))
return params
case .home(let sinceId, let maxId, let mindId):
return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId)
case let .hashtag(_, maxId):
return makePaginationParam(sinceId: nil, maxId: maxId)
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
}
}
}

View file

@ -3,7 +3,7 @@ import Models
import Network
public enum TimelineFilter: Hashable, Equatable {
case pub, home
case pub, local, home
case hashtag(tag: String, accountId: String?)
public func hash(into hasher: inout Hasher) {
@ -11,13 +11,15 @@ public enum TimelineFilter: Hashable, Equatable {
}
public static func availableTimeline() -> [TimelineFilter] {
return [.pub, .home]
return [.pub, .local, .home]
}
public func title() -> String {
switch self {
case .pub:
return "Public"
return "Federated"
case .local:
return "Local"
case .home:
return "Home"
case let .hashtag(tag, _):
@ -25,10 +27,24 @@ public enum TimelineFilter: Hashable, Equatable {
}
}
public func endpoint(sinceId: String?, maxId: String?) -> Endpoint {
public func iconName() -> String? {
switch self {
case .pub: return Timelines.pub(sinceId: sinceId, maxId: maxId)
case .home: return Timelines.home(sinceId: sinceId, maxId: maxId)
case .pub:
return "globe.americas"
case .local:
return "person.3"
case .home:
return "house"
default:
return nil
}
}
public func endpoint(sinceId: String?, maxId: String?, minId: String?) -> Endpoint {
switch self {
case .pub: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: false)
case .local: return Timelines.pub(sinceId: sinceId, maxId: maxId, minId: minId, local: true)
case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId)
case let .hashtag(tag, accountId):
if let accountId {
return Accounts.statuses(id: accountId, sinceId: nil, tag: tag)

View file

@ -46,9 +46,7 @@ public struct TimelineView: View {
viewModel.timeline = timeline
}
.refreshable {
Task {
await viewModel.fetchStatuses(userIntent: true)
}
await viewModel.fetchStatuses(userIntent: true)
}
.onChange(of: watcher.latestEvent?.id) { id in
if let latestEvent = watcher.latestEvent {

View file

@ -65,14 +65,16 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
if statuses.isEmpty {
pendingStatuses = []
statusesState = .loading
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil, maxId: nil))
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil, maxId: nil, minId: nil))
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
} else if let first = statuses.first {
var newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: first.id, maxId: nil))
var newStatuses: [Status] = await fetchNewPages(minId: first.id, maxPages: 10)
if userIntent || !pendingStatusesEnabled {
pendingStatuses = []
statuses.insert(contentsOf: newStatuses, at: 0)
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
withAnimation {
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
}
} else {
newStatuses = newStatuses.filter { status in
!pendingStatuses.contains(where: { $0.id == status.id })
@ -87,12 +89,35 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
}
}
func fetchNewPages(minId: String, maxPages: Int) async -> [Status] {
guard let client else { return [] }
var pagesLoaded = 0
var allStatuses: [Status] = []
var latestMinId = minId
do {
while let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
maxId: nil,
minId: latestMinId)),
!newStatuses.isEmpty,
pagesLoaded < maxPages {
pagesLoaded += 1
allStatuses.insert(contentsOf: newStatuses, at: 0)
latestMinId = newStatuses.first?.id ?? ""
}
} catch {
return []
}
return allStatuses
}
func fetchNextPage() async {
guard let client else { return }
do {
guard let lastId = statuses.last?.id else { return }
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, maxId: lastId))
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
maxId: lastId,
minId: nil))
statuses.append(contentsOf: newStatuses)
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
} catch {