Timeline: Add indicator when loading new posts

This commit is contained in:
Thomas Ricouard 2024-01-04 12:56:46 +01:00
parent c43d1d0dda
commit 3229bf0cb5
8 changed files with 43 additions and 24 deletions

View file

@ -112,7 +112,7 @@ public struct AccountDetailView: View {
group.addTask { await viewModel.fetchAccount() } group.addTask { await viewModel.fetchAccount() }
group.addTask { group.addTask {
if await viewModel.statuses.isEmpty { if await viewModel.statuses.isEmpty {
await viewModel.fetchNewestStatuses() await viewModel.fetchNewestStatuses(pullToRefresh: false)
} }
} }
if !viewModel.isCurrentUser { if !viewModel.isCurrentUser {
@ -126,7 +126,7 @@ public struct AccountDetailView: View {
SoundEffectManager.shared.playSound(.pull) SoundEffectManager.shared.playSound(.pull)
HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.3))
await viewModel.fetchAccount() await viewModel.fetchAccount()
await viewModel.fetchNewestStatuses() await viewModel.fetchNewestStatuses(pullToRefresh: true)
HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7)) HapticManager.shared.fireHaptic(.dataRefresh(intensity: 0.7))
SoundEffectManager.shared.playSound(.refresh) SoundEffectManager.shared.playSound(.refresh)
} }

View file

