krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2.11.0: Hutch/Models/Inbox.swift · raw
1import Foundation
2
3struct InboxThreadSummary: Identifiable, Hashable, Sendable {
4 let rootEmailID: Int
5 let rootMessageID: String
6 let threadRootEmailIDs: [Int]
7 let threadRootMessageIDs: [String]
8 let listID: Int
9 let listRID: String
10 let listName: String
11 let listOwner: Entity
12 let subject: String
13 let latestSender: Entity
14 let lastActivityAt: Date
15 let messageCount: Int?
16 let repo: String?
17 let containsPatch: Bool
18 let isUnread: Bool
19
20 var id: String {
21 threadGroupingKey
22 }
23
24 var listDisplayName: String {
25 "\(listOwner.canonicalName)/\(listName)"
26 }
27
28 var displaySubject: String {
29 Self.normalizedSubject(from: subject)
30 }
31
32 var metadataLine: String {
33 var parts = [latestSenderDisplayName]
34 if let messageCount, messageCount > 1 {
35 let replyCount = max(messageCount - 1, 1)
36 parts.append("\(replyCount) repl\(replyCount == 1 ? "y" : "ies")")
37 }
38 parts.append(lastActivityAt.relativeDescription)
39 return parts.joined(separator: " • ")
40 }
41
42 var latestSenderDisplayName: String {
43 let canonicalName = latestSender.canonicalName.trimmingCharacters(in: .whitespacesAndNewlines)
44 if canonicalName.contains("@") {
45 return canonicalName
46 }
47 return canonicalName
48 }
49
50 var debugIdentifierSummary: String {
51 "subject=\(subject) listRID=\(listRID) listID=\(listID) rootEmailID=\(rootEmailID) rootMessageID=\(rootMessageID) groupingKey=\(threadGroupingKey)"
52 }
53
54 var threadGroupingKey: String {
55 "\(listRID)#\(displaySubject.lowercased())"
56 }
57
58 private static func normalizedSubject(from subject: String) -> String {
59 let collapsedWhitespace = subject
60 .replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
61 .trimmingCharacters(in: .whitespacesAndNewlines)
62
63 let pattern = #"^(?:(?:re|fwd?)\s*:\s*)+"#
64 return collapsedWhitespace.replacingOccurrences(
65 of: pattern,
66 with: "",
67 options: [.regularExpression, .caseInsensitive]
68 )
69 }
70}
71
72struct InboxMessage: Identifiable, Hashable, Sendable {
73 let id: Int
74 let author: Entity
75 let date: Date
76 let subject: String
77 let body: String
78 let senderDisplayName: String
79 let senderEmailAddress: String?
80 let isPatch: Bool
81 let contentBlocks: [InboxMessageContentBlock]
82 let rawMessageURL: URL?
83}
84
85enum InboxMessageContentBlock: Hashable, Sendable {
86 case plainText(String)
87 case diff(String)
88}
89
90struct InboxThreadDetail: Sendable {
91 let id: String
92 let rootEmailID: Int
93 let rootMessageID: String
94 let subject: String
95 let author: Entity
96 let lastActivityAt: Date
97 let mailto: String?
98 let listID: Int
99 let listRID: String
100 let listName: String
101 let listOwner: Entity
102 let messageCount: Int?
103 let messages: [InboxMessage]
104
105 var listDisplayName: String {
106 "\(listOwner.canonicalName)/\(listName)"
107 }
108}
109
110extension InboxThreadDetail {
111 var replyRecipient: String {
112 "\(listOwner.canonicalName)/\(listName)@lists.sr.ht"
113 }
114
115 var replySubject: String {
116 subject.lowercased().hasPrefix("re:") ? subject : "Re: \(subject)"
117 }
118
119 var displaySubject: String {
120 InboxThreadSummary(
121 rootEmailID: rootEmailID,
122 rootMessageID: rootMessageID,
123 threadRootEmailIDs: [rootEmailID],
124 threadRootMessageIDs: [rootMessageID],
125 listID: listID,
126 listRID: listRID,
127 listName: listName,
128 listOwner: listOwner,
129 subject: subject,
130 latestSender: author,
131 lastActivityAt: lastActivityAt,
132 messageCount: messageCount,
133 repo: nil,
134 containsPatch: messages.contains(where: \.isPatch),
135 isUnread: false
136 ).displaySubject
137 }
138}
139
140struct MailComposeDraft: Sendable {
141 let recipients: [String]
142 let ccRecipients: [String]
143 let subject: String
144 let body: String
145
146 var id: String {
147 ([subject] + recipients + ccRecipients).joined(separator: "|")
148 }
149}
150
151extension MailComposeDraft: Identifiable {}
152
153struct InboxMailingListReference: Decodable, Sendable, Hashable, Identifiable {
154 let id: Int
155 let rid: String
156 let name: String
157 let owner: Entity
158}
159
160struct InboxPatchPreview: Decodable, Sendable, Hashable {
161 let subject: String?
162}
163
164enum InboxReadStateStore {
165 private static let key = "InboxThreadLastViewed"
166
167 static func lastViewedAt(for threadID: String, defaults: UserDefaults = .standard) -> Date? {
168 guard let dictionary = defaults.dictionary(forKey: key) as? [String: TimeInterval],
169 let timestamp = dictionary[threadID] else {
170 return nil
171 }
172 return Date(timeIntervalSince1970: timestamp)
173 }
174
175 static func markViewed(_ date: Date, for threadID: String, defaults: UserDefaults = .standard) {
176 var dictionary = defaults.dictionary(forKey: key) as? [String: TimeInterval] ?? [:]
177 dictionary[threadID] = date.timeIntervalSince1970
178 defaults.set(dictionary, forKey: key)
179 }
180
181 static func markUnread(for threadID: String, defaults: UserDefaults = .standard) {
182 var dictionary = defaults.dictionary(forKey: key) as? [String: TimeInterval] ?? [:]
183 dictionary.removeValue(forKey: threadID)
184 defaults.set(dictionary, forKey: key)
185 }
186
187 static func isUnread(threadID: String, lastActivityAt: Date, defaults: UserDefaults = .standard) -> Bool {
188 guard let lastViewedAt = lastViewedAt(for: threadID, defaults: defaults) else {
189 return true
190 }
191 return lastActivityAt > lastViewedAt
192 }
193}