Don't fail decoding when one status fails to decode

Also remove old workaround for bad dates from #477

Closes #478
This commit is contained in:
Shadowfacts 2024-06-08 13:29:56 -07:00
parent e6d9a33dbf
commit c2232a5e14
13 changed files with 70 additions and 36 deletions

View File

@ -42,8 +42,7 @@ public struct Client: Sendable {
} else if let date = iso8601.date(from: str) {
return date
} else {
// throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)"))
return Date(timeIntervalSinceReferenceDate: 0)
throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)"))
}
})
@ -205,8 +204,8 @@ public struct Client: Sendable {
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
}
public static func getFavourites(range: RequestRange = .default) -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/favourites")
public static func getFavourites(range: RequestRange = .default) -> Request<[TryDecode<Status>]> {
var request = Request<[TryDecode<Status>]>(method: .get, path: "/api/v1/favourites")
request.range = range
return request
}
@ -457,14 +456,13 @@ public struct Client: Sendable {
}
// MARK: - Timelines
public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> {
public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[TryDecode<Status>]> {
return timeline.request(range: range)
}
// MARK: - Bookmarks
public static func getBookmarks(range: RequestRange = .default) -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks")
public static func getBookmarks(range: RequestRange = .default) -> Request<[TryDecode<Status>]> {
var request = Request<[TryDecode<Status>]>(method: .get, path: "/api/v1/bookmarks")
request.range = range
return request
}
@ -492,7 +490,7 @@ public struct Client: Sendable {
return Request<[Hashtag]>(method: .get, path: "/api/v1/trends/tags", queryParameters: parameters)
}
public static func getTrendingStatuses(limit: Int? = nil, offset: Int? = nil) -> Request<[Status]> {
public static func getTrendingStatuses(limit: Int? = nil, offset: Int? = nil) -> Request<[TryDecode<Status>]> {
var parameters: [Parameter] = []
if let limit {
parameters.append("limit" => limit)

View File

@ -95,8 +95,8 @@ public final class Account: AccountProtocol, Decodable, Sendable {
return request
}
public static func getStatuses(_ accountID: String, range: RequestRange = .default, onlyMedia: Bool? = nil, pinned: Bool? = nil, excludeReplies: Bool? = nil, excludeReblogs: Bool? = nil) -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/accounts/\(accountID)/statuses", queryParameters: [
public static func getStatuses(_ accountID: String, range: RequestRange = .default, onlyMedia: Bool? = nil, pinned: Bool? = nil, excludeReplies: Bool? = nil, excludeReblogs: Bool? = nil) -> Request<[TryDecode<Status>]> {
var request = Request<[TryDecode<Status>]>(method: .get, path: "/api/v1/accounts/\(accountID)/statuses", queryParameters: [
"only_media" => onlyMedia,
"pinned" => pinned,
"exclude_replies" => excludeReplies,

View File

@ -10,7 +10,7 @@ import Foundation
public struct SearchResults: Decodable, Sendable {
public let accounts: [Account]
public let statuses: [Status]
public let statuses: [TryDecode<Status>]
public let hashtags: [Hashtag]
private enum CodingKeys: String, CodingKey {

View File

@ -32,8 +32,8 @@ extension Timeline {
}
}
func request(range: RequestRange) -> Request<[Status]> {
var request: Request<[Status]> = Request<[Status]>(method: .get, path: endpoint)
func request(range: RequestRange) -> Request<[TryDecode<Status>]> {
var request = Request<[TryDecode<Status>]>(method: .get, path: endpoint)
if case .public(true) = self {
request.queryParameters.append("local" => true)
}

View File

@ -0,0 +1,32 @@
//
// TryDecode.swift
// Pachyderm
//
// Created by Shadowfacts on 6/8/24.
//
import Foundation
public enum TryDecode<T: Decodable>: Decodable {
case error(String)
case value(T)
public init(from decoder: any Decoder) throws {
do {
self = .value(try T(from: decoder))
} catch {
self = .error(error.localizedDescription)
}
}
public var value: T? {
if case .value(let value) = self {
value
} else {
nil
}
}
}
extension TryDecode: Sendable where T: Sendable {
}

View File

@ -232,7 +232,7 @@ class ConversationViewController: UIViewController {
let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true)
do {
let (results, _) = try await mastodonController.run(request)
guard let status = results.statuses.first(where: { $0.url?.serialized() == effectiveURL }) else {
guard let status = results.statuses.compactMap(\.value).first(where: { $0.url?.serialized() == effectiveURL }) else {
throw UnableToResolveError()
}
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)

View File

@ -123,7 +123,7 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
private func loadTrendingStatuses() async {
let statuses: [Status]
do {
statuses = try await mastodonController.run(Client.getTrendingStatuses()).0
statuses = try await mastodonController.run(Client.getTrendingStatuses()).0.compactMap(\.value)
} catch {
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
await MainActor.run {

View File

@ -277,7 +277,7 @@ class TrendsViewController: UIViewController, CollectionViewController {
let linksReq = Client.getTrendingLinks(limit: 10)
async let links = try? mastodonController.run(linksReq).0
let statusesReq = Client.getTrendingStatuses(limit: 10)
async let statuses = try? mastodonController.run(statusesReq).0
async let statuses = try? mastodonController.run(statusesReq).0.compactMap(\.value)
if let links = await links {
if snapshot.sectionIdentifiers.contains(.profileSuggestions) {
@ -332,7 +332,7 @@ class TrendsViewController: UIViewController, CollectionViewController {
do {
let request = Client.getTrendingStatuses(offset: origSnapshot.itemIdentifiers(inSection: .trendingStatuses).count)
let (statuses, _) = try await mastodonController.run(request)
let statuses = try await mastodonController.run(request).0.compactMap(\.value)
await mastodonController.persistentContainer.addAll(statuses: statuses)

View File

@ -17,7 +17,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
let mastodonController: MastodonController
private let predicate: (StatusMO) -> Bool
private let predicateTitle: String
private let request: (RequestRange) -> Request<[Status]>
private let request: (RequestRange) -> Request<[TryDecode<Status>]>
var collectionView: UICollectionView! {
view as? UICollectionView
@ -28,7 +28,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
private var newer: RequestRange?
private var older: RequestRange?
init(predicate: @escaping (StatusMO) -> Bool, predicateTitle: String, request: @escaping (RequestRange) -> Request<[Status]>, mastodonController: MastodonController) {
init(predicate: @escaping (StatusMO) -> Bool, predicateTitle: String, request: @escaping (RequestRange) -> Request<[TryDecode<Status>]>, mastodonController: MastodonController) {
self.mastodonController = mastodonController
self.predicate = predicate
self.predicateTitle = predicateTitle
@ -140,7 +140,8 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
do {
let req = request(.count(Self.pageSize))
let (statuses, pagination) = try await mastodonController.run(req)
let (tryStatuses, pagination) = try await mastodonController.run(req)
let statuses = tryStatuses.compactMap(\.value)
newer = pagination?.newer
older = pagination?.older
@ -180,7 +181,8 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
do {
let req = request(older.withCount(Self.pageSize))
let (statuses, pagination) = try await mastodonController.run(req)
let (tryStatuses, pagination) = try await mastodonController.run(req)
let statuses = tryStatuses.compactMap(\.value)
self.older = pagination?.older
await mastodonController.persistentContainer.addAll(statuses: statuses)
@ -278,7 +280,8 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
Task {
do {
let req = request(newer.withCount(Self.pageSize))
let (statuses, pagination) = try await mastodonController.run(req)
let (tryStatuses, pagination) = try await mastodonController.run(req)
let statuses = tryStatuses.compactMap(\.value)
self.newer = pagination?.newer
await mastodonController.persistentContainer.addAll(statuses: statuses)

View File

@ -310,7 +310,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
}
let request = Account.getStatuses(accountID, range: .default, onlyMedia: false, pinned: true, excludeReplies: false)
let (statuses, _) = try await mastodonController.run(request)
let statuses = try await mastodonController.run(request).0.compactMap(\.value)
await withCheckedContinuation { continuation in
mastodonController.persistentContainer.addAll(statuses: statuses) {
@ -526,7 +526,7 @@ extension ProfileStatusesViewController {
extension ProfileStatusesViewController: TimelineLikeControllerDataSource {
typealias TimelineItem = String // status ID
private func request(for range: RequestRange = .default) -> Request<[Status]> {
private func request(for range: RequestRange = .default) -> Request<[TryDecode<Status>]> {
switch kind {
case .statuses:
return Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: true)
@ -539,7 +539,7 @@ extension ProfileStatusesViewController: TimelineLikeControllerDataSource {
func loadInitial() async throws -> [String] {
let request = request()
let (statuses, _) = try await mastodonController.run(request)
let statuses = try await mastodonController.run(request).0.compactMap(\.value)
if !statuses.isEmpty {
newer = .after(id: statuses.first!.id, count: nil)
@ -559,7 +559,7 @@ extension ProfileStatusesViewController: TimelineLikeControllerDataSource {
}
let request = request(for: newer)
let (statuses, _) = try await mastodonController.run(request)
let statuses = try await mastodonController.run(request).0.compactMap(\.value)
guard !statuses.isEmpty else {
throw Error.allCaughtUp
@ -580,7 +580,7 @@ extension ProfileStatusesViewController: TimelineLikeControllerDataSource {
}
let request = request(for: older)
let (statuses, _) = try await mastodonController.run(request)
let statuses = try await mastodonController.run(request).0.compactMap(\.value)
guard !statuses.isEmpty else {
return []

View File

@ -53,7 +53,7 @@ struct ReportAddStatusView: View {
.task { @MainActor in
do {
let req = Account.getStatuses(report.accountID, range: .count(40), excludeReplies: false, excludeReblogs: true)
let (statuses, _) = try await mastodonController.run(req)
let statuses = try await mastodonController.run(req).0.compactMap(\.value)
await mastodonController.persistentContainer.addAll(statuses: statuses)
self.statuses = statuses.compactMap { mastodonController.persistentContainer.status(for: $0.id) }
} catch {

View File

@ -266,7 +266,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
guard self.currentQuery == query else { return }
self.mastodonController.persistentContainer.performBatchUpdates { (context, addAccounts, addStatuses) in
addAccounts(results.accounts)
addStatuses(results.statuses)
addStatuses(results.statuses.compactMap(\.value))
} completion: {
DispatchQueue.main.async {
self.showSearchResults(results)
@ -299,7 +299,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
}
if !results.statuses.isEmpty && resultTypes.contains(.statuses) {
snapshot.appendSections([.statuses])
snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
snapshot.appendItems(results.statuses.compactMap(\.value).map { .status($0.id, .unknown) }, toSection: .statuses)
}
dataSource.apply(snapshot)

View File

@ -565,7 +565,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
do {
let (marker, _) = try await mastodonController.run(TimelineMarkers.request(timeline: .home))
async let status = try await mastodonController.run(Client.getStatus(id: marker.lastReadID)).0
async let olderStatuses = try await mastodonController.run(Client.getStatuses(timeline: .home, range: .before(id: marker.lastReadID, count: Self.pageSize))).0
// TODO: consider replacing undecodable statuses here with items to indicate that to the user
async let olderStatuses = try await mastodonController.run(Client.getStatuses(timeline: .home, range: .before(id: marker.lastReadID, count: Self.pageSize))).0.compactMap(\.value)
let allStatuses = try await [status] + olderStatuses
await mastodonController.persistentContainer.addAll(statuses: allStatuses)
@ -1100,7 +1101,7 @@ extension TimelineViewController: TimelineLikeControllerDataSource {
func loadInitial() async throws -> [TimelineItem] {
let request = Client.getStatuses(timeline: timeline, range: .count(TimelineViewController.pageSize))
let (statuses, _) = try await mastodonController.run(request)
let statuses = try await mastodonController.run(request).0.compactMap(\.value)
await withCheckedContinuation { continuation in
mastodonController.persistentContainer.addAll(statuses: statuses) {
@ -1119,7 +1120,7 @@ extension TimelineViewController: TimelineLikeControllerDataSource {
let newer = RequestRange.after(id: id, count: TimelineViewController.pageSize)
let request = Client.getStatuses(timeline: timeline, range: newer)
let (statuses, _) = try await mastodonController.run(request)
let statuses = try await mastodonController.run(request).0.compactMap(\.value)
guard !statuses.isEmpty else {
throw TimelineViewController.Error.allCaughtUp
@ -1143,7 +1144,7 @@ extension TimelineViewController: TimelineLikeControllerDataSource {
let older = RequestRange.before(id: id, count: TimelineViewController.pageSize)
let request = Client.getStatuses(timeline: timeline, range: older)
let (statuses, _) = try await mastodonController.run(request)
let statuses = try await mastodonController.run(request).0.compactMap(\.value)
guard !statuses.isEmpty else {
return []
@ -1181,7 +1182,7 @@ extension TimelineViewController: TimelineLikeControllerDataSource {
}
let request = Client.getStatuses(timeline: timeline, range: range)
let (statuses, _) = try await mastodonController.run(request)
let statuses = try await mastodonController.run(request).0.compactMap(\.value)
guard !statuses.isEmpty else {
return []