From 11d4d20873389c9205af7be83ac6946737aa32c4 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 25 Dec 2022 18:43:15 +0100 Subject: [PATCH] Timeline scroll to top UX / Flow --- .../Sources/Timeline/TimelineView.swift | 36 +++++++++++++++---- .../Sources/Timeline/TimelineViewModel.swift | 11 ++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index 26c39019..baeb0fe8 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -18,13 +18,21 @@ public struct TimelineView: View { } public var body: some View { - ScrollView { - LazyVStack { - tagHeaderView - .padding(.bottom, 16) - StatusesListView(fetcher: viewModel) + ScrollViewReader { proxy in + ZStack(alignment: .top) { + ScrollView { + LazyVStack { + 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()) .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 private var tagHeaderView: some View { if let tag = viewModel.tag { diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index 29041bbd..ac01954d 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -27,6 +27,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { } } @Published var tag: Tag? + @Published var pendingStatuses: [Status] = [] var serverName: String { client?.server ?? "Error" @@ -36,6 +37,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { guard let client else { return } do { if statuses.isEmpty { + pendingStatuses = [] statusesState = .loading statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil, maxId: nil)) } else if let first = statuses.first { @@ -86,11 +88,16 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { func handleEvent(event: any StreamEvent) { guard timeline == .home else { return } if let event = event as? StreamEventUpdate { - statuses.insert(event.status, at: 0) - statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) + pendingStatuses.insert(event.status, at: 0) } else if let event = event as? StreamEventDelete { statuses.removeAll(where: { $0.id == event.status }) statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) } } + + func displayPendingStatuses() { + statuses.insert(contentsOf: pendingStatuses, at: 0) + pendingStatuses = [] + statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) + } }