summaryrefslogtreecommitdiff
path: root/Hutch/Models
diff options
context:
space:
mode:
Diffstat (limited to 'Hutch/Models')
-rw-r--r--Hutch/Models/Inbox.swift45
1 files changed, 41 insertions, 4 deletions
diff --git a/Hutch/Models/Inbox.swift b/Hutch/Models/Inbox.swift
index be2e105..c8e41bb 100644
--- a/Hutch/Models/Inbox.swift
+++ b/Hutch/Models/Inbox.swift
@@ -170,6 +170,7 @@ struct InboxPatchPreview: Decodable, Sendable, Hashable {
enum InboxReadStateStore {
private static let key = "InboxThreadLastViewed"
+ private static let baselineKey = "InboxUnreadBaseline"
static func lastViewedAt(for threadID: String, defaults: UserDefaults = .standard) -> Date? {
guard let dictionary = defaults.dictionary(forKey: key) as? [String: TimeInterval],
@@ -185,16 +186,52 @@ enum InboxReadStateStore {
defaults.set(dictionary, forKey: key)
}
+ /// Records an explicit unread marker rather than forgetting the thread.
+ ///
+ /// Deleting the entry would drop the thread back to the baseline rule below,
+ /// which would call anything older than the baseline read — so marking an old
+ /// thread unread would appear to do nothing. `distantPast` always compares as
+ /// older than the thread's activity, so the thread reads as unread.
static func markUnread(for threadID: String, defaults: UserDefaults = .standard) {
var dictionary = defaults.dictionary(forKey: key) as? [String: TimeInterval] ?? [:]
- dictionary.removeValue(forKey: threadID)
+ dictionary[threadID] = Date.distantPast.timeIntervalSince1970
defaults.set(dictionary, forKey: key)
}
+ /// Mail that arrived before this is treated as already read.
+ static func baseline(defaults: UserDefaults = .standard) -> Date? {
+ guard let timestamp = defaults.object(forKey: baselineKey) as? TimeInterval else {
+ return nil
+ }
+ return Date(timeIntervalSince1970: timestamp)
+ }
+
+ /// Sets the point from which mail counts as unread. Called once per account,
+ /// when the account is activated.
+ ///
+ /// Without this, every thread a list has ever carried is unread on first
+ /// login, because an absent view record reads as unread. On a busy list that
+ /// is thousands of threads, none of which the user has any intention of
+ /// reading.
+ ///
+ /// An account that already has read state has been in use, so it keeps the
+ /// old behavior — a baseline of `distantPast` leaves every existing unread
+ /// thread unread rather than silently marking a real backlog as read.
+ static func establishBaselineIfNeeded(now: Date = .now, defaults: UserDefaults = .standard) {
+ guard defaults.object(forKey: baselineKey) == nil else { return }
+
+ let hasExistingReadState = !((defaults.dictionary(forKey: key) as? [String: TimeInterval])?.isEmpty ?? true)
+ let baseline = hasExistingReadState ? Date.distantPast : now
+ defaults.set(baseline.timeIntervalSince1970, forKey: baselineKey)
+ }
+
static func isUnread(threadID: String, lastActivityAt: Date, defaults: UserDefaults = .standard) -> Bool {
- guard let lastViewedAt = lastViewedAt(for: threadID, defaults: defaults) else {
- return true
+ if let lastViewedAt = lastViewedAt(for: threadID, defaults: defaults) {
+ return lastActivityAt > lastViewedAt
+ }
+ if let baseline = baseline(defaults: defaults), lastActivityAt <= baseline {
+ return false
}
- return lastActivityAt > lastViewedAt
+ return true
}
}