krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

b82bbeeea48ad27832a355c2a41408559c411104

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-03-19T21:59:16Z

v2.1: bundled polish and fixes
 Hutch/App/RootView.swift                           | 30 ++++-----
 Hutch/Networking/SRHTError.swift                   | 72 ++++++++++++++++++++++
 Hutch/Views/Auth/AuthView.swift                    |  2 +-
 Hutch/Views/Builds/BuildDetailViewModel.swift      | 10 +--
 Hutch/Views/Builds/BuildListView.swift             |  1 +
 Hutch/Views/Builds/BuildListViewModel.swift        |  6 +-
 Hutch/Views/Home/HomeViewModel.swift               |  6 +-
 Hutch/Views/Inbox/InboxView.swift                  | 15 -----
 Hutch/Views/Inbox/InboxViewModel.swift             | 17 +----
 Hutch/Views/Inbox/ThreadDetailView.swift           | 14 +----
 Hutch/Views/Inbox/ThreadViewModel.swift            | 47 ++------------
 Hutch/Views/Pastes/PasteDetailViewModel.swift      |  8 +--
 Hutch/Views/Pastes/PasteListViewModel.swift        |  6 +-
 .../Views/Repositories/CommitDetailViewModel.swift |  2 +-
 Hutch/Views/Repositories/FileTreeViewModel.swift   |  8 +--
 .../Repositories/HgRepositoryDetailViewModel.swift | 10 +--
 .../HgRepositorySettingsViewModel.swift            | 12 ++--
 .../Repositories/RepositoryDetailViewModel.swift   | 10 +--
 Hutch/Views/Repositories/RepositoryListView.swift  |  1 +
 .../Repositories/RepositoryListViewModel.swift     | 17 +----
 .../Repositories/RepositorySettingsViewModel.swift | 12 ++--
 Hutch/Views/Settings/SettingsViewModel.swift       | 18 +++---
 Hutch/Views/Tickets/TicketDetailView.swift         |  8 +--
 Hutch/Views/Tickets/TicketDetailViewModel.swift    | 20 +++---
 Hutch/Views/Tickets/TicketListView.swift           |  1 +
 Hutch/Views/Tickets/TicketListViewModel.swift      |  6 +-
 Hutch/Views/Tickets/TrackerListViewModel.swift     | 19 +-----
 27 files changed, 177 insertions(+), 201 deletions(-)

diff --git a/Hutch/App/RootView.swift b/Hutch/App/RootView.swift
index 0885835..f615541 100644
--- a/Hutch/App/RootView.swift
+++ b/Hutch/App/RootView.swift
@@ -146,12 +146,10 @@ struct RootView: View {
             resolveRepositoryLink(owner: owner, repo: repo)
 
         case .build(let jobId):
-            // Reset the builds navigation and push the detail
             buildsPath = NavigationPath()
             appState.selectedTab = .builds
-            // Defer the push slightly so the tab switch takes effect
-            Task { @MainActor in
-                try? await Task.sleep(for: .milliseconds(100))
+            Task {
+                await settleNavigationTransition()
                 buildsPath.append(jobId)
             }
 
@@ -165,26 +163,25 @@ struct RootView: View {
         case .repository(let repository):
             repoPath = NavigationPath()
             appState.selectedTab = .repositories
-            Task { @MainActor in
-                try? await Task.sleep(for: .milliseconds(100))
+            Task {
+                await settleNavigationTransition()
                 repoPath.append(repository)
             }
 
         case .tracker(let tracker):
             ticketsPath = NavigationPath()
             appState.selectedTab = .tickets
-            Task { @MainActor in
-                try? await Task.sleep(for: .milliseconds(100))
+            Task {
+                await settleNavigationTransition()
                 ticketsPath.append(tracker)
             }
 
         case .mailingList(let mailingList):
             morePath = NavigationPath()
             appState.selectedTab = .more
-            Task { @MainActor in
-                try? await Task.sleep(for: .milliseconds(100))
+            Task {
+                await settleNavigationTransition()
                 morePath.append(MoreRoute.lists)
-                try? await Task.sleep(for: .milliseconds(100))
                 morePath.append(MoreRoute.mailingList(mailingList))
             }
         }
@@ -198,7 +195,7 @@ struct RootView: View {
                 let summary = try await appState.resolveRepository(owner: owner, name: repo)
                 repoPath = NavigationPath()
                 appState.selectedTab = .repositories
-                try? await Task.sleep(for: .milliseconds(100))
+                await settleNavigationTransition()
                 repoPath.append(summary)
             } catch {
                 // Silently fail — the repo may not exist or be inaccessible
@@ -214,9 +211,8 @@ struct RootView: View {
                 let trackerSummary = try await appState.resolveTracker(owner: owner, name: tracker)
                 ticketsPath = NavigationPath()
                 appState.selectedTab = .tickets
-                try? await Task.sleep(for: .milliseconds(100))
+                await settleNavigationTransition()
                 ticketsPath.append(trackerSummary)
-                try? await Task.sleep(for: .milliseconds(100))
                 ticketsPath.append(TicketDeepLinkTarget(
                     ownerUsername: String(trackerSummary.owner.canonicalName.dropFirst()),
                     trackerName: trackerSummary.name,
@@ -229,6 +225,12 @@ struct RootView: View {
             }
         }
     }
+
+    @MainActor
+    private func settleNavigationTransition() async {
+        await Task.yield()
+        await Task.yield()
+    }
 }
 
 enum MoreDestination: Hashable {
diff --git a/Hutch/Networking/SRHTError.swift b/Hutch/Networking/SRHTError.swift
index f2da408..3c3a87f 100644
--- a/Hutch/Networking/SRHTError.swift
+++ b/Hutch/Networking/SRHTError.swift
@@ -33,6 +33,55 @@ enum SRHTError: LocalizedError, Sendable {
         }
     }
 
+    var userFacingMessage: String {
+        switch self {
+        case .graphQLErrors(let errors):
+            let firstMessage = errors.first?.message.lowercased() ?? ""
+            if firstMessage.contains("unauthorized") || firstMessage.contains("forbidden") {
+                return "You do not have permission to do that."
+            }
+            if firstMessage.contains("not found") || firstMessage.contains("no rows in result set") {
+                return "That content is no longer available."
+            }
+            return "Something went wrong. Please try again."
+        case .httpError(let code):
+            if code == 401 {
+                return "Please sign in again."
+            }
+            if code == 403 {
+                return "You do not have permission to do that."
+            }
+            if code == 404 {
+                return "That content is no longer available."
+            }
+            if (500...599).contains(code) {
+                return "The server is unavailable right now. Please try again."
+            }
+            return "Something went wrong. Please try again."
+        case .invalidAuthenticatedURL:
+            return "That request could not be completed."
+        case .decodingError:
+            return "The response could not be loaded right now."
+        case .networkError(let error):
+            let nsError = error as NSError
+            switch nsError.code {
+            case NSURLErrorNotConnectedToInternet,
+                 NSURLErrorNetworkConnectionLost,
+                 NSURLErrorTimedOut,
+                 NSURLErrorCannotFindHost,
+                 NSURLErrorCannotConnectToHost,
+                 NSURLErrorDNSLookupFailed,
+                 NSURLErrorInternationalRoamingOff,
+                 NSURLErrorDataNotAllowed:
+                return "Check your connection and try again."
+            default:
+                return "The network request failed. Please try again."
+            }
+        case .unauthorized:
+            return "Please sign in again."
+        }
+    }
+
     /// Whether this error represents a connectivity issue (no internet, timeout, DNS).
     var isConnectivityError: Bool {
         switch self {
@@ -55,6 +104,29 @@ enum SRHTError: LocalizedError, Sendable {
     }
 }
 
+extension Error {
+    var userFacingMessage: String {
+        if let error = self as? SRHTError {
+            return error.userFacingMessage
+        }
+
+        let nsError = self as NSError
+        switch nsError.code {
+        case NSURLErrorNotConnectedToInternet,
+             NSURLErrorNetworkConnectionLost,
+             NSURLErrorTimedOut,
+             NSURLErrorCannotFindHost,
+             NSURLErrorCannotConnectToHost,
+             NSURLErrorDNSLookupFailed,
+             NSURLErrorInternationalRoamingOff,
+             NSURLErrorDataNotAllowed:
+            return "Check your connection and try again."
+        default:
+            return "Something went wrong. Please try again."
+        }
+    }
+}
+
 /// A single error entry from the GraphQL `errors` array.
 struct GraphQLError: Decodable, Sendable {
     let message: String
diff --git a/Hutch/Views/Auth/AuthView.swift b/Hutch/Views/Auth/AuthView.swift
index cbf8669..5a249ef 100644
--- a/Hutch/Views/Auth/AuthView.swift
+++ b/Hutch/Views/Auth/AuthView.swift
@@ -87,7 +87,7 @@ struct TokenEntryView: View {
             do {
                 try await appState.connect(with: tokenTrimmed)
             } catch {
-                errorMessage = error.localizedDescription
+                errorMessage = error.userFacingMessage
             }
             isConnecting = false
         }
diff --git a/Hutch/Views/Builds/BuildDetailViewModel.swift b/Hutch/Views/Builds/BuildDetailViewModel.swift
index 1768e47..2b91a90 100644
--- a/Hutch/Views/Builds/BuildDetailViewModel.swift
+++ b/Hutch/Views/Builds/BuildDetailViewModel.swift
@@ -110,7 +110,7 @@ final class BuildDetailViewModel {
             }
             job = loadedJob
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoading = false
@@ -127,7 +127,7 @@ final class BuildDetailViewModel {
         do {
             taskLogs[cacheKey] = try await client.fetchText(url: logURL)
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         loadingTaskLogs.remove(cacheKey)
@@ -148,7 +148,7 @@ final class BuildDetailViewModel {
             // Reload job to get updated status.
             await loadJob()
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isCancelling = false
@@ -185,7 +185,7 @@ final class BuildDetailViewModel {
             )
             return result.submit.id
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
             return nil
         }
     }
@@ -233,7 +233,7 @@ final class BuildDetailViewModel {
             )
             return result.submit.id
         } catch {
-            self.error = "Couldn’t submit the build. \(error.localizedDescription)"
+            self.error = "Couldn’t submit the build. \(error.userFacingMessage)"
             return nil
         }
     }
diff --git a/Hutch/Views/Builds/BuildListView.swift b/Hutch/Views/Builds/BuildListView.swift
index 875a836..77abe87 100644
--- a/Hutch/Views/Builds/BuildListView.swift
+++ b/Hutch/Views/Builds/BuildListView.swift
@@ -23,6 +23,7 @@ struct BuildListView: View {
                     } label: {
                         Image(systemName: "plus")
                     }
+                    .accessibilityLabel("Submit build")
                 }
             }
         }
diff --git a/Hutch/Views/Builds/BuildListViewModel.swift b/Hutch/Views/Builds/BuildListViewModel.swift
index 8d0f961..12f6e69 100644
--- a/Hutch/Views/Builds/BuildListViewModel.swift
+++ b/Hutch/Views/Builds/BuildListViewModel.swift
@@ -97,7 +97,7 @@ final class BuildListViewModel {
             hasMore = page.cursor != nil
         } catch {
             if jobs.isEmpty {
-                self.error = error.localizedDescription
+                self.error = error.userFacingMessage
             }
         }
 
@@ -121,7 +121,7 @@ final class BuildListViewModel {
             cursor = page.cursor
             hasMore = page.cursor != nil
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoadingMore = false
@@ -171,7 +171,7 @@ final class BuildListViewModel {
             await loadJobs()
             return result.submit.id
         } catch {
-            self.error = "Couldn’t submit the build. \(error.localizedDescription)"
+            self.error = "Couldn’t submit the build. \(error.userFacingMessage)"
             return nil
         }
     }
diff --git a/Hutch/Views/Home/HomeViewModel.swift b/Hutch/Views/Home/HomeViewModel.swift
index 31dec1b..4f3a514 100644
--- a/Hutch/Views/Home/HomeViewModel.swift
+++ b/Hutch/Views/Home/HomeViewModel.swift
@@ -285,8 +285,8 @@ final class HomeViewModel {
         case .failure(let error):
             self.recentBuilds = []
             self.failedBuilds = []
-            self.failedBuildsError = error.localizedDescription
-            self.recentBuildsError = error.localizedDescription
+            self.failedBuildsError = error.userFacingMessage
+            self.recentBuildsError = error.userFacingMessage
         }
         isLoadingFailedBuilds = false
         isLoadingRecentBuilds = false
@@ -299,7 +299,7 @@ final class HomeViewModel {
             self.assignedTicketsError = nil
         case .failure(let error):
             self.assignedTickets = []
-            self.assignedTicketsError = error.localizedDescription
+            self.assignedTicketsError = error.userFacingMessage
         }
         isLoadingAssignedTickets = false
 
diff --git a/Hutch/Views/Inbox/InboxView.swift b/Hutch/Views/Inbox/InboxView.swift
index 2b92738..74f78d5 100644
--- a/Hutch/Views/Inbox/InboxView.swift
+++ b/Hutch/Views/Inbox/InboxView.swift
@@ -1,7 +1,4 @@
 import SwiftUI
-import os
-
-private let inboxNavigationLogger = Logger(subsystem: "net.cleberg.Hutch", category: "InboxNavigation")
 
 struct InboxView: View {
     @Environment(AppState.self) private var appState
@@ -101,9 +98,6 @@ struct InboxView: View {
                     systemImage: "tray",
                     description: Text("This thread could not be restored.")
                 )
-                .onAppear {
-                    inboxNavigationLogger.error("Inbox navigation destination missing thread snapshot")
-                }
             }
         }
     }
@@ -123,9 +117,6 @@ struct InboxView: View {
     private func selectThread(_ thread: InboxThreadSummary) {
         cacheSelectedThread(thread)
         isShowingThreadDetail = true
-        inboxNavigationLogger.debug(
-            "Inbox navigation triggered: threadID=\(thread.id, privacy: .public) subject=\(thread.subject, privacy: .public)"
-        )
     }
 
     private func cacheSelectedThread(_ thread: InboxThreadSummary) {
@@ -140,9 +131,6 @@ struct InboxView: View {
 
     private func handleThreadDetailDisappear(for threadID: String) {
         let isActiveSelection = selectedThreadID == threadID
-        inboxNavigationLogger.debug(
-            "Inbox thread detail disappeared: threadID=\(threadID, privacy: .public) activeSelection=\(isActiveSelection, privacy: .public)"
-        )
         guard isActiveSelection else { return }
         clearSelection()
     }
@@ -154,9 +142,6 @@ struct InboxView: View {
     }
 
     private func clearSelection() {
-        if let selectedThreadID {
-            inboxNavigationLogger.debug("Inbox selection cleared: threadID=\(selectedThreadID, privacy: .public)")
-        }
         selectedThreadID = nil
         selectedThreadSnapshot = nil
         isShowingThreadDetail = false
diff --git a/Hutch/Views/Inbox/InboxViewModel.swift b/Hutch/Views/Inbox/InboxViewModel.swift
index e4a0664..00d47b5 100644
--- a/Hutch/Views/Inbox/InboxViewModel.swift
+++ b/Hutch/Views/Inbox/InboxViewModel.swift
@@ -136,7 +136,7 @@ final class InboxViewModel {
                 return lhs.lastActivityAt > rhs.lastActivityAt
             }
         } catch {
-            inboxListLogger.error("Inbox request failed: type=inbox error=\(error.localizedDescription, privacy: .public)")
+            inboxListLogger.error("Inbox request failed")
             self.error = "Failed to load inbox"
         }
     }
@@ -144,17 +144,11 @@ final class InboxViewModel {
     func markThreadRead(_ thread: InboxThreadSummary) {
         let viewedAt = max(Date(), thread.lastActivityAt)
         InboxReadStateStore.markViewed(viewedAt, for: thread.id)
-        inboxListLogger.debug(
-            "Inbox mark read: key=\(thread.id, privacy: .public) latestActivityAt=\(thread.lastActivityAt.ISO8601Format(), privacy: .public) storedLastViewedAt=\(viewedAt.ISO8601Format(), privacy: .public)"
-        )
         threads.removeAll { $0.id == thread.id }
     }
 
     func markThreadUnread(_ thread: InboxThreadSummary) {
         InboxReadStateStore.markUnread(for: thread.id)
-        inboxListLogger.debug(
-            "Inbox mark unread: key=\(thread.id, privacy: .public) latestActivityAt=\(thread.lastActivityAt.ISO8601Format(), privacy: .public) storedLastViewedAt=nil"
-        )
         updateThread(thread, isUnread: true)
     }
 
@@ -237,7 +231,7 @@ final class InboxViewModel {
             summaries.append(contentsOf: batchResult.0)
             failureMessages.append(contentsOf: batchResult.1)
             for failure in batchResult.1 {
-                inboxListLogger.error("Inbox request failed: type=listThreads \(failure, privacy: .public)")
+                inboxListLogger.error("Inbox thread list request failed: \(failure, privacy: .private)")
             }
             startIndex = endIndex
         }
@@ -259,14 +253,7 @@ final class InboxViewModel {
 
         return response.list.threads.results.prefix(listThreadFetchLimit).map { thread in
             let groupingKey = "\(mailingList.rid)#\(thread.subject.replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression).trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: #"^(?:(?:re|fwd?)\s*:\s*)+"#, with: "", options: [.regularExpression, .caseInsensitive]).lowercased())"
-            let lastViewedAt = InboxReadStateStore.lastViewedAt(for: groupingKey)
             let isUnread = InboxReadStateStore.isUnread(threadID: groupingKey, lastActivityAt: thread.updated)
-            inboxListLogger.debug(
-                "Inbox thread grouping candidate: listRID=\(mailingList.rid, privacy: .public) rootMessageID=\(thread.root.messageID, privacy: .public) rootEmailID=\(thread.root.id, privacy: .public) groupingKey=\(groupingKey, privacy: .public)"
-            )
-            inboxListLogger.debug(
-                "Inbox unread state: key=\(groupingKey, privacy: .public) latestActivityAt=\(thread.updated.ISO8601Format(), privacy: .public) lastViewedAt=\(lastViewedAt?.ISO8601Format() ?? "nil", privacy: .public) isUnread=\(isUnread, privacy: .public)"
-            )
             return InboxThreadSummary(
                 rootEmailID: thread.root.id,
                 rootMessageID: thread.root.messageID,
diff --git a/Hutch/Views/Inbox/ThreadDetailView.swift b/Hutch/Views/Inbox/ThreadDetailView.swift
index c5d26a5..e4c34e7 100644
--- a/Hutch/Views/Inbox/ThreadDetailView.swift
+++ b/Hutch/Views/Inbox/ThreadDetailView.swift
@@ -4,7 +4,6 @@ import SwiftUI
 import UIKit
 
 private let inboxReplyLogger = Logger(subsystem: "net.cleberg.Hutch", category: "InboxReply")
-private let inboxThreadNavigationLogger = Logger(subsystem: "net.cleberg.Hutch", category: "InboxThreadNavigation")
 
 struct ThreadDetailView: View {
     let thread: InboxThreadSummary
@@ -53,18 +52,12 @@ struct ThreadDetailView: View {
             isUnread = thread.isUnread
             await vm.loadThread()
         }
-        .onAppear {
-            inboxThreadNavigationLogger.debug("Inbox thread detail appeared: threadID=\(thread.id, privacy: .public)")
-        }
         .onChange(of: viewModel?.thread?.id) { _, threadID in
             guard threadID != nil, !hasMarkedCurrentThreadViewed, !suppressAutoMarkViewed else { return }
             hasMarkedCurrentThreadViewed = true
             isUnread = false
             onViewed()
         }
-        .onDisappear {
-            inboxThreadNavigationLogger.debug("Inbox thread detail view disappeared: threadID=\(thread.id, privacy: .public)")
-        }
         .sheet(item: Binding(
             get: { viewModel?.composeDraft },
             set: { _ in viewModel?.dismissReply() }
@@ -72,14 +65,13 @@ struct ThreadDetailView: View {
             MailComposeView(draft: draft) { result in
                 switch result {
                 case .failed(let message):
-                    inboxReplyLogger.error("Inbox reply failed for thread \(thread.debugIdentifierSummary, privacy: .public): \(message, privacy: .public)")
+                    inboxReplyLogger.error("Inbox reply failed")
                     viewModel?.error = message
                 case .cancelled:
-                    inboxReplyLogger.debug("Inbox reply cancelled for thread \(thread.debugIdentifierSummary, privacy: .public)")
+                    break
                 case .saved:
-                    inboxReplyLogger.debug("Inbox reply draft saved for thread \(thread.debugIdentifierSummary, privacy: .public)")
+                    break
                 case .sent:
-                    inboxReplyLogger.debug("Inbox reply handed off to Mail for thread \(thread.debugIdentifierSummary, privacy: .public)")
                     replySuccessMessage = "Reply handed off to Mail."
                     Task {
                         await viewModel?.loadThread()
diff --git a/Hutch/Views/Inbox/ThreadViewModel.swift b/Hutch/Views/Inbox/ThreadViewModel.swift
index c042fba..6b70a41 100644
--- a/Hutch/Views/Inbox/ThreadViewModel.swift
+++ b/Hutch/Views/Inbox/ThreadViewModel.swift
@@ -172,8 +172,6 @@ final class ThreadViewModel {
         partialWarning = nil
         defer { isLoading = false }
 
-        inboxLogger.debug("Opening inbox thread: \(self.summary.debugIdentifierSummary, privacy: .public)")
-
         do {
             let threadPayloads = try await fetchThreadPayloads()
 
@@ -207,9 +205,7 @@ final class ThreadViewModel {
                     }
                 } catch {
                     hadPartialReplyFailure = true
-                    inboxLogger.error(
-                        "Inbox thread descendants failed for \(self.summary.debugIdentifierSummary, privacy: .public): \(error.localizedDescription, privacy: .public)"
-                    )
+                    inboxLogger.error("Inbox thread descendants failed")
                 }
             }
 
@@ -241,9 +237,9 @@ final class ThreadViewModel {
             if thread == nil {
                 self.error = "Failed to load thread"
             } else {
-                self.error = error.localizedDescription
+                self.error = error.userFacingMessage
             }
-            inboxLogger.error("Inbox thread detail failed for \(self.summary.debugIdentifierSummary, privacy: .public): \(error.localizedDescription, privacy: .public)")
+            inboxLogger.error("Inbox thread detail failed")
         }
     }
 
@@ -275,17 +271,9 @@ final class ThreadViewModel {
 
     private func fetchThreadByMessageID(rootMessageID: String) async throws -> InboxThreadPayloadDetail? {
         let candidateMessageIDs = Self.messageIDCandidates(from: rootMessageID)
-        inboxLogger.debug(
-            "Inbox thread lookup IDs: subject=\(self.summary.subject, privacy: .public) rootEmailID=\(self.summary.rootEmailID, privacy: .public) rootMessageID=\(rootMessageID, privacy: .public) candidates=\(candidateMessageIDs.joined(separator: ", "), privacy: .public)"
-        )
-
         var lastLookupError: Error?
 
         for messageID in candidateMessageIDs {
-            inboxLogger.debug(
-                "Inbox thread detail lookup request: rid=\(self.summary.listRID, privacy: .public) messageID=\(messageID, privacy: .public)"
-            )
-
             do {
                 let response: InboxThreadLookupResponse = try await Self.executeGraphQLRequest(
                     client: client,
@@ -303,10 +291,6 @@ final class ThreadViewModel {
             } catch let error as SRHTError {
                 switch error {
                 case .graphQLErrors(let errors):
-                    let combinedMessage = errors.map(\.message).joined(separator: " | ")
-                    inboxLogger.error(
-                        "Inbox thread message lookup failed: rid=\(self.summary.listRID, privacy: .public) messageID=\(messageID, privacy: .public) errors=\(combinedMessage, privacy: .public)"
-                    )
                     if errors.allSatisfy({ $0.message.localizedCaseInsensitiveContains("no rows in result set") }) {
                         lastLookupError = error
                         continue
@@ -318,11 +302,7 @@ final class ThreadViewModel {
             }
         }
 
-        if let lastLookupError {
-            inboxLogger.debug(
-                "Inbox thread message lookup exhausted candidates for \(self.summary.debugIdentifierSummary, privacy: .public): \(lastLookupError.localizedDescription, privacy: .public)"
-            )
-        }
+        _ = lastLookupError
         return nil
     }
 
@@ -348,7 +328,7 @@ final class ThreadViewModel {
                 )
             } catch {
                 if Self.isRecoverableNoRows(error) {
-                    inboxLogger.error("Inbox thread page scan recoverable miss for \(self.summary.debugIdentifierSummary, privacy: .public): \(error.localizedDescription, privacy: .public)")
+                    inboxLogger.error("Inbox thread page scan missed a recoverable result")
                     return nil
                 }
                 throw error
@@ -358,11 +338,6 @@ final class ThreadViewModel {
                 return nil
             }
 
-            let candidates = threadPage.results.map { payload in
-                "subject=\(payload.subject ?? "<nil>") rootEmailID=\(payload.root?.id.map(String.init) ?? "<nil>") rootMessageID=\(payload.root?.messageID ?? "<nil>")"
-            }.joined(separator: " | ")
-            inboxLogger.debug("Inbox thread detail page candidates: \(candidates, privacy: .public)")
-
             if let matchedThread = threadPage.results.first(where: {
                 $0.root?.messageID == targetRootMessageID ||
                 $0.root?.id == summary.rootEmailID ||
@@ -428,9 +403,7 @@ final class ThreadViewModel {
                 )
             } catch {
                 if Self.isRecoverableNoRows(error) {
-                    inboxLogger.error(
-                        "Inbox descendant page recoverable miss: thread=\(self.summary.debugIdentifierSummary, privacy: .public) messageID=\(messageID, privacy: .public) error=\(error.localizedDescription, privacy: .public)"
-                    )
+                    inboxLogger.error("Inbox descendant page missed a recoverable result")
                     continue
                 }
                 throw error
@@ -449,9 +422,6 @@ final class ThreadViewModel {
             error = "This thread is not ready to reply to yet."
             return
         }
-        inboxLogger.debug(
-            "Preparing inbox reply: subject=\(thread.subject, privacy: .public) listRID=\(thread.listRID, privacy: .public) rootMessageID=\(thread.rootMessageID, privacy: .public) recipient=\(thread.replyRecipient, privacy: .public) senderIdentity=system-mail-account"
-        )
         composeDraft = MailComposeDraft(
             recipients: [thread.replyRecipient],
             ccRecipients: [],
@@ -732,11 +702,6 @@ final class ThreadViewModel {
         )
 
         let (data, _) = try await URLSession.shared.data(for: request)
-        #if DEBUG
-        let responseBody = String(data: data, encoding: .utf8) ?? "<non-utf8 response>"
-        inboxLogger.debug("Inbox thread raw GraphQL response: \(responseBody, privacy: .public)")
-        #endif
-
         let decoder = JSONDecoder()
         decoder.dateDecodingStrategy = .srhtFlexible
         let envelope = try decoder.decode(GraphQLResponse<T>.self, from: data)
diff --git a/Hutch/Views/Pastes/PasteDetailViewModel.swift b/Hutch/Views/Pastes/PasteDetailViewModel.swift
index 8c60edd..984acf6 100644
--- a/Hutch/Views/Pastes/PasteDetailViewModel.swift
+++ b/Hutch/Views/Pastes/PasteDetailViewModel.swift
@@ -47,7 +47,7 @@ final class PasteDetailViewModel {
             }
             await loadSelectedFileContentsIfNeeded()
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -77,7 +77,7 @@ final class PasteDetailViewModel {
             }
             return updatedPaste
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
             return nil
         }
     }
@@ -92,7 +92,7 @@ final class PasteDetailViewModel {
             _ = try await service.deletePaste(id: pasteID)
             return true
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
             return false
         }
     }
@@ -108,7 +108,7 @@ final class PasteDetailViewModel {
         do {
             fileContents[file.hash] = try await service.loadContents(from: url)
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 }
diff --git a/Hutch/Views/Pastes/PasteListViewModel.swift b/Hutch/Views/Pastes/PasteListViewModel.swift
index ee7d47a..cc2180d 100644
--- a/Hutch/Views/Pastes/PasteListViewModel.swift
+++ b/Hutch/Views/Pastes/PasteListViewModel.swift
@@ -41,7 +41,7 @@ final class PasteListViewModel {
             hasMore = page.cursor != nil
         } catch {
             if pastes.isEmpty {
-                self.error = error.localizedDescription
+                self.error = error.userFacingMessage
             }
         }
 
@@ -66,7 +66,7 @@ final class PasteListViewModel {
             cursor = page.cursor
             hasMore = page.cursor != nil
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -90,7 +90,7 @@ final class PasteListViewModel {
             upsertPaste(paste)
             return paste
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
             return nil
         }
     }
diff --git a/Hutch/Views/Repositories/CommitDetailViewModel.swift b/Hutch/Views/Repositories/CommitDetailViewModel.swift
index 5a8a23d..f0898f5 100644
--- a/Hutch/Views/Repositories/CommitDetailViewModel.swift
+++ b/Hutch/Views/Repositories/CommitDetailViewModel.swift
@@ -68,7 +68,7 @@ final class CommitDetailViewModel {
             let result = try await executeWithRetry()
             commit = result.repository?.revparse_single
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoading = false
diff --git a/Hutch/Views/Repositories/FileTreeViewModel.swift b/Hutch/Views/Repositories/FileTreeViewModel.swift
index a9c86b4..c6fecbe 100644
--- a/Hutch/Views/Repositories/FileTreeViewModel.swift
+++ b/Hutch/Views/Repositories/FileTreeViewModel.swift
@@ -294,7 +294,7 @@ final class FileTreeViewModel {
                 entries = []
             }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -372,7 +372,7 @@ final class FileTreeViewModel {
             }
             entries = allEntries
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -397,7 +397,7 @@ final class FileTreeViewModel {
             viewingEntry = entry
             viewingObject = result.repository?.object ?? .unknown
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -449,7 +449,7 @@ final class FileTreeViewModel {
                 }
                 entries = allEntries
             } catch {
-                self.error = error.localizedDescription
+                self.error = error.userFacingMessage
             }
         }
     }
diff --git a/Hutch/Views/Repositories/HgRepositoryDetailViewModel.swift b/Hutch/Views/Repositories/HgRepositoryDetailViewModel.swift
index 30ee3bb..5a0298c 100644
--- a/Hutch/Views/Repositories/HgRepositoryDetailViewModel.swift
+++ b/Hutch/Views/Repositories/HgRepositoryDetailViewModel.swift
@@ -344,7 +344,7 @@ final class HgRepositoryDetailViewModel {
 
             summaryLoaded = true
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -367,7 +367,7 @@ final class HgRepositoryDetailViewModel {
                 logCursor = nil
                 hasMoreLog = false
             } else {
-                self.error = error.localizedDescription
+                self.error = error.userFacingMessage
             }
         }
     }
@@ -389,7 +389,7 @@ final class HgRepositoryDetailViewModel {
             logCursor = page.cursor
             hasMoreLog = page.cursor != nil
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -470,7 +470,7 @@ final class HgRepositoryDetailViewModel {
                 await loadFiles(at: path)
             }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -515,7 +515,7 @@ final class HgRepositoryDetailViewModel {
                 pathStack = path.isEmpty ? [] : path.split(separator: "/").map(String.init)
                 files = []
             } else {
-                self.error = error.localizedDescription
+                self.error = error.userFacingMessage
             }
         }
     }
diff --git a/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift b/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift
index 3e431f8..6cb6e36 100644
--- a/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift
+++ b/Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift
@@ -164,7 +164,7 @@ final class HgRepositorySettingsViewModel {
                 editedNonPublishing = repository.nonPublishing ?? false
             }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -186,7 +186,7 @@ final class HgRepositorySettingsViewModel {
                 responseType: HgUpdateRepositoryResponse.self
             )
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -205,7 +205,7 @@ final class HgRepositorySettingsViewModel {
             )
             acls = result.repository?.accessControlList.results ?? []
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -239,7 +239,7 @@ final class HgRepositorySettingsViewModel {
             if message.localizedCaseInsensitiveContains("No such repository or user found") {
                 self.error = "That user is not available on hg.sr.ht yet. They need to create or activate an hg.sr.ht repository first."
             } else {
-                self.error = message
+                self.error = error.userFacingMessage
             }
         }
     }
@@ -263,7 +263,7 @@ final class HgRepositorySettingsViewModel {
             )
             acls.removeAll { $0.id == entry.id }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -281,7 +281,7 @@ final class HgRepositorySettingsViewModel {
             )
             didDelete = true
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 }
diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
index afc748d..e2eaf4d 100644
--- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
+++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
@@ -161,7 +161,7 @@ final class RepositoryDetailViewModel {
             commitCursor = page.cursor
             hasMoreCommits = page.cursor != nil
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoadingCommits = false
@@ -183,7 +183,7 @@ final class RepositoryDetailViewModel {
             commitCursor = page.cursor
             hasMoreCommits = page.cursor != nil
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoadingMoreCommits = false
@@ -243,7 +243,7 @@ final class RepositoryDetailViewModel {
             branches = allRefs.filter { $0.name.hasPrefix("refs/heads/") }
             tags = allRefs.filter { $0.name.hasPrefix("refs/tags/") }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoadingRefs = false
@@ -337,7 +337,7 @@ final class RepositoryDetailViewModel {
             readmePath = nil
             readmeLoaded = true
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -389,7 +389,7 @@ final class RepositoryDetailViewModel {
                 .filter { !$0.artifacts.results.isEmpty }
                 .map { ReferenceWithArtifacts(name: $0.name, artifacts: $0.artifacts.results) }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoadingArtifacts = false
diff --git a/Hutch/Views/Repositories/RepositoryListView.swift b/Hutch/Views/Repositories/RepositoryListView.swift
index 7cac8a8..90876e7 100644
--- a/Hutch/Views/Repositories/RepositoryListView.swift
+++ b/Hutch/Views/Repositories/RepositoryListView.swift
@@ -24,6 +24,7 @@ struct RepositoryListView: View {
                     } label: {
                         Image(systemName: "plus")
                     }
+                    .accessibilityLabel("Create repository")
                 }
             }
         }
diff --git a/Hutch/Views/Repositories/RepositoryListViewModel.swift b/Hutch/Views/Repositories/RepositoryListViewModel.swift
index 5bdc4a5..fd5bafe 100644
--- a/Hutch/Views/Repositories/RepositoryListViewModel.swift
+++ b/Hutch/Views/Repositories/RepositoryListViewModel.swift
@@ -189,7 +189,7 @@ final class RepositoryListViewModel {
         } catch {
             // Only show error if we have no cached data to fall back on
             if repositories.isEmpty {
-                self.error = error.localizedDescription
+                self.error = error.userFacingMessage
             }
         }
 
@@ -272,20 +272,7 @@ final class RepositoryListViewModel {
     }
 
     private func repositoryCreationErrorMessage(for error: Error) -> String {
-        let message: String
-
-        if let srhtError = error as? SRHTError {
-            switch srhtError {
-            case .graphQLErrors(let errors):
-                message = errors.map(\.message).joined(separator: "\n")
-            default:
-                message = srhtError.localizedDescription
-            }
-        } else {
-            message = error.localizedDescription
-        }
-
-        return "Couldn’t create the repository. \(message)"
+        "Couldn’t create the repository. \(error.userFacingMessage)"
     }
 
     /// Fetch ALL repositories by paginating through all available pages.
diff --git a/Hutch/Views/Repositories/RepositorySettingsViewModel.swift b/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
index 71d6ab6..ca8e501 100644
--- a/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
+++ b/Hutch/Views/Repositories/RepositorySettingsViewModel.swift
@@ -173,7 +173,7 @@ final class RepositorySettingsViewModel {
                 responseType: UpdateRepoInfoResponse.self
             )
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -196,7 +196,7 @@ final class RepositorySettingsViewModel {
             )
             updatedName = result.updateRepository.name
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -255,7 +255,7 @@ final class RepositorySettingsViewModel {
             )
             acls = result.repository?.acls.results ?? []
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -286,7 +286,7 @@ final class RepositorySettingsViewModel {
             }
             newACLEntity = ""
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -331,7 +331,7 @@ final class RepositorySettingsViewModel {
             )
             acls.removeAll { $0.id == entry.id }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -357,7 +357,7 @@ final class RepositorySettingsViewModel {
             )
             didDelete = true
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 }
diff --git a/Hutch/Views/Settings/SettingsViewModel.swift b/Hutch/Views/Settings/SettingsViewModel.swift
index 1cc5426..29103f6 100644
--- a/Hutch/Views/Settings/SettingsViewModel.swift
+++ b/Hutch/Views/Settings/SettingsViewModel.swift
@@ -162,7 +162,7 @@ final class SettingsViewModel {
             sshKeys = result.me.sshKeys.results
             pgpKeys = result.me.pgpKeys.results
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoading = false
@@ -207,7 +207,7 @@ final class SettingsViewModel {
             }
             isEditingProfile = false
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isSavingProfile = false
@@ -252,7 +252,7 @@ final class SettingsViewModel {
                 )
             }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isUploadingAvatar = false
@@ -289,7 +289,7 @@ final class SettingsViewModel {
                 )
             }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isUploadingAvatar = false
@@ -313,7 +313,7 @@ final class SettingsViewModel {
             newSSHKey = ""
             isAddingSSHKey = false
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -329,7 +329,7 @@ final class SettingsViewModel {
             )
             sshKeys.removeAll { $0.id == key.id }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -351,7 +351,7 @@ final class SettingsViewModel {
             newPGPKey = ""
             isAddingPGPKey = false
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -367,7 +367,7 @@ final class SettingsViewModel {
             )
             pgpKeys.removeAll { $0.id == key.id }
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -385,7 +385,7 @@ final class SettingsViewModel {
             )
             personalAccessTokens = result.personalAccessTokens
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoadingPATs = false
diff --git a/Hutch/Views/Tickets/TicketDetailView.swift b/Hutch/Views/Tickets/TicketDetailView.swift
index 5ca8618..0631d7f 100644
--- a/Hutch/Views/Tickets/TicketDetailView.swift
+++ b/Hutch/Views/Tickets/TicketDetailView.swift
@@ -100,6 +100,7 @@ struct TicketDetailView: View {
         } label: {
             Image(systemName: "ellipsis.circle")
         }
+        .accessibilityLabel("Ticket actions")
         .sheet(isPresented: $showResolveSheet) {
             ResolveSheet(viewModel: viewModel, isPresented: $showResolveSheet)
                 .presentationDetents([.medium])
@@ -311,7 +312,7 @@ struct TicketDetailView: View {
                 } else {
                     MarkdownContentView(markdown: viewModel.commentText)
                         .frame(minHeight: 80, maxHeight: 200)
-                    .clipShape(RoundedRectangle(cornerRadius: 8))
+                        .clipShape(RoundedRectangle(cornerRadius: 8))
                 }
             }
 
@@ -359,11 +360,6 @@ private struct MarkdownContentView: View {
             }
         }
         .task(id: markdown) {
-            if renderedHTML != nil {
-                try? await Task.sleep(for: .milliseconds(150))
-                guard !Task.isCancelled else { return }
-            }
-
             let html = await Task.detached(priority: .userInitiated) {
                 markdownToHTML(markdown)
             }.value
diff --git a/Hutch/Views/Tickets/TicketDetailViewModel.swift b/Hutch/Views/Tickets/TicketDetailViewModel.swift
index 660715b..142c62b 100644
--- a/Hutch/Views/Tickets/TicketDetailViewModel.swift
+++ b/Hutch/Views/Tickets/TicketDetailViewModel.swift
@@ -324,7 +324,7 @@ final class TicketDetailViewModel {
             )
             events = payload.events.results.sorted(by: Self.timelineOrder)
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoading = false
@@ -359,7 +359,7 @@ final class TicketDetailViewModel {
             events.sort(by: Self.timelineOrder)
             commentText = ""
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isSubmitting = false
@@ -393,7 +393,7 @@ final class TicketDetailViewModel {
             // Re-fetch the ticket to get updated status/resolution
             await loadTicket()
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isPerformingAction = false
@@ -427,7 +427,7 @@ final class TicketDetailViewModel {
             // Reload to reflect the change
             await loadTicket()
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isPerformingAction = false
@@ -485,7 +485,7 @@ final class TicketDetailViewModel {
                 assignees: currentAssignees,
                 labels: currentTicket.labels
             )
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isPerformingAction = false
@@ -520,7 +520,7 @@ final class TicketDetailViewModel {
             // Reload to reflect the change
             await loadTicket()
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isPerformingAction = false
@@ -544,7 +544,7 @@ final class TicketDetailViewModel {
             )
             await loadTicket()
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isPerformingAction = false
@@ -568,7 +568,7 @@ final class TicketDetailViewModel {
             )
             await loadTicket()
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isPerformingAction = false
@@ -587,7 +587,7 @@ final class TicketDetailViewModel {
             )
             trackerLabels = result.user.tracker.labels.results
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
     }
 
@@ -610,7 +610,7 @@ final class TicketDetailViewModel {
             )
             trackerLabels.append(result.createLabel)
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isPerformingAction = false
diff --git a/Hutch/Views/Tickets/TicketListView.swift b/Hutch/Views/Tickets/TicketListView.swift
index 7e16276..ef0da23 100644
--- a/Hutch/Views/Tickets/TicketListView.swift
+++ b/Hutch/Views/Tickets/TicketListView.swift
@@ -33,6 +33,7 @@ struct TicketListView: View {
                     } label: {
                         Image(systemName: "plus")
                     }
+                    .accessibilityLabel("Create ticket")
                 }
             }
         }
diff --git a/Hutch/Views/Tickets/TicketListViewModel.swift b/Hutch/Views/Tickets/TicketListViewModel.swift
index 916037e..f3c9416 100644
--- a/Hutch/Views/Tickets/TicketListViewModel.swift
+++ b/Hutch/Views/Tickets/TicketListViewModel.swift
@@ -121,7 +121,7 @@ final class TicketListViewModel {
             cursor = page.cursor
             hasMore = page.cursor != nil
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoading = false
@@ -143,7 +143,7 @@ final class TicketListViewModel {
             cursor = page.cursor
             hasMore = page.cursor != nil
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoadingMore = false
@@ -185,7 +185,7 @@ final class TicketListViewModel {
             tickets.insert(ticket, at: 0)
             return ticket
         } catch {
-            self.error = "Couldn’t create the ticket. \(error.localizedDescription)"
+            self.error = "Couldn’t create the ticket. \(error.userFacingMessage)"
             return nil
         }
     }
diff --git a/Hutch/Views/Tickets/TrackerListViewModel.swift b/Hutch/Views/Tickets/TrackerListViewModel.swift
index 978a610..9c241ae 100644
--- a/Hutch/Views/Tickets/TrackerListViewModel.swift
+++ b/Hutch/Views/Tickets/TrackerListViewModel.swift
@@ -78,7 +78,7 @@ final class TrackerListViewModel {
             cursor = page.cursor
             hasMore = page.cursor != nil
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoading = false
@@ -100,7 +100,7 @@ final class TrackerListViewModel {
             cursor = page.cursor
             hasMore = page.cursor != nil
         } catch {
-            self.error = error.localizedDescription
+            self.error = error.userFacingMessage
         }
 
         isLoadingMore = false
@@ -165,19 +165,6 @@ final class TrackerListViewModel {
     }
 
     private func trackerCreationErrorMessage(for error: Error) -> String {
-        let message: String
-
-        if let srhtError = error as? SRHTError {
-            switch srhtError {
-            case .graphQLErrors(let errors):
-                message = errors.map(\.message).joined(separator: "\n")
-            default:
-                message = srhtError.localizedDescription
-            }
-        } else {
-            message = error.localizedDescription
-        }
-
-        return "Couldn’t create the tracker. \(message)"
+        "Couldn’t create the tracker. \(error.userFacingMessage)"
     }
 }