Refactor to NextPageView + handle next page loading failure

This commit is contained in:
Thomas Ricouard 2024-02-11 10:58:51 +01:00
parent 0739264005
commit 219703ecc7
11 changed files with 187 additions and 215 deletions

View file

@ -191,55 +191,46 @@ import SwiftUI
} }
} }
func fetchNextPage() async { func fetchNextPage() async throws {
guard let client else { return } guard let client else { return }
do { switch selectedTab {
switch selectedTab { case .statuses, .replies, .boosts, .media:
case .statuses, .replies, .boosts, .media: guard let lastId = statuses.last?.id else { return }
guard let lastId = statuses.last?.id else { return } let newStatuses: [Status] =
if selectedTab == .boosts { try await client.get(endpoint: Accounts.statuses(id: accountId,
statusesState = .display(statuses: boosts, nextPageState: .loadingNextPage) sinceId: lastId,
} else { tag: nil,
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage) onlyMedia: selectedTab == .media,
} excludeReplies: selectedTab != .replies,
let newStatuses: [Status] = excludeReblogs: selectedTab != .boosts,
try await client.get(endpoint: Accounts.statuses(id: accountId, pinned: nil))
sinceId: lastId, statuses.append(contentsOf: newStatuses)
tag: nil, if selectedTab == .boosts {
onlyMedia: selectedTab == .media, let newBoosts = statuses.filter{ $0.reblog != nil }
excludeReplies: selectedTab != .replies, self.boosts.append(contentsOf: newBoosts)
excludeReblogs: selectedTab != .boosts,
pinned: nil))
statuses.append(contentsOf: newStatuses)
if selectedTab == .boosts {
let newBoosts = statuses.filter{ $0.reblog != nil }
self.boosts.append(contentsOf: newBoosts)
}
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
if selectedTab == .boosts {
statusesState = .display(statuses: boosts,
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
} else {
statusesState = .display(statuses: statuses,
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
}
case .favorites:
guard let nextPageId = favoritesNextPage?.maxId else { return }
let newFavorites: [Status]
(newFavorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nextPageId))
favorites.append(contentsOf: newFavorites)
StatusDataControllerProvider.shared.updateDataControllers(for: newFavorites, client: client)
statusesState = .display(statuses: favorites, nextPageState: .hasNextPage)
case .bookmarks:
guard let nextPageId = bookmarksNextPage?.maxId else { return }
let newBookmarks: [Status]
(newBookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nextPageId))
StatusDataControllerProvider.shared.updateDataControllers(for: newBookmarks, client: client)
bookmarks.append(contentsOf: newBookmarks)
statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage)
} }
} catch { StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
statusesState = .error(error: error) if selectedTab == .boosts {
statusesState = .display(statuses: boosts,
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
} else {
statusesState = .display(statuses: statuses,
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
}
case .favorites:
guard let nextPageId = favoritesNextPage?.maxId else { return }
let newFavorites: [Status]
(newFavorites, favoritesNextPage) = try await client.getWithLink(endpoint: Accounts.favorites(sinceId: nextPageId))
favorites.append(contentsOf: newFavorites)
StatusDataControllerProvider.shared.updateDataControllers(for: newFavorites, client: client)
statusesState = .display(statuses: favorites, nextPageState: .hasNextPage)
case .bookmarks:
guard let nextPageId = bookmarksNextPage?.maxId else { return }
let newBookmarks: [Status]
(newBookmarks, bookmarksNextPage) = try await client.getWithLink(endpoint: Accounts.bookmarks(sinceId: nextPageId))
StatusDataControllerProvider.shared.updateDataControllers(for: newBookmarks, client: client)
bookmarks.append(contentsOf: newBookmarks)
statusesState = .display(statuses: bookmarks, nextPageState: .hasNextPage)
} }
} }

View file

