diff options
| author | Christian Cleberg <[email protected]> | 2026-03-19 16:59:16 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-19 16:59:16 -0500 |
| commit | b82bbeeea48ad27832a355c2a41408559c411104 (patch) | |
| tree | 0af45101300c75970471573b09f878a1b3763b38 /Hutch | |
| parent | 9065ef6e92245a44390e336cc1ae515ae706cdd7 (diff) | |
| download | hutch-b82bbeeea48ad27832a355c2a41408559c411104.tar.gz hutch-b82bbeeea48ad27832a355c2a41408559c411104.tar.bz2 hutch-b82bbeeea48ad27832a355c2a41408559c411104.zip | |
v2.1: bundled polish and fixes
Diffstat (limited to 'Hutch')
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)" } } |
