Timeline scroll to top UX / Flow

This commit is contained in:
Thomas Ricouard 2022-12-25 18:43:15 +01:00
parent 73fde0f6aa
commit 11d4d20873
2 changed files with 39 additions and 8 deletions

View file

@ -18,13 +18,21 @@ public struct TimelineView: View {
} }
public var body: some View { public var body: some View {
ScrollView { ScrollViewReader { proxy in
LazyVStack { ZStack(alignment: .top) {
tagHeaderView ScrollView {
.padding(.bottom, 16) LazyVStack {
StatusesListView(fetcher: viewModel) tagHeaderView
.padding(.bottom, 16)
.id("top")
StatusesListView(fetcher: viewModel)
}
.padding(.top, DS.Constants.layoutPadding)
}
if filter == .home {
makePendingNewPostsView(proxy: proxy)
}
} }
.padding(.top, DS.Constants.layoutPadding)
} }
.navigationTitle(filter?.title() ?? viewModel.timeline.title()) .navigationTitle(filter?.title() ?? viewModel.timeline.title())
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
@ -53,6 +61,22 @@ public struct TimelineView: View {
} }
} }
@ViewBuilder
private func makePendingNewPostsView(proxy: ScrollViewProxy) -> some View {
if !viewModel.pendingStatuses.isEmpty {
Button {
proxy.scrollTo("top")
viewModel.displayPendingStatuses()
} label: {
Text("\(viewModel.pendingStatuses.count) new posts")
}
.buttonStyle(.bordered)
.background(.thinMaterial)
.cornerRadius(8)
.padding(.top, 6)
}
}
@ViewBuilder @ViewBuilder
private var tagHeaderView: some View { private var tagHeaderView: some View {
if let tag = viewModel.tag { if let tag = viewModel.tag {

View file

@ -27,6 +27,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
} }
} }
@Published var tag: Tag? @Published var tag: Tag?
@Published var pendingStatuses: [Status] = []
var serverName: String { var serverName: String {
client?.server ?? "Error" client?.server ?? "Error"
@ -36,6 +37,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
guard let client else { return } guard let client else { return }
do { do {
if statuses.isEmpty { if statuses.isEmpty {
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))
} else if let first = statuses.first { } else if let first = statuses.first {
@ -86,11 +88,16 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
func handleEvent(event: any StreamEvent) { func handleEvent(event: any StreamEvent) {
guard timeline == .home else { return } guard timeline == .home else { return }
if let event = event as? StreamEventUpdate { if let event = event as? StreamEventUpdate {
statuses.insert(event.status, at: 0) pendingStatuses.insert(event.status, at: 0)
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
} else if let event = event as? StreamEventDelete { } else if let event = event as? StreamEventDelete {
statuses.removeAll(where: { $0.id == event.status }) statuses.removeAll(where: { $0.id == event.status })
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
} }
} }
func displayPendingStatuses() {
statuses.insert(contentsOf: pendingStatuses, at: 0)
pendingStatuses = []
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
}
} }