@ -134,21 +134,13 @@ public struct AccountsListView: View {
switch nextPageState { switch nextPageState {
case .hasNextPage: case .hasNextPage:
loadingRow NextPageView {
#if !os(visionOS) try await viewModel.fetchNextPage()
.listRowBackground(theme.primaryBackgroundColor) }
#endif #if !os(visionOS)
.onAppear { .listRowBackground(theme.primaryBackgroundColor)
Task { #endif
await viewModel.fetchNextPage()
}
}
case .loadingNextPage:
loadingRow
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
case .none: case .none:
EmptyView() EmptyView()
} }
@ -160,12 +152,4 @@ public struct AccountsListView: View {
#endif #endif
} }
} }
private var loadingRow: some View {
HStack {
Spacer()
ProgressView()
Spacer()
}
}
} }

View file

@ -33,7 +33,7 @@ public enum AccountsListMode {
public enum State { public enum State {
public enum PagingState { public enum PagingState {
case hasNextPage, loadingNextPage, none case hasNextPage, none
} }
case loading case loading
@ -94,42 +94,36 @@ public enum AccountsListMode {
} catch {} } catch {}
} }
func fetchNextPage() async { func fetchNextPage() async throws {
guard let client, let nextPageId else { return } guard let client, let nextPageId else { return }
do { let newAccounts: [Account]
state = .display(accounts: accounts, relationships: relationships, nextPageState: .loadingNextPage) let link: LinkHandler?
let newAccounts: [Account] switch mode {
let link: LinkHandler? case let .followers(accountId):
switch mode { (newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId,
case let .followers(accountId): maxId: nextPageId))
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.followers(id: accountId, case let .following(accountId):
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId,
maxId: nextPageId))
case let .rebloggedBy(statusId):
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId,
maxId: nextPageId)) maxId: nextPageId))
case let .following(accountId): case let .favoritedBy(statusId):
(newAccounts, link) = try await client.getWithLink(endpoint: Accounts.following(id: accountId, (newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
maxId: nextPageId)) maxId: nextPageId))
case let .rebloggedBy(statusId): case .accountsList:
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.rebloggedBy(id: statusId, newAccounts = []
maxId: nextPageId)) link = nil
case let .favoritedBy(statusId):
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
maxId: nextPageId))
case .accountsList:
newAccounts = []
link = nil
}
accounts.append(contentsOf: newAccounts)
let newRelationships: [Relationship] =
try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map(\.id)))
relationships.append(contentsOf: newRelationships)
self.nextPageId = link?.maxId
state = .display(accounts: accounts,
relationships: relationships,
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
} catch {
let logger = Logger(subsystem: "com.icecubesapp", category: "UI")
logger.log(level: .info, "\(error.localizedDescription)")
} }
accounts.append(contentsOf: newAccounts)
let newRelationships: [Relationship] =
try await client.get(endpoint: Accounts.relationships(ids: newAccounts.map(\.id)))
relationships.append(contentsOf: newRelationships)
self.nextPageId = link?.maxId
state = .display(accounts: accounts,
relationships: relationships,
nextPageState: link?.maxId != nil ? .hasNextPage : .none)
} }
func search() async { func search() async {

View file

@ -53,18 +53,14 @@ public class AccountStatusesListViewModel: StatusesFetcher {
} }
} }
public func fetchNextPage() async { public func fetchNextPage() async throws {
guard let client, let nextId = nextPage?.maxId else { return } guard let client, let nextId = nextPage?.maxId else { return }
var newStatuses: [Status] = []
(newStatuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nextId))
statuses.append(contentsOf: newStatuses)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
statusesState = .display(statuses: statuses, statusesState = .display(statuses: statuses,
nextPageState: .loadingNextPage) nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
do {
var newStatuses: [Status] = []
(newStatuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nextId))
statuses.append(contentsOf: newStatuses)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
statusesState = .display(statuses: statuses,
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
} catch { }
} }
public func statusDidAppear(status: Status) { public func statusDidAppear(status: Status) {

View file

@ -0,0 +1,53 @@
import SwiftUI
public struct NextPageView: View {
@State private var isLoadingNextPage: Bool = false
@State private var showRetry: Bool = false
let loadNextPage: (() async throws -> Void)
public init(loadNextPage: @escaping (() async throws -> Void)) {
self.loadNextPage = loadNextPage
}
public var body: some View {
HStack {
Spacer()
if showRetry {
Button {
Task {
showRetry = false
await executeTask()
}
} label: {
Label("action.retry", systemImage: "arrow.clockwise")
}
.buttonStyle(.bordered)
} else {
Label("placeholder.loading.short", systemImage: "arrow.down")
.font(.footnote)
.foregroundStyle(.secondary)
.symbolEffect(.pulse, value: isLoadingNextPage)
}
Spacer()
}
.task {
await executeTask()
}
.listRowSeparator(.hidden, edges: .all)
}
private func executeTask() async {
showRetry = false
defer {
isLoadingNextPage = false
}
guard !isLoadingNextPage else { return }
isLoadingNextPage = true
do {
try await loadNextPage()
} catch {
showRetry = true
}
}
}

View file

@ -25,25 +25,13 @@ public struct TrendingLinksListView: View {
#endif #endif
.padding(.vertical, 8) .padding(.vertical, 8)
} }
HStack { NextPageView {
Spacer() let nextPage: [Card] = try await client.get(endpoint: Trends.links(offset: links.count))
ProgressView() links.append(contentsOf: nextPage)
Spacer()
} }
#if !os(visionOS) #if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)
#endif #endif
.task {
defer {
isLoadingNextPage = false
}
guard !isLoadingNextPage else { return }
isLoadingNextPage = true
do {
let nextPage: [Card] = try await client.get(endpoint: Trends.links(offset: links.count))
links.append(contentsOf: nextPage)
} catch { }
}
} }
#if os(visionOS) #if os(visionOS)
.listStyle(.insetGrouped) .listStyle(.insetGrouped)

