Commit 2dd2805e4a
2dd2805e4ae00226dd9cfb9ec9b7c916e88a6fca
parent: 57e588172b
Unsigned
cmc <hello@cleberg.net> · 2026-04-01 19:04 UTC
Fix several bugs ahead of 2.6.1
FileTreeView: when wrap is OFF, activate an explicit width constraint on
the code container derived from the widest line, giving the horizontal
scroll view a defined content size. Previously the constraint was
deactivated with nothing to replace it, making horizontal scrolling
unreliable. Also set horizontalScrollView.isScrollEnabled = !wrapLines
to prevent gesture conflicts when wrap is ON.
InboxView: remove the unnecessary loadThreads() network call fired after
markAllThreadsRead(). The threads array is already updated optimistically
and read state is persisted via InboxReadStateStore, so the refetch was
redundant and caused a brief loading flash.
HomeView: add scenePhase onChange handler to refresh dashboard data when
the app returns to the foreground, matching the existing pattern in
InboxView.
DiffView: wrap DiffBlockView content in a horizontal ScrollView so long
diff lines are readable. Switch inner VStack to LazyVStack for better
performance on large diffs.
SettingsView: replace direct UserDefaults access in behaviorSection with
@AppStorage, consistent with the rest of the app.
Layout: unified · split
Hutch/Views/Home/HomeView.swift
+7
| @@ -3,6 +3,7 @@ import SwiftUI |
| 3 | 3 | struct HomeView: View { |
| 4 | 4 | @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true |
| 5 | 5 | @Environment(AppState.self) private var appState |
| 6 | @Environment(\.scenePhase) private var scenePhase |
| 6 | 7 | @State private var viewModel: HomeViewModel? |
| 7 | 8 | private let previewLimit = 4 |
| 8 | 9 | private let projectPreviewLimit = 3 |
| @@ -39,6 +40,12 @@ struct HomeView: View { |
| 39 | 40 | |
| 40 | 41 | await vm.loadDashboard() |
| 41 | 42 | } |
| 43 | .onChange(of: scenePhase) { _, newPhase in |
| 44 | guard newPhase == .active, let viewModel else { return } |
| 45 | Task { |
| 46 | await viewModel.loadDashboard() |
| 47 | } |
| 48 | } |
| 42 | 49 | } |
| 43 | 50 | |
| 44 | 51 | @ViewBuilder |
Hutch/Views/Inbox/InboxView.swift
−3
| @@ -70,9 +70,6 @@ struct InboxView: View { |
| 70 | 70 | withAnimation(.easeInOut(duration: 0.2)) { |
| 71 | 71 | viewModel.markAllThreadsRead() |
| 72 | 72 | } |
| 73 | | Task { |
| 74 | | await viewModel.loadThreads() |
| 75 | | } |
| 76 | 73 | } |
| 77 | 74 | } |
| 78 | 75 | } |
Hutch/Views/Repositories/DiffView.swift
+6 −4
| @@ -76,13 +76,15 @@ private struct DiffBlockView: View { |
| 76 | 76 | let lines: [String] |
| 77 | 77 | |
| 78 | 78 | var body: some View { |
| 79 | | VStack(alignment: .leading, spacing: 0) { |
| 80 | | ForEach(Array(lines.enumerated()), id: \.offset) { _, line in |
| 81 | | DiffLineView(line: line) |
| 79 | ScrollView(.horizontal, showsIndicators: false) { |
| 80 | LazyVStack(alignment: .leading, spacing: 0) { |
| 81 | ForEach(Array(lines.enumerated()), id: \.offset) { _, line in |
| 82 | DiffLineView(line: line) |
| 83 | } |
| 82 | 84 | } |
| 85 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) |
| 83 | 86 | } |
| 84 | 87 | .font(.system(.caption, design: .monospaced)) |
| 85 | | .frame(maxWidth: .infinity, alignment: .leading) |
| 86 | 88 | .background(Color(.secondarySystemBackground)) |
| 87 | 89 | } |
| 88 | 90 | } |
Hutch/Views/Repositories/FileTreeView.swift
+17 −1
| @@ -441,6 +441,7 @@ private final class CodeFileUIView: UIView { |
| 441 | 441 | private let codeStackView = UIStackView() |
| 442 | 442 | |
| 443 | 443 | private var codeContainerWidthConstraint: NSLayoutConstraint? |
| 444 | private var codeContainerExplicitWidthConstraint: NSLayoutConstraint? |
| 444 | 445 | private var gutterWidthConstraint: NSLayoutConstraint? |
| 445 | 446 | |
| 446 | 447 | private var rows: [LineRow] = [] |
| @@ -524,6 +525,7 @@ private final class CodeFileUIView: UIView { |
| 524 | 525 | codeContainerView.addSubview(codeStackView) |
| 525 | 526 | |
| 526 | 527 | codeContainerWidthConstraint = codeContainerView.widthAnchor.constraint(equalTo: horizontalScrollView.frameLayoutGuide.widthAnchor) |
| 528 | codeContainerExplicitWidthConstraint = codeContainerView.widthAnchor.constraint(equalToConstant: 0) |
| 527 | 529 | gutterWidthConstraint = gutterContainerView.widthAnchor.constraint(equalToConstant: 0) |
| 528 | 530 | |
| 529 | 531 | NSLayoutConstraint.activate([ |
| @@ -627,12 +629,14 @@ private final class CodeFileUIView: UIView { |
| 627 | 629 | |
| 628 | 630 | private func updateWrapConfiguration(resetHorizontalOffset: Bool) { |
| 629 | 631 | horizontalScrollView.alwaysBounceHorizontal = !wrapLines |
| 630 | | horizontalScrollView.isScrollEnabled = true |
| 632 | horizontalScrollView.isScrollEnabled = !wrapLines |
| 631 | 633 | |
| 632 | 634 | if wrapLines { |
| 633 | 635 | codeContainerWidthConstraint?.isActive = true |
| 636 | codeContainerExplicitWidthConstraint?.isActive = false |
| 634 | 637 | } else { |
| 635 | 638 | codeContainerWidthConstraint?.isActive = false |
| 639 | codeContainerExplicitWidthConstraint?.isActive = true |
| 636 | 640 | } |
| 637 | 641 | |
| 638 | 642 | for row in rows { |
| @@ -664,6 +668,18 @@ private final class CodeFileUIView: UIView { |
| 664 | 668 | row.codeHeightConstraint.constant = rowHeight |
| 665 | 669 | } |
| 666 | 670 | |
| 671 | if wrapLines { |
| 672 | codeContainerExplicitWidthConstraint?.constant = 0 |
| 673 | } else { |
| 674 | let maxLineWidth = rows.reduce(CGFloat(0)) { partialResult, row in |
| 675 | let measuredWidth = row.codeLabel.sizeThatFits( |
| 676 | CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) |
| 677 | ).width |
| 678 | return max(partialResult, ceil(measuredWidth)) |
| 679 | } |
| 680 | codeContainerExplicitWidthConstraint?.constant = max(maxLineWidth, availableWidth) |
| 681 | } |
| 682 | |
| 667 | 683 | needsLineLayoutUpdate = false |
| 668 | 684 | } |
| 669 | 685 | |
Hutch/Views/Settings/SettingsView.swift
+2 −9
| @@ -8,6 +8,7 @@ private let settingsBioMarkdownOptions = AttributedString.MarkdownParsingOptions |
| 8 | 8 | struct SettingsView: View { |
| 9 | 9 | @Environment(AppState.self) private var appState |
| 10 | 10 | @Environment(\.colorScheme) private var colorScheme |
| 11 | @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true |
| 11 | 12 | @State private var viewModel: SettingsViewModel? |
| 12 | 13 | @State private var pendingDestructiveAction: SettingsDestructiveAction? |
| 13 | 14 | |
| @@ -389,15 +390,7 @@ struct SettingsView: View { |
| 389 | 390 | @ViewBuilder |
| 390 | 391 | private func behaviorSection() -> some View { |
| 391 | 392 | Section { |
| 392 | | Toggle( |
| 393 | | "Swipe actions", |
| 394 | | isOn: Binding( |
| 395 | | get: { |
| 396 | | UserDefaults.standard.object(forKey: AppStorageKeys.swipeActionsEnabled) as? Bool ?? true |
| 397 | | }, |
| 398 | | set: { UserDefaults.standard.set($0, forKey: AppStorageKeys.swipeActionsEnabled) } |
| 399 | | ) |
| 400 | | ) |
| 393 | Toggle("Swipe actions", isOn: $swipeActionsEnabled) |
| 401 | 394 | } header: { |
| 402 | 395 | Text("Behavior") |
| 403 | 396 | } footer: { |