summaryrefslogtreecommitdiff
path: root/Hutch
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-19 16:59:16 -0500
committerChristian Cleberg <[email protected]>2026-03-19 16:59:16 -0500
commitb82bbeeea48ad27832a355c2a41408559c411104 (patch)
tree0af45101300c75970471573b09f878a1b3763b38 /Hutch
parent9065ef6e92245a44390e336cc1ae515ae706cdd7 (diff)
downloadhutch-b82bbeeea48ad27832a355c2a41408559c411104.tar.gz
hutch-b82bbeeea48ad27832a355c2a41408559c411104.tar.bz2
hutch-b82bbeeea48ad27832a355c2a41408559c411104.zip
v2.1: bundled polish and fixes
Diffstat (limited to 'Hutch')
-rw-r--r--Hutch/App/RootView.swift30
-rw-r--r--Hutch/Networking/SRHTError.swift72
-rw-r--r--Hutch/Views/Auth/AuthView.swift2
-rw-r--r--Hutch/Views/Builds/BuildDetailViewModel.swift10
-rw-r--r--Hutch/Views/Builds/BuildListView.swift1
-rw-r--r--Hutch/Views/Builds/BuildListViewModel.swift6
-rw-r--r--Hutch/Views/Home/HomeViewModel.swift6
-rw-r--r--Hutch/Views/Inbox/InboxView.swift15
-rw-r--r--Hutch/Views/Inbox/InboxViewModel.swift17
-rw-r--r--Hutch/Views/Inbox/ThreadDetailView.swift14
-rw-r--r--Hutch/Views/Inbox/ThreadViewModel.swift47
-rw-r--r--Hutch/Views/Pastes/PasteDetailViewModel.swift8
-rw-r--r--Hutch/Views/Pastes/PasteListViewModel.swift6
-rw-r--r--Hutch/Views/Repositories/CommitDetailViewModel.swift2
-rw-r--r--Hutch/Views/Repositories/FileTreeViewModel.swift8
-rw-r--r--Hutch/Views/Repositories/HgRepositoryDetailViewModel.swift10
-rw-r--r--Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift12
-rw-r--r--Hutch/Views/Repositories/RepositoryDetailViewModel.swift10
-rw-r--r--Hutch/Views/Repositories/RepositoryListView.swift1
-rw-r--r--Hutch/Views/Repositories/RepositoryListViewModel.swift17
-rw-r--r--Hutch/Views/Repositories/RepositorySettingsViewModel.swift12
-rw-r--r--Hutch/Views/Settings/SettingsViewModel.swift18
-rw-r--r--Hutch/Views/Tickets/TicketDetailView.swift8
-rw-r--r--Hutch/Views/Tickets/TicketDetailViewModel.swift20
-rw-r--r--Hutch/Views/Tickets/TicketListView.swift1
-rw-r--r--Hutch/Views/Tickets/TicketListViewModel.swift6
-rw-r--r--Hutch/Views/Tickets/TrackerListViewModel.swift19
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)"
}
}