Timeline: Fetch up to 10 new pages on pull to refresh
This commit is contained in:
parent
7cf233c974
commit
2b733e6b10
@ -51,7 +51,7 @@ struct TimelineTab: View {
|
|||||||
Button {
|
Button {
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
} label: {
|
} label: {
|
||||||
Text(timeline.title())
|
Label(timeline.title(), systemImage: timeline.iconName() ?? "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -16,7 +16,7 @@ public struct Poll: Codable {
|
|||||||
public let expired: Bool
|
public let expired: Bool
|
||||||
public let multiple: Bool
|
public let multiple: Bool
|
||||||
public let votesCount: Int
|
public let votesCount: Int
|
||||||
public let votersCount: Int
|
public let votersCount: Int?
|
||||||
public let voted: Bool
|
public let voted: Bool
|
||||||
public let ownVotes: [Int]
|
public let ownVotes: [Int]
|
||||||
public let options: [Option]
|
public let options: [Option]
|
||||||
|
@ -64,9 +64,9 @@ public enum Accounts: Endpoint {
|
|||||||
case let .familiarFollowers(withAccount):
|
case let .familiarFollowers(withAccount):
|
||||||
return [.init(name: "id[]", value: withAccount)]
|
return [.init(name: "id[]", value: withAccount)]
|
||||||
case let .followers(_, maxId):
|
case let .followers(_, maxId):
|
||||||
return makePaginationParam(sinceId: nil, maxId: maxId)
|
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
|
||||||
case let .following(_, maxId):
|
case let .following(_, maxId):
|
||||||
return makePaginationParam(sinceId: nil, maxId: maxId)
|
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
|
||||||
case let .favourites(sinceId):
|
case let .favourites(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)]
|
||||||
|
@ -6,11 +6,13 @@ public protocol Endpoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Endpoint {
|
extension Endpoint {
|
||||||
func makePaginationParam(sinceId: String?, maxId: String?) -> [URLQueryItem]? {
|
func makePaginationParam(sinceId: String?, maxId: String?, mindId: String?) -> [URLQueryItem]? {
|
||||||
if let sinceId {
|
if let sinceId {
|
||||||
return [.init(name: "since_id", value: sinceId)]
|
return [.init(name: "since_id", value: sinceId)]
|
||||||
} else if let maxId {
|
} else if let maxId {
|
||||||
return [.init(name: "max_id", value: maxId)]
|
return [.init(name: "max_id", value: maxId)]
|
||||||
|
} else if let mindId {
|
||||||
|
return [.init(name: "min_id", value: mindId)]
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ public enum Notifications: Endpoint {
|
|||||||
public func queryItems() -> [URLQueryItem]? {
|
public func queryItems() -> [URLQueryItem]? {
|
||||||
switch self {
|
switch self {
|
||||||
case .notifications(let sinceId, let maxId, let types):
|
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 {
|
if let types {
|
||||||
for type in types {
|
for type in types {
|
||||||
params.append(.init(name: "types[]", value: type))
|
params.append(.init(name: "types[]", value: type))
|
||||||
|
@ -76,9 +76,9 @@ public enum Statuses: Endpoint {
|
|||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
case let .rebloggedBy(_, maxId):
|
case let .rebloggedBy(_, maxId):
|
||||||
return makePaginationParam(sinceId: nil, maxId: maxId)
|
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
|
||||||
case let .favouritedBy(_, maxId):
|
case let .favouritedBy(_, maxId):
|
||||||
return makePaginationParam(sinceId: nil, maxId: maxId)
|
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Timelines: Endpoint {
|
public enum Timelines: Endpoint {
|
||||||
case pub(sinceId: String?, maxId: String?)
|
case pub(sinceId: String?, maxId: String?, minId: String?, local: Bool)
|
||||||
case home(sinceId: String?, maxId: String?)
|
case home(sinceId: String?, maxId: String?, minId: String?)
|
||||||
case hashtag(tag: String, maxId: String?)
|
case hashtag(tag: String, maxId: String?)
|
||||||
|
|
||||||
public func path() -> String {
|
public func path() -> String {
|
||||||
@ -18,12 +18,14 @@ public enum Timelines: Endpoint {
|
|||||||
|
|
||||||
public func queryItems() -> [URLQueryItem]? {
|
public func queryItems() -> [URLQueryItem]? {
|
||||||
switch self {
|
switch self {
|
||||||
case .pub(let sinceId, let maxId):
|
case .pub(let sinceId, let maxId, let minId, let local):
|
||||||
return makePaginationParam(sinceId: sinceId, maxId: maxId)
|
var params = makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: minId) ?? []
|
||||||
case .home(let sinceId, let maxId):
|
params.append(.init(name: "local", value: local ? "true" : "false"))
|
||||||
return makePaginationParam(sinceId: sinceId, maxId: maxId)
|
return params
|
||||||
|
case .home(let sinceId, let maxId, let mindId):
|
||||||
|
return makePaginationParam(sinceId: sinceId, maxId: maxId, mindId: mindId)
|
||||||
case let .hashtag(_, maxId):
|
case let .hashtag(_, maxId):
|
||||||
return makePaginationParam(sinceId: nil, maxId: maxId)
|
return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import Models
|
|||||||
import Network
|
import Network
|
||||||
|
|
||||||
public enum TimelineFilter: Hashable, Equatable {
|
public enum TimelineFilter: Hashable, Equatable {
|
||||||
case pub, home
|
case pub, local, home
|
||||||
case hashtag(tag: String, accountId: String?)
|
case hashtag(tag: String, accountId: String?)
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
@ -11,13 +11,15 @@ public enum TimelineFilter: Hashable, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func availableTimeline() -> [TimelineFilter] {
|
public static func availableTimeline() -> [TimelineFilter] {
|
||||||
return [.pub, .home]
|
return [.pub, .local, .home]
|
||||||
}
|
}
|
||||||
|
|
||||||
public func title() -> String {
|
public func title() -> String {
|
||||||
switch self {
|
switch self {
|
||||||
case .pub:
|
case .pub:
|
||||||
return "Public"
|
return "Federated"
|
||||||
|
case .local:
|
||||||
|
return "Local"
|
||||||
case .home:
|
case .home:
|
||||||
return "Home"
|
return "Home"
|
||||||
case let .hashtag(tag, _):
|
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 {
|
switch self {
|
||||||
case .pub: return Timelines.pub(sinceId: sinceId, maxId: maxId)
|
case .pub:
|
||||||
case .home: return Timelines.home(sinceId: sinceId, maxId: maxId)
|
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):
|
case let .hashtag(tag, accountId):
|
||||||
if let accountId {
|
if let accountId {
|
||||||
return Accounts.statuses(id: accountId, sinceId: nil, tag: tag)
|
return Accounts.statuses(id: accountId, sinceId: nil, tag: tag)
|
||||||
|
@ -46,9 +46,7 @@ public struct TimelineView: View {
|
|||||||
viewModel.timeline = timeline
|
viewModel.timeline = timeline
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
Task {
|
await viewModel.fetchStatuses(userIntent: true)
|
||||||
await viewModel.fetchStatuses(userIntent: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onChange(of: watcher.latestEvent?.id) { id in
|
.onChange(of: watcher.latestEvent?.id) { id in
|
||||||
if let latestEvent = watcher.latestEvent {
|
if let latestEvent = watcher.latestEvent {
|
||||||
|
@ -65,14 +65,16 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
|||||||
if statuses.isEmpty {
|
if statuses.isEmpty {
|
||||||
pendingStatuses = []
|
pendingStatuses = []
|
||||||
statusesState = .loading
|
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)
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||||
} else if let first = statuses.first {
|
} 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 {
|
if userIntent || !pendingStatusesEnabled {
|
||||||
pendingStatuses = []
|
pendingStatuses = []
|
||||||
statuses.insert(contentsOf: newStatuses, at: 0)
|
statuses.insert(contentsOf: newStatuses, at: 0)
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
withAnimation {
|
||||||
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
newStatuses = newStatuses.filter { status in
|
newStatuses = newStatuses.filter { status in
|
||||||
!pendingStatuses.contains(where: { $0.id == status.id })
|
!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 {
|
func fetchNextPage() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
guard let lastId = statuses.last?.id else { return }
|
guard let lastId = statuses.last?.id else { return }
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
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)
|
statuses.append(contentsOf: newStatuses)
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||||
} catch {
|
} catch {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user