@ -88,7 +88,7 @@ import SwiftUI
case .statuses, .postsAndReplies, .media: case .statuses, .postsAndReplies, .media:
tabTask?.cancel() tabTask?.cancel()
tabTask = Task { tabTask = Task {
await fetchNewestStatuses() await fetchNewestStatuses(pullToRefresh: false)
} }
default: default:
reloadTabState() reloadTabState()
@ -170,7 +170,7 @@ import SwiftUI
self.familiarFollowers = familiarFollowers?.first?.accounts ?? [] self.familiarFollowers = familiarFollowers?.first?.accounts ?? []
} }
func fetchNewestStatuses() async { func fetchNewestStatuses(pullToRefresh: Bool) async {
guard let client else { return } guard let client else { return }
do { do {
tabState = .statuses(statusesState: .loading) tabState = .statuses(statusesState: .loading)

View file

@ -30,12 +30,12 @@ public struct AccountStatusesListView: View {
.navigationTitle(viewModel.mode.title) .navigationTitle(viewModel.mode.title)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.refreshable { .refreshable {
await viewModel.fetchNewestStatuses() await viewModel.fetchNewestStatuses(pullToRefresh: true)
} }
.task { .task {
guard !isLoaded else { return } guard !isLoaded else { return }
viewModel.client = client viewModel.client = client
await viewModel.fetchNewestStatuses() await viewModel.fetchNewestStatuses(pullToRefresh: false)
isLoaded = true isLoaded = true
} }
} }

View file

@ -40,7 +40,7 @@ public class AccountStatusesListViewModel: StatusesFetcher {
self.mode = mode self.mode = mode
} }
public func fetchNewestStatuses() async { public func fetchNewestStatuses(pullToRefresh: Bool) async {
guard let client else { return } guard let client else { return }
statusesState = .loading statusesState = .loading
do { do {

View file

@ -16,7 +16,7 @@ public enum StatusesState {
@MainActor @MainActor
public protocol StatusesFetcher { public protocol StatusesFetcher {
var statusesState: StatusesState { get } var statusesState: StatusesState { get }
func fetchNewestStatuses() async func fetchNewestStatuses(pullToRefresh: Bool) async
func fetchNextPage() async func fetchNextPage() async
func statusDidAppear(status: Status) func statusDidAppear(status: Status)
func statusDidDisappear(status: Status) func statusDidDisappear(status: Status)

View file

@ -40,7 +40,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
buttonTitle: "action.retry") buttonTitle: "action.retry")
{ {
Task { Task {
await fetcher.fetchNewestStatuses() await fetcher.fetchNewestStatuses(pullToRefresh: false)
} }
} }
.listRowBackground(theme.primaryBackgroundColor) .listRowBackground(theme.primaryBackgroundColor)

View file

@ -11,6 +11,8 @@ import DesignSystem
var disableUpdate: Bool = false var disableUpdate: Bool = false
var scrollToIndex: ((Int) -> Void)? var scrollToIndex: ((Int) -> Void)?
var isLoadingNewStatuses: Bool = false
var pendingStatuses: [String] = [] { var pendingStatuses: [String] = [] {
didSet { didSet {
@ -36,17 +38,26 @@ struct TimelineUnreadStatusesView: View {
@Environment(Theme.self) private var theme @Environment(Theme.self) private var theme
var body: some View { var body: some View {
if observer.pendingStatusesCount > 0 { if observer.pendingStatusesCount > 0 || observer.isLoadingNewStatuses {
Button { Button {
observer.scrollToIndex?(observer.pendingStatusesCount) observer.scrollToIndex?(observer.pendingStatusesCount)
} label: { } label: {
Text("\(observer.pendingStatusesCount)") HStack {
.contentTransition(.numericText(value: Double(observer.pendingStatusesCount))) if observer.isLoadingNewStatuses {
// Accessibility: this results in a frame with a size of at least 44x44 at regular font size ProgressView()
.frame(minWidth: 16, minHeight: 16) .tint(theme.labelColor)
.font(.footnote.monospacedDigit()) .transition(.scale)
.fontWeight(.bold) }
.foregroundStyle(theme.labelColor) if observer.pendingStatusesCount > 0 {
Text("\(observer.pendingStatusesCount)")
.contentTransition(.numericText(value: Double(observer.pendingStatusesCount)))
// Accessibility: this results in a frame with a size of at least 44x44 at regular font size
.frame(minWidth: 16, minHeight: 16)
.font(.footnote.monospacedDigit())
.fontWeight(.bold)
.foregroundStyle(theme.labelColor)
}
}
} }
.accessibilityLabel("accessibility.tabs.timeline.unread-posts.label-\(observer.pendingStatusesCount)") .accessibilityLabel("accessibility.tabs.timeline.unread-posts.label-\(observer.pendingStatusesCount)")
.accessibilityHint("accessibility.tabs.timeline.unread-posts.hint") .accessibilityHint("accessibility.tabs.timeline.unread-posts.hint")

View file

@ -30,7 +30,7 @@ import SwiftUI
return return
} }
await fetchNewestStatuses() await fetchNewestStatuses(pullToRefresh: false)
switch timeline { switch timeline {
case let .hashtag(tag, _): case let .hashtag(tag, _):
await fetchTag(id: tag) await fetchTag(id: tag)
@ -55,7 +55,13 @@ import SwiftUI
@ObservationIgnored @ObservationIgnored
private var visibileStatuses: [Status] = [] private var visibileStatuses: [Status] = []
private var canStreamEvents: Bool = true private var canStreamEvents: Bool = true {
didSet {
if canStreamEvents {
pendingStatusesObserver.isLoadingNewStatuses = false
}
}
}
@ObservationIgnored @ObservationIgnored
var canFilterTimeline: Bool = true var canFilterTimeline: Bool = true
@ -186,7 +192,7 @@ extension TimelineViewModel: StatusesFetcher {
if !timeline.supportNewestPagination || UserPreferences.shared.fastRefreshEnabled { if !timeline.supportNewestPagination || UserPreferences.shared.fastRefreshEnabled {
await reset() await reset()
} }
await fetchNewestStatuses() await fetchNewestStatuses(pullToRefresh: true)
} }
func refreshTimeline() { func refreshTimeline() {
@ -195,7 +201,7 @@ extension TimelineViewModel: StatusesFetcher {
if UserPreferences.shared.fastRefreshEnabled { if UserPreferences.shared.fastRefreshEnabled {
await reset() await reset()
} }
await fetchNewestStatuses() await fetchNewestStatuses(pullToRefresh: false)
} }
} }
@ -218,10 +224,10 @@ extension TimelineViewModel: StatusesFetcher {
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
} }
await fetchNewestStatuses() await fetchNewestStatuses(pullToRefresh: false)
} }
func fetchNewestStatuses() async { func fetchNewestStatuses(pullToRefresh: Bool) async {
guard let client else { return } guard let client else { return }
do { do {
if let marker { if let marker {
@ -229,6 +235,7 @@ extension TimelineViewModel: StatusesFetcher {
} else if await datasource.isEmpty { } else if await datasource.isEmpty {
try await fetchFirstPage(client: client) try await fetchFirstPage(client: client)
} else if let latest = await datasource.get().first, timeline.supportNewestPagination { } else if let latest = await datasource.get().first, timeline.supportNewestPagination {
pendingStatusesObserver.isLoadingNewStatuses = !pullToRefresh
try await fetchNewPagesFrom(latestStatus: latest.id, client: client) try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
} }
} catch { } catch {
@ -269,7 +276,7 @@ extension TimelineViewModel: StatusesFetcher {
} }
} }
// And then we fetch statuses again toget newest statuses from there. // And then we fetch statuses again toget newest statuses from there.
await fetchNewestStatuses() await fetchNewestStatuses(pullToRefresh: false)
} else { } else {
var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
maxId: nil, maxId: nil,
@ -370,6 +377,7 @@ extension TimelineViewModel: StatusesFetcher {
!Task.isCancelled, !Task.isCancelled,
let latest = await datasource.get().first let latest = await datasource.get().first
{ {
pendingStatusesObserver.isLoadingNewStatuses = true
try await fetchNewPagesFrom(latestStatus: latest.id, client: client) try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
} }
} }