mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-17 02:05:13 +00:00
Smoother scrolling up in the Timeline
This commit is contained in:
parent
bef45d8621
commit
1a351eaa7c
6 changed files with 47 additions and 24 deletions
|
@ -247,7 +247,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusDidAppear(status: Models.Status) async {
|
func statusDidAppear(status: Models.Status) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,5 +16,5 @@ public protocol StatusesFetcher: ObservableObject {
|
||||||
var statusesState: StatusesState { get }
|
var statusesState: StatusesState { get }
|
||||||
func fetchStatuses() async
|
func fetchStatuses() async
|
||||||
func fetchNextPage() async
|
func fetchNextPage() async
|
||||||
func statusDidAppear(status: Status) async
|
func statusDidAppear(status: Status)
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,9 +56,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
bottom: 12,
|
bottom: 12,
|
||||||
trailing: .layoutPadding))
|
trailing: .layoutPadding))
|
||||||
.onAppear {
|
.onAppear {
|
||||||
Task {
|
fetcher.statusDidAppear(status: status)
|
||||||
await fetcher.statusDidAppear(status: status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !isEmbdedInList {
|
if !isEmbdedInList {
|
||||||
Divider()
|
Divider()
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class PendingStatusesObserver: ObservableObject {
|
||||||
|
@Published var pendingStatusesCount: Int = 0
|
||||||
|
|
||||||
|
var pendingStatuses: [String] = [] {
|
||||||
|
didSet {
|
||||||
|
pendingStatusesCount = pendingStatuses.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeStatus(status: Status) {
|
||||||
|
if let index = pendingStatuses.firstIndex(of: status.id) {
|
||||||
|
pendingStatuses.removeSubrange(index...(pendingStatuses.count - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() { }
|
||||||
|
}
|
|
@ -19,8 +19,10 @@ public struct TimelineView: View {
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
|
|
||||||
@StateObject private var viewModel = TimelineViewModel()
|
@StateObject private var viewModel = TimelineViewModel()
|
||||||
|
@StateObject private var pendingStatusesObserver = PendingStatusesObserver()
|
||||||
|
|
||||||
@State private var scrollProxy: ScrollViewProxy?
|
@State private var scrollProxy: ScrollViewProxy?
|
||||||
|
|
||||||
@Binding var timeline: TimelineFilter
|
@Binding var timeline: TimelineFilter
|
||||||
@Binding var scrollToTopSignal: Int
|
@Binding var scrollToTopSignal: Int
|
||||||
|
|
||||||
|
@ -62,9 +64,14 @@ public struct TimelineView: View {
|
||||||
.navigationTitle(timeline.localizedTitle())
|
.navigationTitle(timeline.localizedTitle())
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
viewModel.pendingStatusesObserver = pendingStatusesObserver
|
||||||
if viewModel.client == nil {
|
if viewModel.client == nil {
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
viewModel.timeline = timeline
|
viewModel.timeline = timeline
|
||||||
|
} else {
|
||||||
|
Task {
|
||||||
|
await viewModel.fetchStatuses(userIntent: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
|
@ -105,15 +112,15 @@ public struct TimelineView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func makePendingNewPostsView(proxy: ScrollViewProxy) -> some View {
|
private func makePendingNewPostsView(proxy: ScrollViewProxy) -> some View {
|
||||||
if !viewModel.pendingStatuses.isEmpty {
|
if pendingStatusesObserver.pendingStatusesCount > 0 {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
proxy.scrollTo(viewModel.pendingStatuses.last?.id, anchor: .bottom)
|
proxy.scrollTo(pendingStatusesObserver.pendingStatuses.last, anchor: .bottom)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text(viewModel.pendingStatusesButtonTitle)
|
Text("\(pendingStatusesObserver.pendingStatusesCount)")
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.background(.thinMaterial)
|
.background(.thinMaterial)
|
||||||
|
|
|
@ -16,6 +16,8 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
|
|
||||||
// Internal source of truth for a timeline.
|
// Internal source of truth for a timeline.
|
||||||
private var statuses: [Status] = []
|
private var statuses: [Status] = []
|
||||||
|
|
||||||
|
var pendingStatusesObserver: PendingStatusesObserver?
|
||||||
|
|
||||||
@Published var statusesState: StatusesState = .loading
|
@Published var statusesState: StatusesState = .loading
|
||||||
@Published var timeline: TimelineFilter = .federated {
|
@Published var timeline: TimelineFilter = .federated {
|
||||||
|
@ -23,7 +25,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
Task {
|
Task {
|
||||||
if oldValue != timeline {
|
if oldValue != timeline {
|
||||||
statuses = []
|
statuses = []
|
||||||
pendingStatuses = []
|
pendingStatusesObserver?.pendingStatuses = []
|
||||||
tag = nil
|
tag = nil
|
||||||
}
|
}
|
||||||
await fetchStatuses(userIntent: false)
|
await fetchStatuses(userIntent: false)
|
||||||
|
@ -38,11 +40,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var tag: Tag?
|
@Published var tag: Tag?
|
||||||
@Published var pendingStatuses: [Status] = []
|
@Published var pendingStatusesCount: Int = 0
|
||||||
|
|
||||||
var pendingStatusesButtonTitle: LocalizedStringKey {
|
|
||||||
"\(pendingStatuses.count)"
|
|
||||||
}
|
|
||||||
|
|
||||||
var pendingStatusesEnabled: Bool {
|
var pendingStatusesEnabled: Bool {
|
||||||
timeline == .home
|
timeline == .home
|
||||||
|
@ -60,7 +58,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
if statuses.isEmpty {
|
if statuses.isEmpty {
|
||||||
pendingStatuses = []
|
pendingStatusesObserver?.pendingStatuses = []
|
||||||
statusesState = .loading
|
statusesState = .loading
|
||||||
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||||
maxId: nil,
|
maxId: nil,
|
||||||
|
@ -69,11 +67,11 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
}
|
}
|
||||||
} else if let first = pendingStatuses.first ?? statuses.first {
|
} else if let first = statuses.first {
|
||||||
var newStatuses: [Status] = await fetchNewPages(minId: first.id, maxPages: 20)
|
var newStatuses: [Status] = await fetchNewPages(minId: first.id, maxPages: 20)
|
||||||
if userIntent || !pendingStatusesEnabled {
|
if userIntent || !pendingStatusesEnabled {
|
||||||
statuses.insert(contentsOf: pendingStatuses, at: 0)
|
statuses.insert(contentsOf: newStatuses, at: 0)
|
||||||
pendingStatuses = []
|
pendingStatusesObserver?.pendingStatuses = []
|
||||||
withAnimation {
|
withAnimation {
|
||||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
}
|
}
|
||||||
|
@ -81,7 +79,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
newStatuses = newStatuses.filter { status in
|
newStatuses = newStatuses.filter { status in
|
||||||
!statuses.contains(where: { $0.id == status.id })
|
!statuses.contains(where: { $0.id == status.id })
|
||||||
}
|
}
|
||||||
pendingStatuses.insert(contentsOf: newStatuses, at: 0)
|
pendingStatusesObserver?.pendingStatuses.insert(contentsOf: newStatuses.map{ $0.id }, at: 0)
|
||||||
statuses.insert(contentsOf: newStatuses, at: 0)
|
statuses.insert(contentsOf: newStatuses, at: 0)
|
||||||
withAnimation {
|
withAnimation {
|
||||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
|
@ -145,7 +143,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
pendingStatusesEnabled,
|
pendingStatusesEnabled,
|
||||||
!statuses.contains(where: { $0.id == event.status.id })
|
!statuses.contains(where: { $0.id == event.status.id })
|
||||||
{
|
{
|
||||||
pendingStatuses.insert(event.status, at: 0)
|
pendingStatusesObserver?.pendingStatuses.insert(event.status.id, at: 0)
|
||||||
statuses.insert(event.status, at: 0)
|
statuses.insert(event.status, at: 0)
|
||||||
withAnimation {
|
withAnimation {
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||||
|
@ -163,9 +161,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusDidAppear(status: Status) async {
|
func statusDidAppear(status: Status) {
|
||||||
if let index = pendingStatuses.firstIndex(of: status) {
|
pendingStatusesObserver?.removeStatus(status: status)
|
||||||
pendingStatuses.remove(at: index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue