summaryrefslogtreecommitdiff
path: root/Hutch/Views/Inbox
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-15 22:41:33 -0500
committerGitHub <[email protected]>2026-07-15 22:41:33 -0500
commit922502a2c74c66034f3ec2db6612f2a36d242042 (patch)
tree0867730c955e308437b8c86dd37638c9bff6c9c5 /Hutch/Views/Inbox
parentb9ec80716ea015de5b6b31395fdc5ff03191398c (diff)
parent1d2769fc7a347939275e9130ee174d61d96ea401 (diff)
downloadhutch-922502a2c74c66034f3ec2db6612f2a36d242042.tar.gz
hutch-922502a2c74c66034f3ec2db6612f2a36d242042.tar.bz2
hutch-922502a2c74c66034f3ec2db6612f2a36d242042.zip
Merge pull request #4 from zerolabsco/phase-2-patchsets
Phase 2: patchset review
Diffstat (limited to 'Hutch/Views/Inbox')
-rw-r--r--Hutch/Views/Inbox/InboxThreadUtilities.swift88
-rw-r--r--Hutch/Views/Inbox/ThreadViewModel.swift91
2 files changed, 90 insertions, 89 deletions
diff --git a/Hutch/Views/Inbox/InboxThreadUtilities.swift b/Hutch/Views/Inbox/InboxThreadUtilities.swift
index 1dd88a3..6958b50 100644
--- a/Hutch/Views/Inbox/InboxThreadUtilities.swift
+++ b/Hutch/Views/Inbox/InboxThreadUtilities.swift
@@ -8,4 +8,92 @@ enum InboxThreadUtilities {
}
return nil
}
+
+ /// Splits an email body into its commit message and diff, so patch mail can be
+ /// rendered as prose plus a diff rather than one undifferentiated blob.
+ ///
+ /// Shared by the inbox thread view and patchset review: sr.ht's `Patch` type
+ /// carries no diff, so the diff has to be recovered from the email body.
+ 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
+ }
+
+ 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
+ }
+
+ nonisolated static func isEmailSignatureSeparator(_ line: String) -> Bool {
+ line == "-- " || line == "--"
+ }
+
+ nonisolated static func normalizeLineEndings(in text: String) -> String {
+ text
+ .replacingOccurrences(of: "\r\n", with: "\n")
+ .replacingOccurrences(of: "\r", with: "\n")
+ }
}
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 {