Commit fa8188260f
Unsigned
Layout: unified · split
Hutch/App/RootView.swift +16 −14
| @@ -146,12 +146,10 @@ struct RootView: View { | ||
| 146 | 146 | resolveRepositoryLink(owner: owner, repo: repo) |
| 147 | 147 | |
| 148 | 148 | case .build(let jobId): |
| 149 | // Reset the builds navigation and push the detail | |
| 150 | 149 | buildsPath = NavigationPath() |
| 151 | 150 | appState.selectedTab = .builds |
| 152 | // Defer the push slightly so the tab switch takes effect | |
| 153 | Task { @MainActor in | |
| 154 | try? await Task.sleep(for: .milliseconds(100)) | |
| 151 | Task { | |
| 152 | await settleNavigationTransition() | |
| 155 | 153 | buildsPath.append(jobId) |
| 156 | 154 | } |
| 157 | 155 | |
| @@ -165,26 +163,25 @@ struct RootView: View { | ||
| 165 | 163 | case .repository(let repository): |
| 166 | 164 | repoPath = NavigationPath() |
| 167 | 165 | appState.selectedTab = .repositories |
| 168 | Task { @MainActor in | |
| 169 | try? await Task.sleep(for: .milliseconds(100)) | |
| 166 | Task { | |
| 167 | await settleNavigationTransition() | |
| 170 | 168 | repoPath.append(repository) |
| 171 | 169 | } |
| 172 | 170 | |
| 173 | 171 | case .tracker(let tracker): |
| 174 | 172 | ticketsPath = NavigationPath() |
| 175 | 173 | appState.selectedTab = .tickets |
| 176 | Task { @MainActor in | |
| 177 | try? await Task.sleep(for: .milliseconds(100)) | |
| 174 | Task { | |
| 175 | await settleNavigationTransition() | |
| 178 | 176 | ticketsPath.append(tracker) |
| 179 | 177 | } |
| 180 | 178 | |
| 181 | 179 | case .mailingList(let mailingList): |
| 182 | 180 | morePath = NavigationPath() |
| 183 | 181 | appState.selectedTab = .more |
| 184 | Task { @MainActor in | |
| 185 | try? await Task.sleep(for: .milliseconds(100)) | |
| 182 | Task { | |
| 183 | await settleNavigationTransition() | |
| 186 | 184 | morePath.append(MoreRoute.lists) |
| 187 | try? await Task.sleep(for: .milliseconds(100)) | |
| 188 | 185 | morePath.append(MoreRoute.mailingList(mailingList)) |
| 189 | 186 | } |
| 190 | 187 | } |
| @@ -198,7 +195,7 @@ struct RootView: View { | ||
| 198 | 195 | let summary = try await appState.resolveRepository(owner: owner, name: repo) |
| 199 | 196 | repoPath = NavigationPath() |
| 200 | 197 | appState.selectedTab = .repositories |
| 201 | try? await Task.sleep(for: .milliseconds(100)) | |
| 198 | await settleNavigationTransition() | |
| 202 | 199 | repoPath.append(summary) |
| 203 | 200 | } catch { |
| 204 | 201 | // Silently fail — the repo may not exist or be inaccessible |
| @@ -214,9 +211,8 @@ struct RootView: View { | ||
| 214 | 211 | let trackerSummary = try await appState.resolveTracker(owner: owner, name: tracker) |
| 215 | 212 | ticketsPath = NavigationPath() |
| 216 | 213 | appState.selectedTab = .tickets |
| 217 | try? await Task.sleep(for: .milliseconds(100)) | |
| 214 | await settleNavigationTransition() | |
| 218 | 215 | ticketsPath.append(trackerSummary) |
| 219 | try? await Task.sleep(for: .milliseconds(100)) | |
| 220 | 216 | ticketsPath.append(TicketDeepLinkTarget( |
| 221 | 217 | ownerUsername: String(trackerSummary.owner.canonicalName.dropFirst()), |
| 222 | 218 | trackerName: trackerSummary.name, |
| @@ -229,6 +225,12 @@ struct RootView: View { | ||
| 229 | 225 | } |
| 230 | 226 | } |
| 231 | 227 | } |
| 228 | ||
| 229 | @MainActor | |
| 230 | private func settleNavigationTransition() async { | |
| 231 | await Task.yield() | |
| 232 | await Task.yield() | |
| 233 | } | |
| 232 | 234 | } |
| 233 | 235 | |
| 234 | 236 | enum MoreDestination: Hashable { |
Hutch/Networking/SRHTError.swift +72
| @@ -33,6 +33,55 @@ enum SRHTError: LocalizedError, Sendable { | ||
| 33 | 33 | } |
| 34 | 34 | } |
| 35 | 35 | |
| 36 | var userFacingMessage: String { | |
| 37 | switch self { | |
| 38 | case .graphQLErrors(let errors): | |
| 39 | let firstMessage = errors.first?.message.lowercased() ?? "" | |
| 40 | if firstMessage.contains("unauthorized") || firstMessage.contains("forbidden") { | |
| 41 | return "You do not have permission to do that." | |
| 42 | } | |
| 43 | if firstMessage.contains("not found") || firstMessage.contains("no rows in result set") { | |
| 44 | return "That content is no longer available." | |
| 45 | } | |
| 46 | return "Something went wrong. Please try again." | |
| 47 | case .httpError(let code): | |
| 48 | if code == 401 { | |
| 49 | return "Please sign in again." | |
| 50 | } | |
| 51 | if code == 403 { | |
| 52 | return "You do not have permission to do that." | |
| 53 | } | |
| 54 | if code == 404 { | |
| 55 | return "That content is no longer available." | |
| 56 | } | |
| 57 | if (500...599).contains(code) { | |
| 58 | return "The server is unavailable right now. Please try again." | |
| 59 | } | |
| 60 | return "Something went wrong. Please try again." | |
| 61 | case .invalidAuthenticatedURL: | |
| 62 | return "That request could not be completed." | |
| 63 | case .decodingError: | |
| 64 | return "The response could not be loaded right now." | |
| 65 | case .networkError(let error): | |
| 66 | let nsError = error as NSError | |
| 67 | switch nsError.code { | |
| 68 | case NSURLErrorNotConnectedToInternet, | |
| 69 | NSURLErrorNetworkConnectionLost, | |
| 70 | NSURLErrorTimedOut, | |
| 71 | NSURLErrorCannotFindHost, | |
| 72 | NSURLErrorCannotConnectToHost, | |
| 73 | NSURLErrorDNSLookupFailed, | |
| 74 | NSURLErrorInternationalRoamingOff, | |
| 75 | NSURLErrorDataNotAllowed: | |
| 76 | return "Check your connection and try again." | |
| 77 | default: | |
| 78 | return "The network request failed. Please try again." | |
| 79 | } | |
| 80 | case .unauthorized: | |
| 81 | return "Please sign in again." | |
| 82 | } | |
| 83 | } | |
| 84 | ||
| 36 | 85 | /// Whether this error represents a connectivity issue (no internet, timeout, DNS). |
| 37 | 86 | var isConnectivityError: Bool { |
| 38 | 87 | switch self { |
| @@ -55,6 +104,29 @@ enum SRHTError: LocalizedError, Sendable { | ||
| 55 | 104 | } |
| 56 | 105 | } |
| 57 | 106 | |
| 107 | extension Error { | |
| 108 | var userFacingMessage: String { | |
| 109 | if let error = self as? SRHTError { | |
| 110 | return error.userFacingMessage | |
| 111 | } | |
| 112 | ||
| 113 | let nsError = self as NSError | |
| 114 | switch nsError.code { | |
| 115 | case NSURLErrorNotConnectedToInternet, | |
| 116 | NSURLErrorNetworkConnectionLost, | |
| 117 | NSURLErrorTimedOut, | |
| 118 | NSURLErrorCannotFindHost, | |
| 119 | NSURLErrorCannotConnectToHost, | |
| 120 | NSURLErrorDNSLookupFailed, | |
| 121 | NSURLErrorInternationalRoamingOff, | |
| 122 | NSURLErrorDataNotAllowed: | |
| 123 | return "Check your connection and try again." | |
| 124 | default: | |
| 125 | return "Something went wrong. Please try again." | |
| 126 | } | |
| 127 | } | |
| 128 | } | |
| 129 | ||
| 58 | 130 | /// A single error entry from the GraphQL `errors` array. |
| 59 | 131 | struct GraphQLError: Decodable, Sendable { |
| 60 | 132 | let message: String |
Hutch/Views/Auth/AuthView.swift +1 −1
| @@ -87,7 +87,7 @@ struct TokenEntryView: View { | ||
| 87 | 87 | do { |
| 88 | 88 | try await appState.connect(with: tokenTrimmed) |
| 89 | 89 | } catch { |
| 90 | errorMessage = error.localizedDescription | |
| 90 | errorMessage = error.userFacingMessage | |
| 91 | 91 | } |
| 92 | 92 | isConnecting = false |
| 93 | 93 | } |
Hutch/Views/Builds/BuildDetailViewModel.swift +5 −5
| @@ -110,7 +110,7 @@ final class BuildDetailViewModel { | ||
| 110 | 110 | } |
| 111 | 111 | job = loadedJob |
| 112 | 112 | } catch { |
| 113 | self.error = error.localizedDescription | |
| 113 | self.error = error.userFacingMessage | |
| 114 | 114 | } |
| 115 | 115 | |
| 116 | 116 | isLoading = false |
| @@ -127,7 +127,7 @@ final class BuildDetailViewModel { | ||
| 127 | 127 | do { |
| 128 | 128 | taskLogs[cacheKey] = try await client.fetchText(url: logURL) |
| 129 | 129 | } catch { |
| 130 | self.error = error.localizedDescription | |
| 130 | self.error = error.userFacingMessage | |
| 131 | 131 | } |
| 132 | 132 | |
| 133 | 133 | loadingTaskLogs.remove(cacheKey) |
| @@ -148,7 +148,7 @@ final class BuildDetailViewModel { | ||
| 148 | 148 | // Reload job to get updated status. |
| 149 | 149 | await loadJob() |
| 150 | 150 | } catch { |
| 151 | self.error = error.localizedDescription | |
| 151 | self.error = error.userFacingMessage | |
| 152 | 152 | } |
| 153 | 153 | |
| 154 | 154 | isCancelling = false |
| @@ -185,7 +185,7 @@ final class BuildDetailViewModel { | ||
| 185 | 185 | ) |
| 186 | 186 | return result.submit.id |
| 187 | 187 | } catch { |
| 188 | self.error = error.localizedDescription | |
| 188 | self.error = error.userFacingMessage | |
| 189 | 189 | return nil |
| 190 | 190 | } |
| 191 | 191 | } |
| @@ -233,7 +233,7 @@ final class BuildDetailViewModel { | ||
| 233 | 233 | ) |
| 234 | 234 | return result.submit.id |
| 235 | 235 | } catch { |
| 236 | self.error = "Couldn’t submit the build. \(error.localizedDescription)" | |
| 236 | self.error = "Couldn’t submit the build. \(error.userFacingMessage)" | |
| 237 | 237 | return nil |
| 238 | 238 | } |
| 239 | 239 | } |
Hutch/Views/Builds/BuildListView.swift +1
| @@ -23,6 +23,7 @@ struct BuildListView: View { | ||
| 23 | 23 | } label: { |
| 24 | 24 | Image(systemName: "plus") |
| 25 | 25 | } |
| 26 | .accessibilityLabel("Submit build") | |
| 26 | 27 | } |
| 27 | 28 | } |
| 28 | 29 | } |
Hutch/Views/Builds/BuildListViewModel.swift +3 −3
| @@ -97,7 +97,7 @@ final class BuildListViewModel { | ||
| 97 | 97 | hasMore = page.cursor != nil |
| 98 | 98 | } catch { |
| 99 | 99 | if jobs.isEmpty { |
| 100 | self.error = error.localizedDescription | |
| 100 | self.error = error.userFacingMessage | |
| 101 | 101 | } |
| 102 | 102 | } |
| 103 | 103 | |
| @@ -121,7 +121,7 @@ final class BuildListViewModel { | ||
| 121 | 121 | cursor = page.cursor |
| 122 | 122 | hasMore = page.cursor != nil |
| 123 | 123 | } catch { |
| 124 | self.error = error.localizedDescription | |
| 124 | self.error = error.userFacingMessage | |
| 125 | 125 | } |
| 126 | 126 | |
| 127 | 127 | isLoadingMore = false |
| @@ -171,7 +171,7 @@ final class BuildListViewModel { | ||
| 171 | 171 | await loadJobs() |
| 172 | 172 | return result.submit.id |
| 173 | 173 | } catch { |
| 174 | self.error = "Couldn’t submit the build. \(error.localizedDescription)" | |
| 174 | self.error = "Couldn’t submit the build. \(error.userFacingMessage)" | |
| 175 | 175 | return nil |
| 176 | 176 | } |
| 177 | 177 | } |
Hutch/Views/Home/HomeViewModel.swift +3 −3
| @@ -285,8 +285,8 @@ final class HomeViewModel { | ||
| 285 | 285 | case .failure(let error): |
| 286 | 286 | self.recentBuilds = [] |
| 287 | 287 | self.failedBuilds = [] |
| 288 | self.failedBuildsError = error.localizedDescription | |
| 289 | self.recentBuildsError = error.localizedDescription | |
| 288 | self.failedBuildsError = error.userFacingMessage | |
| 289 | self.recentBuildsError = error.userFacingMessage | |
| 290 | 290 | } |
| 291 | 291 | isLoadingFailedBuilds = false |
| 292 | 292 | isLoadingRecentBuilds = false |
| @@ -299,7 +299,7 @@ final class HomeViewModel { | ||
| 299 | 299 | self.assignedTicketsError = nil |
| 300 | 300 | case .failure(let error): |
| 301 | 301 | self.assignedTickets = [] |
| 302 | self.assignedTicketsError = error.localizedDescription | |
| 302 | self.assignedTicketsError = error.userFacingMessage | |
| 303 | 303 | } |
| 304 | 304 | isLoadingAssignedTickets = false |
| 305 | 305 | |
Hutch/Views/Inbox/InboxView.swift −15
| @@ -1,7 +1,4 @@ | ||
| 1 | 1 | import SwiftUI |
| 2 | import os | |
| 3 | ||
| 4 | private let inboxNavigationLogger = Logger(subsystem: "net.cleberg.Hutch", category: "InboxNavigation") | |
| 5 | 2 | |
| 6 | 3 | struct InboxView: View { |
| 7 | 4 | @Environment(AppState.self) private var appState |
| @@ -101,9 +98,6 @@ struct InboxView: View { | ||
| 101 | 98 | systemImage: "tray", |
| 102 | 99 | description: Text("This thread could not be restored.") |
| 103 | 100 | ) |
| 104 | .onAppear { | |
| 105 | inboxNavigationLogger.error("Inbox navigation destination missing thread snapshot") | |
| 106 | } | |
| 107 | 101 | } |
| 108 | 102 | } |
| 109 | 103 | } |
| @@ -123,9 +117,6 @@ struct InboxView: View { | ||
| 123 | 117 | private func selectThread(_ thread: InboxThreadSummary) { |
| 124 | 118 | cacheSelectedThread(thread) |
| 125 | 119 | isShowingThreadDetail = true |
| 126 | inboxNavigationLogger.debug( | |
| 127 | "Inbox navigation triggered: threadID=\(thread.id, privacy: .public) subject=\(thread.subject, privacy: .public)" | |
| 128 | ) | |
| 129 | 120 | } |
| 130 | 121 | |
| 131 | 122 | private func cacheSelectedThread(_ thread: InboxThreadSummary) { |
| @@ -140,9 +131,6 @@ struct InboxView: View { | ||
| 140 | 131 | |
| 141 | 132 | private func handleThreadDetailDisappear(for threadID: String) { |
| 142 | 133 | let isActiveSelection = selectedThreadID == threadID |
| 143 | inboxNavigationLogger.debug( | |
| 144 | "Inbox thread detail disappeared: threadID=\(threadID, privacy: .public) activeSelection=\(isActiveSelection, privacy: .public)" | |
| 145 | ) | |
| 146 | 134 | guard isActiveSelection else { return } |
| 147 | 135 | clearSelection() |
| 148 | 136 | } |
| @@ -154,9 +142,6 @@ struct InboxView: View { | ||
| 154 | 142 | } |
| 155 | 143 | |
| 156 | 144 | private func clearSelection() { |
| 157 | if let selectedThreadID { | |
| 158 | inboxNavigationLogger.debug("Inbox selection cleared: threadID=\(selectedThreadID, privacy: .public)") | |
| 159 | } | |
| 160 | 145 | selectedThreadID = nil |
| 161 | 146 | selectedThreadSnapshot = nil |
| 162 | 147 | isShowingThreadDetail = false |
Hutch/Views/Inbox/InboxViewModel.swift +2 −15
| @@ -136,7 +136,7 @@ final class InboxViewModel { | ||
| 136 | 136 | return lhs.lastActivityAt > rhs.lastActivityAt |
| 137 | 137 | } |
| 138 | 138 | } catch { |
| 139 | inboxListLogger.error("Inbox request failed: type=inbox error=\(error.localizedDescription, privacy: .public)") | |
| 139 | inboxListLogger.error("Inbox request failed") | |
| 140 | 140 | self.error = "Failed to load inbox" |
| 141 | 141 | } |
| 142 | 142 | } |
| @@ -144,17 +144,11 @@ final class InboxViewModel { | ||
| 144 | 144 | func markThreadRead(_ thread: InboxThreadSummary) { |
| 145 | 145 | let viewedAt = max(Date(), thread.lastActivityAt) |
| 146 | 146 | InboxReadStateStore.markViewed(viewedAt, for: thread.id) |
| 147 | inboxListLogger.debug( | |
| 148 | "Inbox mark read: key=\(thread.id, privacy: .public) latestActivityAt=\(thread.lastActivityAt.ISO8601Format(), privacy: .public) storedLastViewedAt=\(viewedAt.ISO8601Format(), privacy: .public)" | |
| 149 | ) | |
| 150 | 147 | threads.removeAll { $0.id == thread.id } |
| 151 | 148 | } |
| 152 | 149 | |
| 153 | 150 | func markThreadUnread(_ thread: InboxThreadSummary) { |
| 154 | 151 | InboxReadStateStore.markUnread(for: thread.id) |
| 155 | inboxListLogger.debug( | |
| 156 | "Inbox mark unread: key=\(thread.id, privacy: .public) latestActivityAt=\(thread.lastActivityAt.ISO8601Format(), privacy: .public) storedLastViewedAt=nil" | |
| 157 | ) | |
| 158 | 152 | updateThread(thread, isUnread: true) |
| 159 | 153 | } |
| 160 | 154 | |
| @@ -237,7 +231,7 @@ final class InboxViewModel { | ||
| 237 | 231 | summaries.append(contentsOf: batchResult.0) |
| 238 | 232 | failureMessages.append(contentsOf: batchResult.1) |
| 239 | 233 | for failure in batchResult.1 { |
| 240 | inboxListLogger.error("Inbox request failed: type=listThreads \(failure, privacy: .public)") | |
| 234 | inboxListLogger.error("Inbox thread list request failed: \(failure, privacy: .private)") | |
| 241 | 235 | } |
| 242 | 236 | startIndex = endIndex |
| 243 | 237 | } |
| @@ -259,14 +253,7 @@ final class InboxViewModel { | ||
| 259 | 253 | |
| 260 | 254 | return response.list.threads.results.prefix(listThreadFetchLimit).map { thread in |
| 261 | 255 | 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())" |
| 262 | let lastViewedAt = InboxReadStateStore.lastViewedAt(for: groupingKey) | |
| 263 | 256 | let isUnread = InboxReadStateStore.isUnread(threadID: groupingKey, lastActivityAt: thread.updated) |
| 264 | inboxListLogger.debug( | |
| 265 | "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)" | |
| 266 | ) | |
| 267 | inboxListLogger.debug( | |
| 268 | "Inbox unread state: key=\(groupingKey, privacy: .public) latestActivityAt=\(thread.updated.ISO8601Format(), privacy: .public) lastViewedAt=\(lastViewedAt?.ISO8601Format() ?? "nil", privacy: .public) isUnread=\(isUnread, privacy: .public)" | |
| 269 | ) | |
| 270 | 257 | return InboxThreadSummary( |
| 271 | 258 | rootEmailID: thread.root.id, |
| 272 | 259 | rootMessageID: thread.root.messageID, |
Hutch/Views/Inbox/ThreadDetailView.swift +3 −11
| @@ -4,7 +4,6 @@ import SwiftUI | ||
| 4 | 4 | import UIKit |
| 5 | 5 | |
| 6 | 6 | private let inboxReplyLogger = Logger(subsystem: "net.cleberg.Hutch", category: "InboxReply") |
| 7 | private let inboxThreadNavigationLogger = Logger(subsystem: "net.cleberg.Hutch", category: "InboxThreadNavigation") | |
| 8 | 7 | |
| 9 | 8 | struct ThreadDetailView: View { |
| 10 | 9 | let thread: InboxThreadSummary |
| @@ -53,18 +52,12 @@ struct ThreadDetailView: View { | ||
| 53 | 52 | isUnread = thread.isUnread |
| 54 | 53 | await vm.loadThread() |
| 55 | 54 | } |
| 56 | .onAppear { | |
| 57 | inboxThreadNavigationLogger.debug("Inbox thread detail appeared: threadID=\(thread.id, privacy: .public)") | |
| 58 | } | |
| 59 | 55 | .onChange(of: viewModel?.thread?.id) { _, threadID in |
| 60 | 56 | guard threadID != nil, !hasMarkedCurrentThreadViewed, !suppressAutoMarkViewed else { return } |
| 61 | 57 | hasMarkedCurrentThreadViewed = true |
| 62 | 58 | isUnread = false |
| 63 | 59 | onViewed() |
| 64 | 60 | } |
| 65 | .onDisappear { | |
| 66 | inboxThreadNavigationLogger.debug("Inbox thread detail view disappeared: threadID=\(thread.id, privacy: .public)") | |
| 67 | } | |
| 68 | 61 | .sheet(item: Binding( |
| 69 | 62 | get: { viewModel?.composeDraft }, |
| 70 | 63 | set: { _ in viewModel?.dismissReply() } |
| @@ -72,14 +65,13 @@ struct ThreadDetailView: View { | ||
| 72 | 65 | MailComposeView(draft: draft) { result in |
| 73 | 66 | switch result { |
| 74 | 67 | case .failed(let message): |
| 75 | inboxReplyLogger.error("Inbox reply failed for thread \(thread.debugIdentifierSummary, privacy: .public): \(message, privacy: .public)") | |
| 68 | inboxReplyLogger.error("Inbox reply failed") | |
| 76 | 69 | viewModel?.error = message |
| 77 | 70 | case .cancelled: |
| 78 | inboxReplyLogger.debug("Inbox reply cancelled for thread \(thread.debugIdentifierSummary, privacy: .public)") | |
| 71 | break | |
| 79 | 72 | case .saved: |
| 80 | inboxReplyLogger.debug("Inbox reply draft saved for thread \(thread.debugIdentifierSummary, privacy: .public)") | |
| 73 | break | |
| 81 | 74 | case .sent: |
| 82 | inboxReplyLogger.debug("Inbox reply handed off to Mail for thread \(thread.debugIdentifierSummary, privacy: .public)") | |
| 83 | 75 | replySuccessMessage = "Reply handed off to Mail." |
| 84 | 76 | Task { |
| 85 | 77 | await viewModel?.loadThread() |
Hutch/Views/Inbox/ThreadViewModel.swift +6 −41
| @@ -172,8 +172,6 @@ final class ThreadViewModel { | ||
| 172 | 172 | partialWarning = nil |
| 173 | 173 | defer { isLoading = false } |
| 174 | 174 | |
| 175 | inboxLogger.debug("Opening inbox thread: \(self.summary.debugIdentifierSummary, privacy: .public)") | |
| 176 | ||
| 177 | 175 | do { |
| 178 | 176 | let threadPayloads = try await fetchThreadPayloads() |
| 179 | 177 | |
| @@ -207,9 +205,7 @@ final class ThreadViewModel { | ||
| 207 | 205 | } |
| 208 | 206 | } catch { |
| 209 | 207 | hadPartialReplyFailure = true |
| 210 | inboxLogger.error( | |
| 211 | "Inbox thread descendants failed for \(self.summary.debugIdentifierSummary, privacy: .public): \(error.localizedDescription, privacy: .public)" | |
| 212 | ) | |
| 208 | inboxLogger.error("Inbox thread descendants failed") | |
| 213 | 209 | } |
| 214 | 210 | } |
| 215 | 211 | |
| @@ -241,9 +237,9 @@ final class ThreadViewModel { | ||
| 241 | 237 | if thread == nil { |
| 242 | 238 | self.error = "Failed to load thread" |
| 243 | 239 | } else { |
| 244 | self.error = error.localizedDescription | |
| 240 | self.error = error.userFacingMessage | |
| 245 | 241 | } |
| 246 | inboxLogger.error("Inbox thread detail failed for \(self.summary.debugIdentifierSummary, privacy: .public): \(error.localizedDescription, privacy: .public)") | |
| 242 | inboxLogger.error("Inbox thread detail failed") | |
| 247 | 243 | } |
| 248 | 244 | } |
| 249 | 245 | |
| @@ -275,17 +271,9 @@ final class ThreadViewModel { | ||
| 275 | 271 | |
| 276 | 272 | private func fetchThreadByMessageID(rootMessageID: String) async throws -> InboxThreadPayloadDetail? { |
| 277 | 273 | let candidateMessageIDs = Self.messageIDCandidates(from: rootMessageID) |
| 278 | inboxLogger.debug( | |
| 279 | "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)" | |
| 280 | ) | |
| 281 | ||
| 282 | 274 | var lastLookupError: Error? |
| 283 | 275 | |
| 284 | 276 | for messageID in candidateMessageIDs { |
| 285 | inboxLogger.debug( | |
| 286 | "Inbox thread detail lookup request: rid=\(self.summary.listRID, privacy: .public) messageID=\(messageID, privacy: .public)" | |
| 287 | ) | |
| 288 | ||
| 289 | 277 | do { |
| 290 | 278 | let response: InboxThreadLookupResponse = try await Self.executeGraphQLRequest( |
| 291 | 279 | client: client, |
| @@ -303,10 +291,6 @@ final class ThreadViewModel { | ||
| 303 | 291 | } catch let error as SRHTError { |
| 304 | 292 | switch error { |
| 305 | 293 | case .graphQLErrors(let errors): |
| 306 | let combinedMessage = errors.map(\.message).joined(separator: " | ") | |
| 307 | inboxLogger.error( | |
| 308 | "Inbox thread message lookup failed: rid=\(self.summary.listRID, privacy: .public) messageID=\(messageID, privacy: .public) errors=\(combinedMessage, privacy: .public)" | |
| 309 | ) | |
| 310 | 294 | if errors.allSatisfy({ $0.message.localizedCaseInsensitiveContains("no rows in result set") }) { |
| 311 | 295 | lastLookupError = error |
| 312 | 296 | continue |
| @@ -318,11 +302,7 @@ final class ThreadViewModel { | ||
| 318 | 302 | } |
| 319 | 303 | } |
| 320 | 304 | |
| 321 | if let lastLookupError { | |
| 322 | inboxLogger.debug( | |
| 323 | "Inbox thread message lookup exhausted candidates for \(self.summary.debugIdentifierSummary, privacy: .public): \(lastLookupError.localizedDescription, privacy: .public)" | |
| 324 | ) | |
| 325 | } | |
| 305 | _ = lastLookupError | |
| 326 | 306 | return nil |
| 327 | 307 | } |
| 328 | 308 | |
| @@ -348,7 +328,7 @@ final class ThreadViewModel { | ||
| 348 | 328 | ) |
| 349 | 329 | } catch { |
| 350 | 330 | if Self.isRecoverableNoRows(error) { |
| 351 | inboxLogger.error("Inbox thread page scan recoverable miss for \(self.summary.debugIdentifierSummary, privacy: .public): \(error.localizedDescription, privacy: .public)") | |
| 331 | inboxLogger.error("Inbox thread page scan missed a recoverable result") | |
| 352 | 332 | return nil |
| 353 | 333 | } |
| 354 | 334 | throw error |
| @@ -358,11 +338,6 @@ final class ThreadViewModel { | ||
| 358 | 338 | return nil |
| 359 | 339 | } |
| 360 | 340 | |
| 361 | let candidates = threadPage.results.map { payload in | |
| 362 | "subject=\(payload.subject ?? "<nil>") rootEmailID=\(payload.root?.id.map(String.init) ?? "<nil>") rootMessageID=\(payload.root?.messageID ?? "<nil>")" | |
| 363 | }.joined(separator: " | ") | |
| 364 | inboxLogger.debug("Inbox thread detail page candidates: \(candidates, privacy: .public)") | |
| 365 | ||
| 366 | 341 | if let matchedThread = threadPage.results.first(where: { |
| 367 | 342 | $0.root?.messageID == targetRootMessageID || |
| 368 | 343 | $0.root?.id == summary.rootEmailID || |
| @@ -428,9 +403,7 @@ final class ThreadViewModel { | ||
| 428 | 403 | ) |
| 429 | 404 | } catch { |
| 430 | 405 | if Self.isRecoverableNoRows(error) { |
| 431 | inboxLogger.error( | |
| 432 | "Inbox descendant page recoverable miss: thread=\(self.summary.debugIdentifierSummary, privacy: .public) messageID=\(messageID, privacy: .public) error=\(error.localizedDescription, privacy: .public)" | |
| 433 | ) | |
| 406 | inboxLogger.error("Inbox descendant page missed a recoverable result") | |
| 434 | 407 | continue |
| 435 | 408 | } |
| 436 | 409 | throw error |
| @@ -449,9 +422,6 @@ final class ThreadViewModel { | ||
| 449 | 422 | error = "This thread is not ready to reply to yet." |
| 450 | 423 | return |
| 451 | 424 | } |
| 452 | inboxLogger.debug( | |
| 453 | "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" | |
| 454 | ) | |
| 455 | 425 | composeDraft = MailComposeDraft( |
| 456 | 426 | recipients: [thread.replyRecipient], |
| 457 | 427 | ccRecipients: [], |
| @@ -732,11 +702,6 @@ final class ThreadViewModel { | ||
| 732 | 702 | ) |
| 733 | 703 | |
| 734 | 704 | let (data, _) = try await URLSession.shared.data(for: request) |
| 735 | #if DEBUG | |
| 736 | let responseBody = String(data: data, encoding: .utf8) ?? "<non-utf8 response>" | |
| 737 | inboxLogger.debug("Inbox thread raw GraphQL response: \(responseBody, privacy: .public)") | |
| 738 | #endif | |
| 739 | ||
| 740 | 705 | let decoder = JSONDecoder() |
| 741 | 706 | decoder.dateDecodingStrategy = .srhtFlexible |
| 742 | 707 | let envelope = try decoder.decode(GraphQLResponse<T>.self, from: data) |
Hutch/Views/Pastes/PasteDetailViewModel.swift +4 −4
| @@ -47,7 +47,7 @@ final class PasteDetailViewModel { | ||
| 47 | 47 | } |
| 48 | 48 | await loadSelectedFileContentsIfNeeded() |
| 49 | 49 | } catch { |
| 50 | self.error = error.localizedDescription | |
| 50 | self.error = error.userFacingMessage | |
| 51 | 51 | } |
| 52 | 52 | } |
| 53 | 53 | |
| @@ -77,7 +77,7 @@ final class PasteDetailViewModel { | ||
| 77 | 77 | } |
| 78 | 78 | return updatedPaste |
| 79 | 79 | } catch { |
| 80 | self.error = error.localizedDescription | |
| 80 | self.error = error.userFacingMessage | |
| 81 | 81 | return nil |
| 82 | 82 | } |
| 83 | 83 | } |
| @@ -92,7 +92,7 @@ final class PasteDetailViewModel { | ||
| 92 | 92 | _ = try await service.deletePaste(id: pasteID) |
| 93 | 93 | return true |
| 94 | 94 | } catch { |
| 95 | self.error = error.localizedDescription | |
| 95 | self.error = error.userFacingMessage | |
| 96 | 96 | return false |
| 97 | 97 | } |
| 98 | 98 | } |
| @@ -108,7 +108,7 @@ final class PasteDetailViewModel { | ||
| 108 | 108 | do { |
| 109 | 109 | fileContents[file.hash] = try await service.loadContents(from: url) |
| 110 | 110 | } catch { |
| 111 | self.error = error.localizedDescription | |
| 111 | self.error = error.userFacingMessage | |
| 112 | 112 | } |
| 113 | 113 | } |
| 114 | 114 | } |
Hutch/Views/Pastes/PasteListViewModel.swift +3 −3
| @@ -41,7 +41,7 @@ final class PasteListViewModel { | ||
| 41 | 41 | hasMore = page.cursor != nil |
| 42 | 42 | } catch { |
| 43 | 43 | if pastes.isEmpty { |
| 44 | self.error = error.localizedDescription | |
| 44 | self.error = error.userFacingMessage | |
| 45 | 45 | } |
| 46 | 46 | } |
| 47 | 47 | |
| @@ -66,7 +66,7 @@ final class PasteListViewModel { | ||
| 66 | 66 | cursor = page.cursor |
| 67 | 67 | hasMore = page.cursor != nil |
| 68 | 68 | } catch { |
| 69 | self.error = error.localizedDescription | |
| 69 | self.error = error.userFacingMessage | |
| 70 | 70 | } |
| 71 | 71 | } |
| 72 | 72 | |
| @@ -90,7 +90,7 @@ final class PasteListViewModel { | ||
| 90 | 90 | upsertPaste(paste) |
| 91 | 91 | return paste |
| 92 | 92 | } catch { |
| 93 | self.error = error.localizedDescription | |
| 93 | self.error = error.userFacingMessage | |
| 94 | 94 | return nil |
| 95 | 95 | } |
| 96 | 96 | } |
Hutch/Views/Repositories/CommitDetailViewModel.swift +1 −1
| @@ -68,7 +68,7 @@ final class CommitDetailViewModel { | ||
| 68 | 68 | let result = try await executeWithRetry() |
| 69 | 69 | commit = result.repository?.revparse_single |
| 70 | 70 | } catch { |
| 71 | self.error = error.localizedDescription | |
| 71 | self.error = error.userFacingMessage | |
| 72 | 72 | } |
| 73 | 73 | |
| 74 | 74 | isLoading = false |
Hutch/Views/Repositories/FileTreeViewModel.swift +4 −4
| @@ -294,7 +294,7 @@ final class FileTreeViewModel { | ||
| 294 | 294 | entries = [] |
| 295 | 295 | } |
| 296 | 296 | } catch { |
| 297 | self.error = error.localizedDescription | |
| 297 | self.error = error.userFacingMessage | |
| 298 | 298 | } |
| 299 | 299 | } |
| 300 | 300 | |
| @@ -372,7 +372,7 @@ final class FileTreeViewModel { | ||
| 372 | 372 | } |
| 373 | 373 | entries = allEntries |
| 374 | 374 | } catch { |
| 375 | self.error = error.localizedDescription | |
| 375 | self.error = error.userFacingMessage | |
| 376 | 376 | } |
| 377 | 377 | } |
| 378 | 378 | |
| @@ -397,7 +397,7 @@ final class FileTreeViewModel { | ||
| 397 | 397 | viewingEntry = entry |
| 398 | 398 | viewingObject = result.repository?.object ?? .unknown |
| 399 | 399 | } catch { |
| 400 | self.error = error.localizedDescription | |
| 400 | self.error = error.userFacingMessage | |
| 401 | 401 | } |
| 402 | 402 | } |
| 403 | 403 | |
| @@ -449,7 +449,7 @@ final class FileTreeViewModel { | ||
| 449 | 449 | } |
| 450 | 450 | entries = allEntries |
| 451 | 451 | } catch { |
| 452 | self.error = error.localizedDescription | |
| 452 | self.error = error.userFacingMessage | |
| 453 | 453 | } |
| 454 | 454 | } |
| 455 | 455 | } |
Hutch/Views/Repositories/HgRepositoryDetailViewModel.swift +5 −5
| @@ -344,7 +344,7 @@ final class HgRepositoryDetailViewModel { | ||
| 344 | 344 | |
| 345 | 345 | summaryLoaded = true |
| 346 | 346 | } catch { |
| 347 | self.error = error.localizedDescription | |
| 347 | self.error = error.userFacingMessage | |
| 348 | 348 | } |
| 349 | 349 | } |
| 350 | 350 | |
| @@ -367,7 +367,7 @@ final class HgRepositoryDetailViewModel { | ||
| 367 | 367 | logCursor = nil |
| 368 | 368 | hasMoreLog = false |
| 369 | 369 | } else { |
| 370 | self.error = error.localizedDescription | |
| 370 | self.error = error.userFacingMessage | |
| 371 | 371 | } |
| 372 | 372 | } |
| 373 | 373 | } |
| @@ -389,7 +389,7 @@ final class HgRepositoryDetailViewModel { | ||
| 389 | 389 | logCursor = page.cursor |
| 390 | 390 | hasMoreLog = page.cursor != nil |
| 391 | 391 | } catch { |
| 392 | self.error = error.localizedDescription | |
| 392 | self.error = error.userFacingMessage | |
| 393 | 393 | } |
| 394 | 394 | } |
| 395 | 395 | |
| @@ -470,7 +470,7 @@ final class HgRepositoryDetailViewModel { | ||
| 470 | 470 | await loadFiles(at: path) |
| 471 | 471 | } |
| 472 | 472 | } catch { |
| 473 | self.error = error.localizedDescription | |
| 473 | self.error = error.userFacingMessage | |
| 474 | 474 | } |
| 475 | 475 | } |
| 476 | 476 | |
| @@ -515,7 +515,7 @@ final class HgRepositoryDetailViewModel { | ||
| 515 | 515 | pathStack = path.isEmpty ? [] : path.split(separator: "/").map(String.init) |
| 516 | 516 | files = [] |
| 517 | 517 | } else { |
| 518 | self.error = error.localizedDescription | |
| 518 | self.error = error.userFacingMessage | |
| 519 | 519 | } |
| 520 | 520 | } |
| 521 | 521 | } |
Hutch/Views/Repositories/HgRepositorySettingsViewModel.swift +6 −6
| @@ -164,7 +164,7 @@ final class HgRepositorySettingsViewModel { | ||
| 164 | 164 | editedNonPublishing = repository.nonPublishing ?? false |
| 165 | 165 | } |
| 166 | 166 | } catch { |
| 167 | self.error = error.localizedDescription | |
| 167 | self.error = error.userFacingMessage | |
| 168 | 168 | } |
| 169 | 169 | } |
| 170 | 170 | |
| @@ -186,7 +186,7 @@ final class HgRepositorySettingsViewModel { | ||
| 186 | 186 | responseType: HgUpdateRepositoryResponse.self |
| 187 | 187 | ) |
| 188 | 188 | } catch { |
| 189 | self.error = error.localizedDescription | |
| 189 | self.error = error.userFacingMessage | |
| 190 | 190 | } |
| 191 | 191 | } |
| 192 | 192 | |
| @@ -205,7 +205,7 @@ final class HgRepositorySettingsViewModel { | ||
| 205 | 205 | ) |
| 206 | 206 | acls = result.repository?.accessControlList.results ?? [] |
| 207 | 207 | } catch { |
| 208 | self.error = error.localizedDescription | |
| 208 | self.error = error.userFacingMessage | |
| 209 | 209 | } |
| 210 | 210 | } |
| 211 | 211 | |
| @@ -239,7 +239,7 @@ final class HgRepositorySettingsViewModel { | ||
| 239 | 239 | if message.localizedCaseInsensitiveContains("No such repository or user found") { |
| 240 | 240 | self.error = "That user is not available on hg.sr.ht yet. They need to create or activate an hg.sr.ht repository first." |
| 241 | 241 | } else { |
| 242 | self.error = message | |
| 242 | self.error = error.userFacingMessage | |
| 243 | 243 | } |
| 244 | 244 | } |
| 245 | 245 | } |
| @@ -263,7 +263,7 @@ final class HgRepositorySettingsViewModel { | ||
| 263 | 263 | ) |
| 264 | 264 | acls.removeAll { $0.id == entry.id } |
| 265 | 265 | } catch { |
| 266 | self.error = error.localizedDescription | |
| 266 | self.error = error.userFacingMessage | |
| 267 | 267 | } |
| 268 | 268 | } |
| 269 | 269 | |
| @@ -281,7 +281,7 @@ final class HgRepositorySettingsViewModel { | ||
| 281 | 281 | ) |
| 282 | 282 | didDelete = true |
| 283 | 283 | } catch { |
| 284 | self.error = error.localizedDescription | |
| 284 | self.error = error.userFacingMessage | |
| 285 | 285 | } |
| 286 | 286 | } |
| 287 | 287 | } |
Hutch/Views/Repositories/RepositoryDetailViewModel.swift +5 −5
| @@ -161,7 +161,7 @@ final class RepositoryDetailViewModel { | ||
| 161 | 161 | commitCursor = page.cursor |
| 162 | 162 | hasMoreCommits = page.cursor != nil |
| 163 | 163 | } catch { |
| 164 | self.error = error.localizedDescription | |
| 164 | self.error = error.userFacingMessage | |
| 165 | 165 | } |
| 166 | 166 | |
| 167 | 167 | isLoadingCommits = false |
| @@ -183,7 +183,7 @@ final class RepositoryDetailViewModel { | ||
| 183 | 183 | commitCursor = page.cursor |
| 184 | 184 | hasMoreCommits = page.cursor != nil |
| 185 | 185 | } catch { |
| 186 | self.error = error.localizedDescription | |
| 186 | self.error = error.userFacingMessage | |
| 187 | 187 | } |
| 188 | 188 | |
| 189 | 189 | isLoadingMoreCommits = false |
| @@ -243,7 +243,7 @@ final class RepositoryDetailViewModel { | ||
| 243 | 243 | branches = allRefs.filter { $0.name.hasPrefix("refs/heads/") } |
| 244 | 244 | tags = allRefs.filter { $0.name.hasPrefix("refs/tags/") } |
| 245 | 245 | } catch { |
| 246 | self.error = error.localizedDescription | |
| 246 | self.error = error.userFacingMessage | |
| 247 | 247 | } |
| 248 | 248 | |
| 249 | 249 | isLoadingRefs = false |
| @@ -337,7 +337,7 @@ final class RepositoryDetailViewModel { | ||
| 337 | 337 | readmePath = nil |
| 338 | 338 | readmeLoaded = true |
| 339 | 339 | } catch { |
| 340 | self.error = error.localizedDescription | |
| 340 | self.error = error.userFacingMessage | |
| 341 | 341 | } |
| 342 | 342 | } |
| 343 | 343 | |
| @@ -389,7 +389,7 @@ final class RepositoryDetailViewModel { | ||
| 389 | 389 | .filter { !$0.artifacts.results.isEmpty } |
| 390 | 390 | .map { ReferenceWithArtifacts(name: $0.name, artifacts: $0.artifacts.results) } |
| 391 | 391 | } catch { |
| 392 | self.error = error.localizedDescription | |
| 392 | self.error = error.userFacingMessage | |
| 393 | 393 | } |
| 394 | 394 | |
| 395 | 395 | isLoadingArtifacts = false |
Hutch/Views/Repositories/RepositoryListView.swift +1
| @@ -24,6 +24,7 @@ struct RepositoryListView: View { | ||
| 24 | 24 | } label: { |
| 25 | 25 | Image(systemName: "plus") |
| 26 | 26 | } |
| 27 | .accessibilityLabel("Create repository") | |
| 27 | 28 | } |
| 28 | 29 | } |
| 29 | 30 | } |
Hutch/Views/Repositories/RepositoryListViewModel.swift +2 −15
| @@ -189,7 +189,7 @@ final class RepositoryListViewModel { | ||
| 189 | 189 | } catch { |
| 190 | 190 | // Only show error if we have no cached data to fall back on |
| 191 | 191 | if repositories.isEmpty { |
| 192 | self.error = error.localizedDescription | |
| 192 | self.error = error.userFacingMessage | |
| 193 | 193 | } |
| 194 | 194 | } |
| 195 | 195 | |
| @@ -272,20 +272,7 @@ final class RepositoryListViewModel { | ||
| 272 | 272 | } |
| 273 | 273 | |
| 274 | 274 | private func repositoryCreationErrorMessage(for error: Error) -> String { |
| 275 | let message: String | |
| 276 | ||
| 277 | if let srhtError = error as? SRHTError { | |
| 278 | switch srhtError { | |
| 279 | case .graphQLErrors(let errors): | |
| 280 | message = errors.map(\.message).joined(separator: "\n") | |
| 281 | default: | |
| 282 | message = srhtError.localizedDescription | |
| 283 | } | |
| 284 | } else { | |
| 285 | message = error.localizedDescription | |
| 286 | } | |
| 287 | ||
| 288 | return "Couldn’t create the repository. \(message)" | |
| 275 | "Couldn’t create the repository. \(error.userFacingMessage)" | |
| 289 | 276 | } |
| 290 | 277 | |
| 291 | 278 | /// Fetch ALL repositories by paginating through all available pages. |
Hutch/Views/Repositories/RepositorySettingsViewModel.swift +6 −6
| @@ -173,7 +173,7 @@ final class RepositorySettingsViewModel { | ||
| 173 | 173 | responseType: UpdateRepoInfoResponse.self |
| 174 | 174 | ) |
| 175 | 175 | } catch { |
| 176 | self.error = error.localizedDescription | |
| 176 | self.error = error.userFacingMessage | |
| 177 | 177 | } |
| 178 | 178 | } |
| 179 | 179 | |
| @@ -196,7 +196,7 @@ final class RepositorySettingsViewModel { | ||
| 196 | 196 | ) |
| 197 | 197 | updatedName = result.updateRepository.name |
| 198 | 198 | } catch { |
| 199 | self.error = error.localizedDescription | |
| 199 | self.error = error.userFacingMessage | |
| 200 | 200 | } |
| 201 | 201 | } |
| 202 | 202 | |
| @@ -255,7 +255,7 @@ final class RepositorySettingsViewModel { | ||
| 255 | 255 | ) |
| 256 | 256 | acls = result.repository?.acls.results ?? [] |
| 257 | 257 | } catch { |
| 258 | self.error = error.localizedDescription | |
| 258 | self.error = error.userFacingMessage | |
| 259 | 259 | } |
| 260 | 260 | } |
| 261 | 261 | |
| @@ -286,7 +286,7 @@ final class RepositorySettingsViewModel { | ||
| 286 | 286 | } |
| 287 | 287 | newACLEntity = "" |
| 288 | 288 | } catch { |
| 289 | self.error = error.localizedDescription | |
| 289 | self.error = error.userFacingMessage | |
| 290 | 290 | } |
| 291 | 291 | } |
| 292 | 292 | |
| @@ -331,7 +331,7 @@ final class RepositorySettingsViewModel { | ||
| 331 | 331 | ) |
| 332 | 332 | acls.removeAll { $0.id == entry.id } |
| 333 | 333 | } catch { |
| 334 | self.error = error.localizedDescription | |
| 334 | self.error = error.userFacingMessage | |
| 335 | 335 | } |
| 336 | 336 | } |
| 337 | 337 | |
| @@ -357,7 +357,7 @@ final class RepositorySettingsViewModel { | ||
| 357 | 357 | ) |
| 358 | 358 | didDelete = true |
| 359 | 359 | } catch { |
| 360 | self.error = error.localizedDescription | |
| 360 | self.error = error.userFacingMessage | |
| 361 | 361 | } |
| 362 | 362 | } |
| 363 | 363 | } |
Hutch/Views/Settings/SettingsViewModel.swift +9 −9
| @@ -162,7 +162,7 @@ final class SettingsViewModel { | ||
| 162 | 162 | sshKeys = result.me.sshKeys.results |
| 163 | 163 | pgpKeys = result.me.pgpKeys.results |
| 164 | 164 | } catch { |
| 165 | self.error = error.localizedDescription | |
| 165 | self.error = error.userFacingMessage | |
| 166 | 166 | } |
| 167 | 167 | |
| 168 | 168 | isLoading = false |
| @@ -207,7 +207,7 @@ final class SettingsViewModel { | ||
| 207 | 207 | } |
| 208 | 208 | isEditingProfile = false |
| 209 | 209 | } catch { |
| 210 | self.error = error.localizedDescription | |
| 210 | self.error = error.userFacingMessage | |
| 211 | 211 | } |
| 212 | 212 | |
| 213 | 213 | isSavingProfile = false |
| @@ -252,7 +252,7 @@ final class SettingsViewModel { | ||
| 252 | 252 | ) |
| 253 | 253 | } |
| 254 | 254 | } catch { |
| 255 | self.error = error.localizedDescription | |
| 255 | self.error = error.userFacingMessage | |
| 256 | 256 | } |
| 257 | 257 | |
| 258 | 258 | isUploadingAvatar = false |
| @@ -289,7 +289,7 @@ final class SettingsViewModel { | ||
| 289 | 289 | ) |
| 290 | 290 | } |
| 291 | 291 | } catch { |
| 292 | self.error = error.localizedDescription | |
| 292 | self.error = error.userFacingMessage | |
| 293 | 293 | } |
| 294 | 294 | |
| 295 | 295 | isUploadingAvatar = false |
| @@ -313,7 +313,7 @@ final class SettingsViewModel { | ||
| 313 | 313 | newSSHKey = "" |
| 314 | 314 | isAddingSSHKey = false |
| 315 | 315 | } catch { |
| 316 | self.error = error.localizedDescription | |
| 316 | self.error = error.userFacingMessage | |
| 317 | 317 | } |
| 318 | 318 | } |
| 319 | 319 | |
| @@ -329,7 +329,7 @@ final class SettingsViewModel { | ||
| 329 | 329 | ) |
| 330 | 330 | sshKeys.removeAll { $0.id == key.id } |
| 331 | 331 | } catch { |
| 332 | self.error = error.localizedDescription | |
| 332 | self.error = error.userFacingMessage | |
| 333 | 333 | } |
| 334 | 334 | } |
| 335 | 335 | |
| @@ -351,7 +351,7 @@ final class SettingsViewModel { | ||
| 351 | 351 | newPGPKey = "" |
| 352 | 352 | isAddingPGPKey = false |
| 353 | 353 | } catch { |
| 354 | self.error = error.localizedDescription | |
| 354 | self.error = error.userFacingMessage | |
| 355 | 355 | } |
| 356 | 356 | } |
| 357 | 357 | |
| @@ -367,7 +367,7 @@ final class SettingsViewModel { | ||
| 367 | 367 | ) |
| 368 | 368 | pgpKeys.removeAll { $0.id == key.id } |
| 369 | 369 | } catch { |
| 370 | self.error = error.localizedDescription | |
| 370 | self.error = error.userFacingMessage | |
| 371 | 371 | } |
| 372 | 372 | } |
| 373 | 373 | |
| @@ -385,7 +385,7 @@ final class SettingsViewModel { | ||
| 385 | 385 | ) |
| 386 | 386 | personalAccessTokens = result.personalAccessTokens |
| 387 | 387 | } catch { |
| 388 | self.error = error.localizedDescription | |
| 388 | self.error = error.userFacingMessage | |
| 389 | 389 | } |
| 390 | 390 | |
| 391 | 391 | isLoadingPATs = false |
Hutch/Views/Tickets/TicketDetailView.swift +2 −6
| @@ -100,6 +100,7 @@ struct TicketDetailView: View { | ||
| 100 | 100 | } label: { |
| 101 | 101 | Image(systemName: "ellipsis.circle") |
| 102 | 102 | } |
| 103 | .accessibilityLabel("Ticket actions") | |
| 103 | 104 | .sheet(isPresented: $showResolveSheet) { |
| 104 | 105 | ResolveSheet(viewModel: viewModel, isPresented: $showResolveSheet) |
| 105 | 106 | .presentationDetents([.medium]) |
| @@ -311,7 +312,7 @@ struct TicketDetailView: View { | ||
| 311 | 312 | } else { |
| 312 | 313 | MarkdownContentView(markdown: viewModel.commentText) |
| 313 | 314 | .frame(minHeight: 80, maxHeight: 200) |
| 314 | .clipShape(RoundedRectangle(cornerRadius: 8)) | |
| 315 | .clipShape(RoundedRectangle(cornerRadius: 8)) | |
| 315 | 316 | } |
| 316 | 317 | } |
| 317 | 318 | |
| @@ -359,11 +360,6 @@ private struct MarkdownContentView: View { | ||
| 359 | 360 | } |
| 360 | 361 | } |
| 361 | 362 | .task(id: markdown) { |
| 362 | if renderedHTML != nil { | |
| 363 | try? await Task.sleep(for: .milliseconds(150)) | |
| 364 | guard !Task.isCancelled else { return } | |
| 365 | } | |
| 366 | ||
| 367 | 363 | let html = await Task.detached(priority: .userInitiated) { |
| 368 | 364 | markdownToHTML(markdown) |
| 369 | 365 | }.value |
Hutch/Views/Tickets/TicketDetailViewModel.swift +10 −10
| @@ -324,7 +324,7 @@ final class TicketDetailViewModel { | ||
| 324 | 324 | ) |
| 325 | 325 | events = payload.events.results.sorted(by: Self.timelineOrder) |
| 326 | 326 | } catch { |
| 327 | self.error = error.localizedDescription | |
| 327 | self.error = error.userFacingMessage | |
| 328 | 328 | } |
| 329 | 329 | |
| 330 | 330 | isLoading = false |
| @@ -359,7 +359,7 @@ final class TicketDetailViewModel { | ||
| 359 | 359 | events.sort(by: Self.timelineOrder) |
| 360 | 360 | commentText = "" |
| 361 | 361 | } catch { |
| 362 | self.error = error.localizedDescription | |
| 362 | self.error = error.userFacingMessage | |
| 363 | 363 | } |
| 364 | 364 | |
| 365 | 365 | isSubmitting = false |
| @@ -393,7 +393,7 @@ final class TicketDetailViewModel { | ||
| 393 | 393 | // Re-fetch the ticket to get updated status/resolution |
| 394 | 394 | await loadTicket() |
| 395 | 395 | } catch { |
| 396 | self.error = error.localizedDescription | |
| 396 | self.error = error.userFacingMessage | |
| 397 | 397 | } |
| 398 | 398 | |
| 399 | 399 | isPerformingAction = false |
| @@ -427,7 +427,7 @@ final class TicketDetailViewModel { | ||
| 427 | 427 | // Reload to reflect the change |
| 428 | 428 | await loadTicket() |
| 429 | 429 | } catch { |
| 430 | self.error = error.localizedDescription | |
| 430 | self.error = error.userFacingMessage | |
| 431 | 431 | } |
| 432 | 432 | |
| 433 | 433 | isPerformingAction = false |
| @@ -485,7 +485,7 @@ final class TicketDetailViewModel { | ||
| 485 | 485 | assignees: currentAssignees, |
| 486 | 486 | labels: currentTicket.labels |
| 487 | 487 | ) |
| 488 | self.error = error.localizedDescription | |
| 488 | self.error = error.userFacingMessage | |
| 489 | 489 | } |
| 490 | 490 | |
| 491 | 491 | isPerformingAction = false |
| @@ -520,7 +520,7 @@ final class TicketDetailViewModel { | ||
| 520 | 520 | // Reload to reflect the change |
| 521 | 521 | await loadTicket() |
| 522 | 522 | } catch { |
| 523 | self.error = error.localizedDescription | |
| 523 | self.error = error.userFacingMessage | |
| 524 | 524 | } |
| 525 | 525 | |
| 526 | 526 | isPerformingAction = false |
| @@ -544,7 +544,7 @@ final class TicketDetailViewModel { | ||
| 544 | 544 | ) |
| 545 | 545 | await loadTicket() |
| 546 | 546 | } catch { |
| 547 | self.error = error.localizedDescription | |
| 547 | self.error = error.userFacingMessage | |
| 548 | 548 | } |
| 549 | 549 | |
| 550 | 550 | isPerformingAction = false |
| @@ -568,7 +568,7 @@ final class TicketDetailViewModel { | ||
| 568 | 568 | ) |
| 569 | 569 | await loadTicket() |
| 570 | 570 | } catch { |
| 571 | self.error = error.localizedDescription | |
| 571 | self.error = error.userFacingMessage | |
| 572 | 572 | } |
| 573 | 573 | |
| 574 | 574 | isPerformingAction = false |
| @@ -587,7 +587,7 @@ final class TicketDetailViewModel { | ||
| 587 | 587 | ) |
| 588 | 588 | trackerLabels = result.user.tracker.labels.results |
| 589 | 589 | } catch { |
| 590 | self.error = error.localizedDescription | |
| 590 | self.error = error.userFacingMessage | |
| 591 | 591 | } |
| 592 | 592 | } |
| 593 | 593 | |
| @@ -610,7 +610,7 @@ final class TicketDetailViewModel { | ||
| 610 | 610 | ) |
| 611 | 611 | trackerLabels.append(result.createLabel) |
| 612 | 612 | } catch { |
| 613 | self.error = error.localizedDescription | |
| 613 | self.error = error.userFacingMessage | |
| 614 | 614 | } |
| 615 | 615 | |
| 616 | 616 | isPerformingAction = false |
Hutch/Views/Tickets/TicketListView.swift +1
| @@ -33,6 +33,7 @@ struct TicketListView: View { | ||
| 33 | 33 | } label: { |
| 34 | 34 | Image(systemName: "plus") |
| 35 | 35 | } |
| 36 | .accessibilityLabel("Create ticket") | |
| 36 | 37 | } |
| 37 | 38 | } |
| 38 | 39 | } |
Hutch/Views/Tickets/TicketListViewModel.swift +3 −3
| @@ -121,7 +121,7 @@ final class TicketListViewModel { | ||
| 121 | 121 | cursor = page.cursor |
| 122 | 122 | hasMore = page.cursor != nil |
| 123 | 123 | } catch { |
| 124 | self.error = error.localizedDescription | |
| 124 | self.error = error.userFacingMessage | |
| 125 | 125 | } |
| 126 | 126 | |
| 127 | 127 | isLoading = false |
| @@ -143,7 +143,7 @@ final class TicketListViewModel { | ||
| 143 | 143 | cursor = page.cursor |
| 144 | 144 | hasMore = page.cursor != nil |
| 145 | 145 | } catch { |
| 146 | self.error = error.localizedDescription | |
| 146 | self.error = error.userFacingMessage | |
| 147 | 147 | } |
| 148 | 148 | |
| 149 | 149 | isLoadingMore = false |
| @@ -185,7 +185,7 @@ final class TicketListViewModel { | ||
| 185 | 185 | tickets.insert(ticket, at: 0) |
| 186 | 186 | return ticket |
| 187 | 187 | } catch { |
| 188 | self.error = "Couldn’t create the ticket. \(error.localizedDescription)" | |
| 188 | self.error = "Couldn’t create the ticket. \(error.userFacingMessage)" | |
| 189 | 189 | return nil |
| 190 | 190 | } |
| 191 | 191 | } |
Hutch/Views/Tickets/TrackerListViewModel.swift +3 −16
| @@ -78,7 +78,7 @@ final class TrackerListViewModel { | ||
| 78 | 78 | cursor = page.cursor |
| 79 | 79 | hasMore = page.cursor != nil |
| 80 | 80 | } catch { |
| 81 | self.error = error.localizedDescription | |
| 81 | self.error = error.userFacingMessage | |
| 82 | 82 | } |
| 83 | 83 | |
| 84 | 84 | isLoading = false |
| @@ -100,7 +100,7 @@ final class TrackerListViewModel { | ||
| 100 | 100 | cursor = page.cursor |
| 101 | 101 | hasMore = page.cursor != nil |
| 102 | 102 | } catch { |
| 103 | self.error = error.localizedDescription | |
| 103 | self.error = error.userFacingMessage | |
| 104 | 104 | } |
| 105 | 105 | |
| 106 | 106 | isLoadingMore = false |
| @@ -165,19 +165,6 @@ final class TrackerListViewModel { | ||
| 165 | 165 | } |
| 166 | 166 | |
| 167 | 167 | private func trackerCreationErrorMessage(for error: Error) -> String { |
| 168 | let message: String | |
| 169 | ||
| 170 | if let srhtError = error as? SRHTError { | |
| 171 | switch srhtError { | |
| 172 | case .graphQLErrors(let errors): | |
| 173 | message = errors.map(\.message).joined(separator: "\n") | |
| 174 | default: | |
| 175 | message = srhtError.localizedDescription | |
| 176 | } | |
| 177 | } else { | |
| 178 | message = error.localizedDescription | |
| 179 | } | |
| 180 | ||
| 181 | return "Couldn’t create the tracker. \(message)" | |
| 168 | "Couldn’t create the tracker. \(error.userFacingMessage)" | |
| 182 | 169 | } |
| 183 | 170 | } |