mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 16:31:00 +00:00
Timeline: Basic timeline sync using the marker API
This commit is contained in:
parent
590299d102
commit
962c7c0295
6 changed files with 231 additions and 24 deletions
|
@ -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
|
||||
|
|
|
@ -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" : {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,7 +147,8 @@ 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)
|
||||
|
|
|
@ -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,7 +88,8 @@ 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
|
||||
self?.scrollToIndexAnimated = true
|
||||
|
@ -174,19 +186,42 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
await fetchNewestStatuses()
|
||||
}
|
||||
}
|
||||
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue