mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-23 00:40:59 +00:00
Iron out timeline issues with the new behaviour
This commit is contained in:
parent
c88ef750f0
commit
52eff96ab4
7 changed files with 57 additions and 15 deletions
|
@ -247,7 +247,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusDidAppear(status: Models.Status) {
|
func statusDidAppear(status: Models.Status) { }
|
||||||
|
|
||||||
}
|
func statusDidDisappear(status: Status) { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue