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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,6 +12,8 @@ import DesignSystem
var disableUpdate: Bool = false
var scrollToIndex: ((Int) -> Void)?
var isLoadingNewStatuses: Bool = false
var pendingStatuses: [String] = [] {
didSet {
withAnimation(.default) {
@ -36,17 +38,26 @@ struct TimelineUnreadStatusesView: View {
@Environment(Theme.self) private var theme
var body: some View {
if observer.pendingStatusesCount > 0 {
if observer.pendingStatusesCount > 0 || observer.isLoadingNewStatuses {
Button {
observer.scrollToIndex?(observer.pendingStatusesCount)
} label: {
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)
HStack {
if observer.isLoadingNewStatuses {
ProgressView()
.tint(theme.labelColor)
.transition(.scale)
}
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)")
.accessibilityHint("accessibility.tabs.timeline.unread-posts.hint")

View file

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