View file

@ -180,20 +180,22 @@ public struct NotificationsListView: View {
#endif #endif
.id(notification.id) .id(notification.id)
} }
}
switch nextPageState { switch nextPageState {
case .none: case .none:
EmptyView() EmptyView()
case .hasNextPage: case .hasNextPage:
loadingRow NextPageView {
.onAppear { try await viewModel.fetchNextPage()
Task {
await viewModel.fetchNextPage()
}
} }
case .loadingNextPage: .listRowInsets(.init(top: .layoutPadding,
loadingRow leading: .layoutPadding + 4,
bottom: .layoutPadding,
trailing: .layoutPadding))
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
} }
case .error: case .error:
@ -212,21 +214,6 @@ public struct NotificationsListView: View {
} }
} }
private var loadingRow: some View {
HStack {
Spacer()
ProgressView()
Spacer()
}
.listRowInsets(.init(top: .layoutPadding,
leading: .layoutPadding + 4,
bottom: .layoutPadding,
trailing: .layoutPadding))
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
private var topPaddingView: some View { private var topPaddingView: some View {
HStack {} HStack {}
.listRowBackground(Color.clear) .listRowBackground(Color.clear)

View file

@ -9,7 +9,7 @@ import SwiftUI
@Observable class NotificationsViewModel { @Observable class NotificationsViewModel {
public enum State { public enum State {
public enum PagingState { public enum PagingState {
case none, hasNextPage, loadingNextPage case none, hasNextPage
} }
case loading case loading
@ -143,25 +143,20 @@ import SwiftUI
return allNotifications return allNotifications
} }
func fetchNextPage() async { func fetchNextPage() async throws {
guard let client else { return } guard let client else { return }
do { guard let lastId = consolidatedNotifications.last?.notificationIds.last else { return }
guard let lastId = consolidatedNotifications.last?.notificationIds.last else { return } let newNotifications: [Models.Notification] =
state = .display(notifications: consolidatedNotifications, nextPageState: .loadingNextPage) try await client.get(endpoint: Notifications.notifications(minId: nil,
let newNotifications: [Models.Notification] = maxId: lastId,
try await client.get(endpoint: Notifications.notifications(minId: nil, types: queryTypes,
maxId: lastId, limit: Constants.notificationLimit))
types: queryTypes, await consolidatedNotifications.append(contentsOf: newNotifications.consolidated(selectedType: selectedType))
limit: Constants.notificationLimit)) if consolidatedNotifications.contains(where: { $0.type == .follow_request }) {
await consolidatedNotifications.append(contentsOf: newNotifications.consolidated(selectedType: selectedType)) await currentAccount?.fetchFollowerRequests()
if consolidatedNotifications.contains(where: { $0.type == .follow_request }) {
await currentAccount?.fetchFollowerRequests()
}
state = .display(notifications: consolidatedNotifications,
nextPageState: newNotifications.count < Constants.notificationLimit ? .none : .hasNextPage)
} catch {
state = .error(error: error)
} }
state = .display(notifications: consolidatedNotifications,
nextPageState: newNotifications.count < Constants.notificationLimit ? .none : .hasNextPage)
} }
func markAsRead() { func markAsRead() {

View file

@ -5,7 +5,7 @@ import SwiftUI
public enum StatusesState { public enum StatusesState {
public enum PagingState { public enum PagingState {
case hasNextPage, loadingNextPage, none case hasNextPage, none
} }
case loading case loading
@ -17,7 +17,7 @@ public enum StatusesState {
public protocol StatusesFetcher { public protocol StatusesFetcher {
var statusesState: StatusesState { get } var statusesState: StatusesState { get }
func fetchNewestStatuses(pullToRefresh: Bool) async func fetchNewestStatuses(pullToRefresh: Bool) async
func fetchNextPage() async func fetchNextPage() async throws
func statusDidAppear(status: Status) func statusDidAppear(status: Status)
func statusDidDisappear(status: Status) func statusDidDisappear(status: Status)
} }

View file

@ -60,31 +60,17 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
} }
switch nextPageState { switch nextPageState {
case .hasNextPage: case .hasNextPage:
loadingRow NextPageView {
.id(UUID().uuidString) try await fetcher.fetchNextPage()
.onAppear { }
Task { .padding(.horizontal, .layoutPadding)
await fetcher.fetchNextPage() #if !os(visionOS)
} .listRowBackground(theme.primaryBackgroundColor)
} #endif
case .loadingNextPage:
loadingRow
.id(UUID().uuidString)
case .none: case .none:
EmptyView() EmptyView()
} }
} }
} }
private var loadingRow: some View {
HStack {
Spacer()
ProgressView()
Spacer()
}
.padding(.horizontal, .layoutPadding)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
} }

View file

@ -417,26 +417,24 @@ extension TimelineViewModel: StatusesFetcher {
return allStatuses return allStatuses
} }
func fetchNextPage() async { enum NextPageError: Error {
guard let client else { return } case internalError
do { }
let statuses = await datasource.get()
guard let lastId = statuses.last?.id else { return } func fetchNextPage() async throws {
statusesState = await .display(statuses: datasource.getFiltered(), nextPageState: .loadingNextPage) let statuses = await datasource.get()
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, guard let client, let lastId = statuses.last?.id else { throw NextPageError.internalError }
maxId: lastId, let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
minId: nil, maxId: lastId,
offset: statuses.count)) minId: nil,
offset: statuses.count))
await datasource.append(contentOf: newStatuses) await datasource.append(contentOf: newStatuses)
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client) StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
statusesState = await .display(statuses: datasource.getFiltered(), statusesState = await .display(statuses: datasource.getFiltered(),
nextPageState: newStatuses.count < 20 ? .none : .hasNextPage) nextPageState: newStatuses.count < 20 ? .none : .hasNextPage)
} catch {
statusesState = .error(error: error)
}
} }
func statusDidAppear(status: Status) { func statusDidAppear(status: Status) {