aboutsummaryrefslogtreecommitdiff
path: root/Hutch/Views/Inbox/ThreadViewModel.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-15 21:42:38 -0500
committerChristian Cleberg <[email protected]>2026-07-15 21:42:38 -0500
commitccec322f8eac4d14638f5abdae7dae7abc95eb7e (patch)
tree7eb92e38d5dc88ba839b94c294ae9be0ea21a6d0 /Hutch/Views/Inbox/ThreadViewModel.swift
parentb9ec80716ea015de5b6b31395fdc5ff03191398c (diff)
downloadhutch-ccec322f8eac4d14638f5abdae7dae7abc95eb7e.tar.gz
hutch-ccec322f8eac4d14638f5abdae7dae7abc95eb7e.tar.bz2
hutch-ccec322f8eac4d14638f5abdae7dae7abc95eb7e.zip
refactor: share the email body diff splitter
segmentMessageBody and its helpers were private to ThreadViewModel, reachable from tests only through a segmentMessageBodyForTesting shim. Patchset review needs the same splitting, because sr.ht's Patch type carries no diff — the diff only exists inside the email body — so this has to be shared rather than duplicated. Moved to InboxThreadUtilities. The shim is gone; the existing test calls the real function directly now. Also adds the Patchset model layer that the coming views build on.
Diffstat (limited to 'Hutch/Views/Inbox/ThreadViewModel.swift')
-rw-r--r--Hutch/Views/Inbox/ThreadViewModel.swift91
1 files changed, 2 insertions, 89 deletions
diff --git a/Hutch/Views/Inbox/ThreadViewModel.swift b/Hutch/Views/Inbox/ThreadViewModel.swift
index 850b83c..f422fe3 100644
--- a/Hutch/Views/Inbox/ThreadViewModel.swift
+++ b/Hutch/Views/Inbox/ThreadViewModel.swift
@@ -446,7 +446,7 @@ final class ThreadViewModel {
let normalizedIdentity = normalizedSenderIdentity(from: body, fallbackAuthor: author)
let displayBody = sanitizedDisplayBody(from: body)
- let contentBlocks = segmentMessageBody(displayBody, isPatch: payload.patch != nil)
+ let contentBlocks = InboxThreadUtilities.segmentMessageBody(displayBody, isPatch: payload.patch != nil)
return InboxMessage(
id: id,
@@ -540,7 +540,7 @@ final class ThreadViewModel {
}
private static func sanitizedDisplayBody(from body: String) -> String {
- let normalizedBody = normalizeLineEndings(in: body)
+ let normalizedBody = InboxThreadUtilities.normalizeLineEndings(in: body)
let lines = normalizedBody.components(separatedBy: "\n")
let headerPrefixes = ["From:", "Date:", "To:", "Cc:", "Subject:"]
var headerCount = 0
@@ -565,93 +565,6 @@ final class ThreadViewModel {
return lines.dropFirst(blankLineIndex + 1).joined(separator: "\n")
}
- nonisolated static func segmentMessageBodyForTesting(_ body: String, isPatch: Bool) -> [InboxMessageContentBlock] {
- segmentMessageBody(body, isPatch: isPatch)
- }
-
- private nonisolated static func segmentMessageBody(_ body: String, isPatch: Bool) -> [InboxMessageContentBlock] {
- guard isPatch else {
- let trimmedBody = body.trimmingCharacters(in: .whitespacesAndNewlines)
- return trimmedBody.isEmpty ? [] : [.plainText(trimmedBody)]
- }
-
- let normalizedBody = normalizeLineEndings(in: body)
- let lines = normalizedBody.components(separatedBy: "\n")
- guard let diffStartIndex = actualDiffStartIndex(in: lines) else {
- let trimmedBody = normalizedBody.trimmingCharacters(in: .whitespacesAndNewlines)
- return trimmedBody.isEmpty ? [] : [.plainText(trimmedBody)]
- }
-
- var blocks: [InboxMessageContentBlock] = []
- let leadingPlainText = lines[..<diffStartIndex]
- .joined(separator: "\n")
- .trimmingCharacters(in: .whitespacesAndNewlines)
- if !leadingPlainText.isEmpty {
- blocks.append(.plainText(leadingPlainText))
- }
-
- let remainingLines = Array(lines[diffStartIndex...])
- let signatureIndex = remainingLines.firstIndex(where: isEmailSignatureSeparator)
-
- let diffLines: ArraySlice<String>
- let trailingPlainText: String
- if let signatureIndex {
- diffLines = remainingLines[..<signatureIndex]
- trailingPlainText = remainingLines[signatureIndex...]
- .joined(separator: "\n")
- .trimmingCharacters(in: .whitespacesAndNewlines)
- } else {
- diffLines = remainingLines[...]
- trailingPlainText = ""
- }
-
- let diff = diffLines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
- if !diff.isEmpty {
- blocks.append(.diff(diff))
- }
-
- if !trailingPlainText.isEmpty {
- blocks.append(.plainText(trailingPlainText))
- }
- return blocks
- }
-
- private nonisolated static func actualDiffStartIndex(in lines: [String]) -> Int? {
- if let explicitDiffIndex = lines.firstIndex(where: { $0.hasPrefix("diff --git ") }) {
- return explicitDiffIndex
- }
-
- for index in lines.indices {
- let line = lines[index]
- guard line.hasPrefix("--- ") else { continue }
- let nextIndex = lines.index(after: index)
- guard nextIndex < lines.endIndex else { continue }
- let nextLine = lines[nextIndex]
- guard nextLine.hasPrefix("+++ ") else { continue }
-
- let oldPath = String(line.dropFirst(4))
- let newPath = String(nextLine.dropFirst(4))
- let looksLikeUnifiedDiff = (oldPath.hasPrefix("a/") || oldPath == "/dev/null") &&
- (newPath.hasPrefix("b/") || newPath == "/dev/null")
-
- if looksLikeUnifiedDiff {
- return index
- }
- }
-
- return nil
- }
-
- private nonisolated static func isEmailSignatureSeparator(_ line: String) -> Bool {
- line == "-- " || line == "--"
- }
-
- private nonisolated static func normalizeLineEndings(in text: String) -> String {
- text
- .replacingOccurrences(of: "\r\n", with: "\n")
- .replacingOccurrences(of: "\r", with: "\n")
- }
-
private static func stripLeadingFromLineIfPresent(in body: String) -> String {
let lines = body.components(separatedBy: "\n")
guard let firstLine = lines.first, firstLine.hasPrefix("From:") else {