Timeline: Basic timeline sync using the marker API

This commit is contained in:
Thomas Ricouard 2023-12-27 13:26:30 +01:00
parent 590299d102
commit 962c7c0295
6 changed files with 231 additions and 24 deletions

View file

@ -125,6 +125,16 @@ struct TimelineTab: View {
} label: {
Label(TimelineFilter.latest.localizedTitle(), systemImage: TimelineFilter.latest.iconName() ?? "")
}
if timeline == .home {
Button {
timeline = .resume
} label: {
VStack {
Label(TimelineFilter.resume.localizedTitle(),
systemImage: TimelineFilter.resume.iconName() ?? "")
}
}
}
Divider()
}
ForEach(TimelineFilter.availableTimeline(client: client), id: \.self) { timeline in

View file

@ -10983,7 +10983,7 @@
},
"de" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "Konto hinzufügen"
}
},
@ -11013,13 +11013,13 @@
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "Ajouter un compte"
}
},
"it" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "Aggiungi account"
}
},
@ -11073,13 +11073,13 @@
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "添加账户"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "新增帳戶"
}
}
@ -38368,7 +38368,7 @@
},
"de" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "Konto hinzufügen"
}
},
@ -38398,13 +38398,13 @@
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "Ajouter un compte"
}
},
"it" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "Aggiungi un account"
}
},
@ -38458,13 +38458,13 @@
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "添加账户"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "新增帳戶"
}
}
@ -44190,8 +44190,8 @@
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Account on Hover"
"state" : "translated",
"value" : "Afficher le popover du compte"
}
},
"it" : {
@ -72324,6 +72324,125 @@
}
}
},
"timeline.resume" : {
"extractionState" : "manual",
"localizations" : {
"be" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resime"
}
},
"ca" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"de" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Resume"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Resume"
}
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"eu" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Reprendre"
}
},
"it" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"ja" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"ko" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"nb" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"nl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"pl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"tr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"uk" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Resume"
}
}
}
},
"timeline.trending" : {
"extractionState" : "manual",
"localizations" : {

View file

@ -11,12 +11,13 @@ public enum Markers: Endpoint {
public func queryItems() -> [URLQueryItem]? {
switch self {
case .markers:
[URLQueryItem(name: "timeline[]", value: "home"),
URLQueryItem(name: "timeline[]", value: "notifications")]
case let .markNotifications(lastReadId):
[URLQueryItem(name: "notifications[last_read_id]", value: lastReadId)]
case let .markHome(lastReadId):
[URLQueryItem(name: "home[last_read_id]", value: lastReadId)]
default:
nil
}
}
}

View file

@ -36,6 +36,7 @@ public enum TimelineFilter: Hashable, Equatable {
case list(list: Models.List)
case remoteLocal(server: String, filter: RemoteTimelineFilter)
case latest
case resume
public func hash(into hasher: inout Hasher) {
hasher.combine(title)
@ -63,6 +64,8 @@ public enum TimelineFilter: Hashable, Equatable {
switch self {
case .latest:
"Latest"
case .resume:
"Resume"
case .federated:
"Federated"
case .local:
@ -86,6 +89,8 @@ public enum TimelineFilter: Hashable, Equatable {
switch self {
case .latest:
"timeline.latest"
case .resume:
"timeline.resume"
case .federated:
"timeline.federated"
case .local:
@ -109,6 +114,8 @@ public enum TimelineFilter: Hashable, Equatable {
switch self {
case .latest:
"arrow.counterclockwise"
case .resume:
"clock.arrow.2.circlepath"
case .federated:
"globe.americas"
case .local:
@ -140,6 +147,7 @@ public enum TimelineFilter: Hashable, Equatable {
return Trends.statuses(offset: offset)
}
case .latest: return Timelines.home(sinceId: nil, maxId: nil, minId: nil)
case .resume: return Timelines.home(sinceId: nil, maxId: nil, minId: nil)
case .home: return Timelines.home(sinceId: sinceId, maxId: maxId, minId: minId)
case .trending: return Trends.statuses(offset: offset)
case let .list(list): return Timelines.list(listId: list.id, sinceId: sinceId, maxId: maxId, minId: minId)
@ -172,6 +180,7 @@ extension TimelineFilter: Codable {
case list
case remoteLocal
case latest
case resume
}
public init(from decoder: Decoder) throws {
@ -255,6 +264,8 @@ extension TimelineFilter: Codable {
try nestedContainer.encode(filter)
case .latest:
try container.encode(CodingKeys.latest.rawValue, forKey: .latest)
case .resume:
try container.encode(CodingKeys.resume.rawValue, forKey: .latest)
}
}
}

View file

@ -34,7 +34,8 @@ public struct TimelineView: View {
public init(timeline: Binding<TimelineFilter>,
selectedTagGroup: Binding<TagGroup?>,
scrollToTopSignal: Binding<Int>, canFilterTimeline: Bool)
scrollToTopSignal: Binding<Int>,
canFilterTimeline: Bool)
{
_timeline = timeline
_selectedTagGroup = selectedTagGroup
@ -111,6 +112,7 @@ public struct TimelineView: View {
}
.onDisappear {
viewModel.isTimelineVisible = false
viewModel.saveMarker()
}
.refreshable {
SoundEffectManager.shared.playSound(.pull)
@ -145,6 +147,7 @@ public struct TimelineView: View {
}
case .background:
wasBackgrounded = true
viewModel.saveMarker()
default:
break
@ -243,6 +246,9 @@ public struct TimelineView: View {
Text(timeline.localizedTitle())
.font(.caption)
.foregroundStyle(.secondary)
case .home:
Text(timeline.localizedTitle())
.font(.headline)
default:
Text(timeline.localizedTitle())
.font(.headline)

View file

@ -10,23 +10,34 @@ import SwiftUI
var scrollToIndex: Int?
var statusesState: StatusesState = .loading
var timeline: TimelineFilter = .federated {
willSet {
if timeline == .home && newValue != .resume {
saveMarker()
}
}
didSet {
timelineTask?.cancel()
timelineTask = Task {
if timeline == .latest {
if timeline == .latest || timeline == .resume {
if oldValue == .home {
await clearHomeCache()
}
if timeline == .resume, let marker = await fetchMarker() {
self.marker = marker
}
timeline = oldValue
}
if oldValue != timeline {
await reset()
pendingStatusesObserver.pendingStatuses = []
tag = nil
}
guard !Task.isCancelled else {
return
}
await fetchNewestStatuses()
switch timeline {
case let .hashtag(tag, _):
@ -77,6 +88,7 @@ import SwiftUI
var isTimelineVisible: Bool = false
let pendingStatusesObserver: PendingStatusesObserver = .init()
var scrollToIndexAnimated: Bool = false
var marker: Marker.Content?
init() {
pendingStatusesObserver.scrollToIndex = { [weak self] index in
@ -175,18 +187,41 @@ extension TimelineViewModel: StatusesFetcher {
}
}
func fetchStatuses(from: Marker.Content) async throws {
guard let client else { return }
statusesState = .loading
var statuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil,
maxId: from.lastReadId,
minId: nil,
offset: 0))
ReblogCache.shared.removeDuplicateReblogs(&statuses)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
await datasource.set(statuses)
await cacheHome()
marker = nil
withAnimation {
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
}
await fetchNewestStatuses()
}
func fetchNewestStatuses() async {
guard let client else { return }
do {
if await datasource.isEmpty {
if let marker {
try await fetchStatuses(from: marker)
} else if await datasource.isEmpty {
try await fetchFirstPage(client: client)
} else if let latest = await datasource.get().first, timeline.supportNewestPagination {
try await fetchNewPagesFrom(latestStatus: latest, client: client)
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
}
} catch {
statusesState = .error(error: error)
canStreamEvents = true
print("timeline parse error: \(error)")
}
}
@ -241,10 +276,10 @@ extension TimelineViewModel: StatusesFetcher {
}
// Fetch pages from the top most status of the tomeline.
private func fetchNewPagesFrom(latestStatus: Status, client: Client) async throws {
private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws {
canStreamEvents = false
let initialTimeline = timeline
var newStatuses: [Status] = await fetchNewPages(minId: latestStatus.id, maxPages: 10)
var newStatuses: [Status] = await fetchNewPages(minId: latestStatus, maxPages: 10)
// Dedup statuses, a status with the same id could have been streamed in.
let ids = await datasource.get().map(\.id)
@ -321,7 +356,7 @@ extension TimelineViewModel: StatusesFetcher {
!Task.isCancelled,
let latest = await datasource.get().first
{
try await fetchNewPagesFrom(latestStatus: latest, client: client)
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
}
}
@ -392,3 +427,28 @@ extension TimelineViewModel: StatusesFetcher {
visibileStatusesIds.remove(status.id)
}
}
// MARK: - MARKER
extension TimelineViewModel {
func fetchMarker() async -> Marker.Content? {
guard let client else {
return nil
}
do {
let data: Marker = try await client.get(endpoint: Markers.markers)
return data.home
} catch {
return nil
}
}
func saveMarker() {
guard timeline == .home, let client else { return }
Task {
guard let id = await cache.getLatestSeenStatus(for: client)?.first else { return }
do {
let _: Marker = try await client.post(endpoint: Markers.markHome(lastReadId: id))
} catch { }
}
}
}