Iron out timeline issues with the new behaviour

This commit is contained in:
Thomas Ricouard 2023-01-31 12:17:35 +01:00
parent c88ef750f0
commit 52eff96ab4
7 changed files with 57 additions and 15 deletions

View file

@ -247,7 +247,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
} }
} }
func statusDidAppear(status: Models.Status) { func statusDidAppear(status: Models.Status) { }
} func statusDidDisappear(status: Status) { }
} }

View file

@ -17,4 +17,5 @@ public protocol StatusesFetcher: ObservableObject {
func fetchStatuses() async func fetchStatuses() async
func fetchNextPage() async func fetchNextPage() async
func statusDidAppear(status: Status) func statusDidAppear(status: Status)
func statusDidDisappear(status: Status)
} }

View file

@ -59,6 +59,9 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
.onAppear { .onAppear {
fetcher.statusDidAppear(status: status) fetcher.statusDidAppear(status: status)
} }
.onDisappear {
fetcher.statusDidDisappear(status: status)
}
if !isEmbdedInList { if !isEmbdedInList {
Divider() Divider()
.padding(.vertical, .dividerPadding) .padding(.vertical, .dividerPadding)

View file

@ -199,6 +199,7 @@ public struct StatusMediaPreviewView: View {
Text("status.image.alt-text.abbreviation") Text("status.image.alt-text.abbreviation")
.font(theme.statusDisplayStyle == .compact ? .footnote : .body) .font(theme.statusDisplayStyle == .compact ? .footnote : .body)
} }
.buttonStyle(.borderless)
.padding(4) .padding(4)
.background(.thinMaterial) .background(.thinMaterial)
.cornerRadius(4) .cornerRadius(4)
@ -242,6 +243,7 @@ public struct StatusMediaPreviewView: View {
Text("status.image.alt-text.abbreviation") Text("status.image.alt-text.abbreviation")
.font(.scaledFootnote) .font(.scaledFootnote)
} }
.buttonStyle(.borderless)
.padding(4) .padding(4)
.background(.thinMaterial) .background(.thinMaterial)
.cornerRadius(4) .cornerRadius(4)
@ -287,6 +289,7 @@ public struct StatusMediaPreviewView: View {
private var sensitiveMediaOverlay: some View { private var sensitiveMediaOverlay: some View {
ZStack { ZStack {
Rectangle() Rectangle()
.foregroundColor(.clear)
.background(.ultraThinMaterial) .background(.ultraThinMaterial)
if !isNotifications { if !isNotifications {
Button { Button {
@ -294,11 +297,14 @@ public struct StatusMediaPreviewView: View {
isHidingMedia = false isHidingMedia = false
} }
} label: { } label: {
if sensitive { Group {
Label("status.media.sensitive.show", systemImage: "eye") if sensitive {
} else { Label("status.media.sensitive.show", systemImage: "eye")
Label("status.media.content.show", systemImage: "eye") } else {
Label("status.media.content.show", systemImage: "eye")
}
} }
.foregroundColor(theme.labelColor)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
} }

View file

@ -4,8 +4,12 @@ import Models
@MainActor @MainActor
class PendingStatusesObserver: ObservableObject { class PendingStatusesObserver: ObservableObject {
let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
@Published var pendingStatusesCount: Int = 0 @Published var pendingStatusesCount: Int = 0
var disableUpdate: Bool = false
var pendingStatuses: [String] = [] { var pendingStatuses: [String] = [] {
didSet { didSet {
pendingStatusesCount = pendingStatuses.count pendingStatusesCount = pendingStatuses.count
@ -13,8 +17,9 @@ class PendingStatusesObserver: ObservableObject {
} }
func removeStatus(status: Status) { func removeStatus(status: Status) {
if let index = pendingStatuses.firstIndex(of: status.id) { if !disableUpdate, let index = pendingStatuses.firstIndex(of: status.id) {
pendingStatuses.removeSubrange(index...(pendingStatuses.count - 1)) pendingStatuses.removeSubrange(index...(pendingStatuses.count - 1))
feedbackGenerator.impactOccurred()
} }
} }

View file

@ -20,7 +20,6 @@ public struct TimelineView: View {
@StateObject private var viewModel = TimelineViewModel() @StateObject private var viewModel = TimelineViewModel()
@State private var scrollProxy: ScrollViewProxy?
@State private var wasBackgrounded: Bool = false @State private var wasBackgrounded: Bool = false
@Binding var timeline: TimelineFilter @Binding var timeline: TimelineFilter
@ -58,7 +57,7 @@ public struct TimelineView: View {
} }
} }
.onAppear { .onAppear {
scrollProxy = proxy viewModel.scrollProxy = proxy
} }
} }
.navigationTitle(timeline.localizedTitle()) .navigationTitle(timeline.localizedTitle())
@ -85,7 +84,7 @@ public struct TimelineView: View {
} }
.onChange(of: scrollToTopSignal, perform: { _ in .onChange(of: scrollToTopSignal, perform: { _ in
withAnimation { withAnimation {
scrollProxy?.scrollTo(Constants.scrollToTop, anchor: .top) viewModel.scrollProxy?.scrollTo(Constants.scrollToTop, anchor: .top)
} }
}) })
.onChange(of: timeline) { newTimeline in .onChange(of: timeline) { newTimeline in

View file

@ -16,6 +16,10 @@ 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] = []
private var visibileStatusesIds = Set<String>()
private var isFetchingNewPages: Bool = false
var scrollProxy: ScrollViewProxy?
var pendingStatusesObserver: PendingStatusesObserver = .init() var pendingStatusesObserver: PendingStatusesObserver = .init()
@ -55,7 +59,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
} }
func fetchStatuses(userIntent: Bool) async { func fetchStatuses(userIntent: Bool) async {
guard let client else { return } guard let client, !isFetchingNewPages else { return }
do { do {
if statuses.isEmpty { if statuses.isEmpty {
pendingStatusesObserver.pendingStatuses = [] pendingStatusesObserver.pendingStatuses = []
@ -68,6 +72,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage) statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
} }
} else if let first = statuses.first { } else if let first = statuses.first {
isFetchingNewPages = true
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: newStatuses, at: 0) statuses.insert(contentsOf: newStatuses, at: 0)
@ -80,14 +85,31 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
!statuses.contains(where: { $0.id == status.id }) !statuses.contains(where: { $0.id == status.id })
} }
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map{ $0.id }, at: 0) pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map{ $0.id }, at: 0)
statuses.insert(contentsOf: newStatuses, at: 0) pendingStatusesObserver.feedbackGenerator.impactOccurred()
withAnimation {
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage) // High chance the user is scrolled to the top, this is a workaround to keep scroll position when prepending statuses.
if let firstStatusId = statuses.first?.id, visibileStatusesIds.contains(firstStatusId) {
statuses.insert(contentsOf: newStatuses, at: 0)
pendingStatusesObserver.disableUpdate = true
withAnimation {
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
DispatchQueue.main.async {
self.scrollProxy?.scrollTo(firstStatusId)
self.pendingStatusesObserver.disableUpdate = false
}
}
} else {
statuses.insert(contentsOf: newStatuses, at: 0)
withAnimation {
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
}
} }
} }
isFetchingNewPages = false
} }
} catch { } catch {
statusesState = .error(error: error) statusesState = .error(error: error)
isFetchingNewPages = false
print("timeline parse error: \(error)") print("timeline parse error: \(error)")
} }
} }
@ -141,6 +163,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) { func handleEvent(event: any StreamEvent, currentAccount: CurrentAccount) {
if let event = event as? StreamEventUpdate, if let event = event as? StreamEventUpdate,
pendingStatusesEnabled, pendingStatusesEnabled,
!isFetchingNewPages,
!statuses.contains(where: { $0.id == event.status.id }) !statuses.contains(where: { $0.id == event.status.id })
{ {
pendingStatusesObserver.pendingStatuses.insert(event.status.id, at: 0) pendingStatusesObserver.pendingStatuses.insert(event.status.id, at: 0)
@ -163,5 +186,10 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
func statusDidAppear(status: Status) { func statusDidAppear(status: Status) {
pendingStatusesObserver.removeStatus(status: status) pendingStatusesObserver.removeStatus(status: status)
visibileStatusesIds.insert(status.id)
}
func statusDidDisappear(status: Status) {
visibileStatusesIds.remove(status.id)
} }
} }