From 013410a80e9d09ae0712b0a6abf8b4510092fcbb Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Thu, 9 Feb 2023 09:12:44 +0100 Subject: [PATCH] Faster / Cached ServerDate decoding --- Packages/Models/Sources/Models/Account.swift | 2 +- .../Sources/Models/Alias/ServerDate.swift | 57 ++++++++++++------- .../Models/ConsolidatedNotification.swift | 2 +- .../Models/Sources/Models/Notification.swift | 2 +- Packages/Models/Sources/Models/Poll.swift | 4 +- Packages/Models/Sources/Models/Status.swift | 20 +++---- .../Models/Sources/Models/StatusHistory.swift | 2 +- .../Notification+Consolidated.swift | 2 +- .../Sources/Status/Row/StatusRowView.swift | 41 +++++++------ .../Sources/Timeline/TimelineCache.swift | 2 +- 10 files changed, 79 insertions(+), 55 deletions(-) diff --git a/Packages/Models/Sources/Models/Account.swift b/Packages/Models/Sources/Models/Account.swift index 4785e843..5b862d36 100644 --- a/Packages/Models/Sources/Models/Account.swift +++ b/Packages/Models/Sources/Models/Account.swift @@ -59,7 +59,7 @@ public struct Account: Codable, Identifiable, Equatable, Hashable { header: URL(string: "https://files.mastodon.social/media_attachments/files/003/134/405/original/04060b07ddf7bb0b.png")!, acct: "johnm@example.com", note: .init(stringValue: "Some content"), - createdAt: "2022-12-16T10:20:54.000Z", + createdAt: ServerDate(), followersCount: 10, followingCount: 10, statusesCount: 10, diff --git a/Packages/Models/Sources/Models/Alias/ServerDate.swift b/Packages/Models/Sources/Models/Alias/ServerDate.swift index b5fd06c2..90a8c05e 100644 --- a/Packages/Models/Sources/Models/Alias/ServerDate.swift +++ b/Packages/Models/Sources/Models/Alias/ServerDate.swift @@ -1,13 +1,16 @@ import Foundation -public typealias ServerDate = String +fileprivate enum CodingKeys: CodingKey { + case asDate, relativeFormatted, shortDateFormatted +} -extension ServerDate { - - public static var sampleDate: ServerDate { - createdAtDateFormatter.string(from: Date()-100) - } +public struct ServerDate: Codable, Hashable, Equatable { + public let asDate: Date + public let relativeFormatted: String + public let shortDateFormatted: String + private static let calendar = Calendar(identifier: .gregorian) + private static var createdAtDateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.calendar = .init(identifier: .iso8601) @@ -15,31 +18,47 @@ extension ServerDate { dateFormatter.timeZone = .init(abbreviation: "UTC") return dateFormatter }() - + private static var createdAtRelativeFormatter: RelativeDateTimeFormatter = { let dateFormatter = RelativeDateTimeFormatter() dateFormatter.unitsStyle = .short return dateFormatter }() - + private static var createdAtShortDateFormatted: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short dateFormatter.timeStyle = .none return dateFormatter }() - - private static let calendar = Calendar(identifier: .gregorian) - - public var asDate: Date { - Self.createdAtDateFormatter.date(from: self)! + + public init() { + asDate = Date()-100 + relativeFormatted = Self.createdAtRelativeFormatter.localizedString(for: asDate, relativeTo: Date()) + shortDateFormatted = Self.createdAtShortDateFormatted.string(from: asDate) } - - public var relativeFormatted: String { - return Self.createdAtRelativeFormatter.localizedString(for: asDate, relativeTo: Date()) + + public init(from decoder: Decoder) throws { + do { + // Decode from server + let container = try decoder.singleValueContainer() + let stringDate = try container.decode(String.self) + asDate = Self.createdAtDateFormatter.date(from: stringDate)! + relativeFormatted = Self.createdAtRelativeFormatter.localizedString(for: asDate, relativeTo: Date()) + shortDateFormatted = Self.createdAtShortDateFormatted.string(from: asDate) + } catch { + // Decode from cache + let container = try decoder.container(keyedBy: CodingKeys.self) + asDate = try container.decode(Date.self, forKey: .asDate) + relativeFormatted = try container.decode(String.self, forKey: .relativeFormatted) + shortDateFormatted = try container.decode(String.self, forKey: .shortDateFormatted) + } } - - public var shortDateFormatted: String { - return Self.createdAtShortDateFormatted.string(from: asDate) + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(asDate, forKey: .asDate) + try container.encode(relativeFormatted, forKey: .relativeFormatted) + try container.encode(shortDateFormatted, forKey: .shortDateFormatted) } } diff --git a/Packages/Models/Sources/Models/ConsolidatedNotification.swift b/Packages/Models/Sources/Models/ConsolidatedNotification.swift index 78ac0357..4b1897f1 100644 --- a/Packages/Models/Sources/Models/ConsolidatedNotification.swift +++ b/Packages/Models/Sources/Models/ConsolidatedNotification.swift @@ -32,7 +32,7 @@ public struct ConsolidatedNotification: Identifiable { public static func placeholder() -> ConsolidatedNotification { .init(notifications: [Notification.placeholder()], type: .favourite, - createdAt: "2022-12-16T10:20:54.000Z", + createdAt: ServerDate(), accounts: [.placeholder()], status: .placeholder()) } diff --git a/Packages/Models/Sources/Models/Notification.swift b/Packages/Models/Sources/Models/Notification.swift index a82c119a..96589008 100644 --- a/Packages/Models/Sources/Models/Notification.swift +++ b/Packages/Models/Sources/Models/Notification.swift @@ -18,7 +18,7 @@ public struct Notification: Decodable, Identifiable { public static func placeholder() -> Notification { .init(id: UUID().uuidString, type: NotificationType.favourite.rawValue, - createdAt: "2022-12-16T10:20:54.000Z", + createdAt: ServerDate(), account: .placeholder(), status: .placeholder()) } diff --git a/Packages/Models/Sources/Models/Poll.swift b/Packages/Models/Sources/Models/Poll.swift index b3921ebc..69f28bd5 100644 --- a/Packages/Models/Sources/Models/Poll.swift +++ b/Packages/Models/Sources/Models/Poll.swift @@ -30,12 +30,12 @@ public struct Poll: Codable, Equatable, Hashable { } public struct NullableString: Codable, Equatable, Hashable { - public let value: String? + public let value: ServerDate? public init(from decoder: Decoder) throws { do { let container = try decoder.singleValueContainer() - value = try container.decode(String.self) + value = try container.decode(ServerDate.self) } catch { value = nil } diff --git a/Packages/Models/Sources/Models/Status.swift b/Packages/Models/Sources/Models/Status.swift index 98b1cad6..114aef44 100644 --- a/Packages/Models/Sources/Models/Status.swift +++ b/Packages/Models/Sources/Models/Status.swift @@ -54,15 +54,19 @@ public protocol AnyStatus { var language: String? { get } } +extension AnyStatus { + public var viewId: String { + get { + "\(id)\(createdAt.asDate.description)\(editedAt?.asDate.description ?? "")" + } + } +} + protocol StatusUI { var userMentioned: Bool? { get set } } public struct Status: AnyStatus, Codable, Identifiable, Equatable, Hashable, StatusUI { - public var viewId: String { - id + createdAt + (editedAt ?? "") - } - public var userMentioned: Bool? public static func == (lhs: Status, rhs: Status) -> Bool { @@ -105,7 +109,7 @@ public struct Status: AnyStatus, Codable, Identifiable, Equatable, Hashable, Sta content: .init(stringValue: "Lorem ipsum [#dolor](#) sit amet\nconsectetur [@adipiscing](#) elit\nAsed do eiusmod tempor incididunt ut labore.", parseMarkdown: forSettings), account: .placeholder(), - createdAt: ServerDate.sampleDate, + createdAt: ServerDate(), editedAt: nil, reblog: nil, mediaAttachments: [], @@ -137,10 +141,6 @@ public struct Status: AnyStatus, Codable, Identifiable, Equatable, Hashable, Sta } public struct ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Hashable { - public var viewId: String { - id + createdAt + (editedAt ?? "") - } - public static func == (lhs: ReblogStatus, rhs: ReblogStatus) -> Bool { lhs.id == rhs.id } @@ -152,7 +152,7 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Hashabl public let id: String public let content: HTMLString public let account: Account - public let createdAt: String + public let createdAt: ServerDate public let editedAt: ServerDate? public let mediaAttachments: [MediaAttachment] public let mentions: [Mention] diff --git a/Packages/Models/Sources/Models/StatusHistory.swift b/Packages/Models/Sources/Models/StatusHistory.swift index 86353e2a..5664c427 100644 --- a/Packages/Models/Sources/Models/StatusHistory.swift +++ b/Packages/Models/Sources/Models/StatusHistory.swift @@ -2,7 +2,7 @@ import Foundation public struct StatusHistory: Decodable, Identifiable { public var id: String { - createdAt.description + createdAt.asDate.description } public let content: HTMLString diff --git a/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift b/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift index 3a2c343e..95437ee4 100644 --- a/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift +++ b/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift @@ -23,7 +23,7 @@ extension Array where Element == Notification { status: notification.status) } .sorted { - $0.createdAt > $1.createdAt + $0.createdAt.asDate > $1.createdAt.asDate } } } diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 3379034a..1462f92a 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -96,24 +96,7 @@ public struct StatusRowView: View { } .accessibilityElement(children: viewModel.isFocused ? .contain : .combine) .accessibilityActions { - // Add the individual mentions as accessibility actions - ForEach(viewModel.status.mentions, id: \.id) { mention in - Button("@\(mention.username)") { - routerPath.navigate(to: .accountDetail(id: mention.id)) - } - } - - Button(viewModel.displaySpoiler ? "status.show-more" : "status.show-less") { - withAnimation { - viewModel.displaySpoiler.toggle() - } - } - - Button("@\(viewModel.status.account.username)") { - routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id)) - } - - contextMenu + accesibilityActions } .background { Color.clear @@ -144,6 +127,28 @@ public struct StatusRowView: View { } } } + + @ViewBuilder + private var accesibilityActions: some View { + // Add the individual mentions as accessibility actions + ForEach(viewModel.status.mentions, id: \.id) { mention in + Button("@\(mention.username)") { + routerPath.navigate(to: .accountDetail(id: mention.id)) + } + } + + Button(viewModel.displaySpoiler ? "status.show-more" : "status.show-less") { + withAnimation { + viewModel.displaySpoiler.toggle() + } + } + + Button("@\(viewModel.status.account.username)") { + routerPath.navigate(to: .accountDetail(id: viewModel.status.account.id)) + } + + contextMenu + } private func makeFilterView(filter: Filter) -> some View { HStack { diff --git a/Packages/Timeline/Sources/Timeline/TimelineCache.swift b/Packages/Timeline/Sources/Timeline/TimelineCache.swift index bc2fe152..5744fb73 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineCache.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineCache.swift @@ -45,7 +45,7 @@ public actor TimelineCache { return try await engine .readAllData() .map { try decoder.decode(Status.self, from: $0) } - .sorted(by: { $0.createdAt > $1.createdAt }) + .sorted(by: { $0.createdAt.asDate > $1.createdAt.asDate }) } catch { return nil }