Faster / Cached ServerDate decoding

This commit is contained in:
Thomas Ricouard 2023-02-09 09:12:44 +01:00
parent 76c0c843cd
commit 013410a80e
10 changed files with 79 additions and 55 deletions

View File

@ -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")!, header: URL(string: "https://files.mastodon.social/media_attachments/files/003/134/405/original/04060b07ddf7bb0b.png")!,
acct: "johnm@example.com", acct: "johnm@example.com",
note: .init(stringValue: "Some content"), note: .init(stringValue: "Some content"),
createdAt: "2022-12-16T10:20:54.000Z", createdAt: ServerDate(),
followersCount: 10, followersCount: 10,
followingCount: 10, followingCount: 10,
statusesCount: 10, statusesCount: 10,

View File

@ -1,13 +1,16 @@
import Foundation import Foundation
public typealias ServerDate = String fileprivate enum CodingKeys: CodingKey {
case asDate, relativeFormatted, shortDateFormatted
}
extension ServerDate { public struct ServerDate: Codable, Hashable, Equatable {
public let asDate: Date
public static var sampleDate: ServerDate { public let relativeFormatted: String
createdAtDateFormatter.string(from: Date()-100) public let shortDateFormatted: String
}
private static let calendar = Calendar(identifier: .gregorian)
private static var createdAtDateFormatter: DateFormatter = { private static var createdAtDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.calendar = .init(identifier: .iso8601) dateFormatter.calendar = .init(identifier: .iso8601)
@ -15,31 +18,47 @@ extension ServerDate {
dateFormatter.timeZone = .init(abbreviation: "UTC") dateFormatter.timeZone = .init(abbreviation: "UTC")
return dateFormatter return dateFormatter
}() }()
private static var createdAtRelativeFormatter: RelativeDateTimeFormatter = { private static var createdAtRelativeFormatter: RelativeDateTimeFormatter = {
let dateFormatter = RelativeDateTimeFormatter() let dateFormatter = RelativeDateTimeFormatter()
dateFormatter.unitsStyle = .short dateFormatter.unitsStyle = .short
return dateFormatter return dateFormatter
}() }()
private static var createdAtShortDateFormatted: DateFormatter = { private static var createdAtShortDateFormatted: DateFormatter = {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .none dateFormatter.timeStyle = .none
return dateFormatter return dateFormatter
}() }()
private static let calendar = Calendar(identifier: .gregorian) public init() {
asDate = Date()-100
public var asDate: Date { relativeFormatted = Self.createdAtRelativeFormatter.localizedString(for: asDate, relativeTo: Date())
Self.createdAtDateFormatter.date(from: self)! shortDateFormatted = Self.createdAtShortDateFormatted.string(from: asDate)
} }
public var relativeFormatted: String { public init(from decoder: Decoder) throws {
return Self.createdAtRelativeFormatter.localizedString(for: asDate, relativeTo: Date()) 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 { public func encode(to encoder: Encoder) throws {
return Self.createdAtShortDateFormatted.string(from: asDate) 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)
} }
} }

View File

@ -32,7 +32,7 @@ public struct ConsolidatedNotification: Identifiable {
public static func placeholder() -> ConsolidatedNotification { public static func placeholder() -> ConsolidatedNotification {
.init(notifications: [Notification.placeholder()], .init(notifications: [Notification.placeholder()],
type: .favourite, type: .favourite,
createdAt: "2022-12-16T10:20:54.000Z", createdAt: ServerDate(),
accounts: [.placeholder()], accounts: [.placeholder()],
status: .placeholder()) status: .placeholder())
} }

View File

@ -18,7 +18,7 @@ public struct Notification: Decodable, Identifiable {
public static func placeholder() -> Notification { public static func placeholder() -> Notification {
.init(id: UUID().uuidString, .init(id: UUID().uuidString,
type: NotificationType.favourite.rawValue, type: NotificationType.favourite.rawValue,
createdAt: "2022-12-16T10:20:54.000Z", createdAt: ServerDate(),
account: .placeholder(), account: .placeholder(),
status: .placeholder()) status: .placeholder())
} }

View File

@ -30,12 +30,12 @@ public struct Poll: Codable, Equatable, Hashable {
} }
public struct NullableString: Codable, Equatable, Hashable { public struct NullableString: Codable, Equatable, Hashable {
public let value: String? public let value: ServerDate?
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
do { do {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
value = try container.decode(String.self) value = try container.decode(ServerDate.self)
} catch { } catch {
value = nil value = nil
} }

View File

@ -54,15 +54,19 @@ public protocol AnyStatus {
var language: String? { get } var language: String? { get }
} }
extension AnyStatus {
public var viewId: String {
get {
"\(id)\(createdAt.asDate.description)\(editedAt?.asDate.description ?? "")"
}
}
}
protocol StatusUI { protocol StatusUI {
var userMentioned: Bool? { get set } var userMentioned: Bool? { get set }
} }
public struct Status: AnyStatus, Codable, Identifiable, Equatable, Hashable, StatusUI { public struct Status: AnyStatus, Codable, Identifiable, Equatable, Hashable, StatusUI {
public var viewId: String {
id + createdAt + (editedAt ?? "")
}
public var userMentioned: Bool? public var userMentioned: Bool?
public static func == (lhs: Status, rhs: Status) -> 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), content: .init(stringValue: "Lorem ipsum [#dolor](#) sit amet\nconsectetur [@adipiscing](#) elit\nAsed do eiusmod tempor incididunt ut labore.", parseMarkdown: forSettings),
account: .placeholder(), account: .placeholder(),
createdAt: ServerDate.sampleDate, createdAt: ServerDate(),
editedAt: nil, editedAt: nil,
reblog: nil, reblog: nil,
mediaAttachments: [], mediaAttachments: [],
@ -137,10 +141,6 @@ public struct Status: AnyStatus, Codable, Identifiable, Equatable, Hashable, Sta
} }
public struct ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Hashable { public struct ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Hashable {
public var viewId: String {
id + createdAt + (editedAt ?? "")
}
public static func == (lhs: ReblogStatus, rhs: ReblogStatus) -> Bool { public static func == (lhs: ReblogStatus, rhs: ReblogStatus) -> Bool {
lhs.id == rhs.id lhs.id == rhs.id
} }
@ -152,7 +152,7 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable, Equatable, Hashabl
public let id: String public let id: String
public let content: HTMLString public let content: HTMLString
public let account: Account public let account: Account
public let createdAt: String public let createdAt: ServerDate
public let editedAt: ServerDate? public let editedAt: ServerDate?
public let mediaAttachments: [MediaAttachment] public let mediaAttachments: [MediaAttachment]
public let mentions: [Mention] public let mentions: [Mention]

View File

@ -2,7 +2,7 @@ import Foundation
public struct StatusHistory: Decodable, Identifiable { public struct StatusHistory: Decodable, Identifiable {
public var id: String { public var id: String {
createdAt.description createdAt.asDate.description
} }
public let content: HTMLString public let content: HTMLString

View File

@ -23,7 +23,7 @@ extension Array where Element == Notification {
status: notification.status) status: notification.status)
} }
.sorted { .sorted {
$0.createdAt > $1.createdAt $0.createdAt.asDate > $1.createdAt.asDate
} }
} }
} }

View File

@ -96,24 +96,7 @@ public struct StatusRowView: View {
} }
.accessibilityElement(children: viewModel.isFocused ? .contain : .combine) .accessibilityElement(children: viewModel.isFocused ? .contain : .combine)
.accessibilityActions { .accessibilityActions {
// Add the individual mentions as accessibility actions accesibilityActions
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
} }
.background { .background {
Color.clear 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 { private func makeFilterView(filter: Filter) -> some View {
HStack { HStack {

View File

@ -45,7 +45,7 @@ public actor TimelineCache {
return try await engine return try await engine
.readAllData() .readAllData()
.map { try decoder.decode(Status.self, from: $0) } .map { try decoder.decode(Status.self, from: $0) }
.sorted(by: { $0.createdAt > $1.createdAt }) .sorted(by: { $0.createdAt.asDate > $1.createdAt.asDate })
} catch { } catch {
return nil return nil
} }