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 | struct HomeView: View { |
3 | struct HomeView: View { |
| 4 | @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true |
4 | @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true |
| 5 | @Environment(AppState.self) private var appState |
5 | @Environment(AppState.self) private var appState |
| |
6 | @Environment(\.scenePhase) private var scenePhase |
| 6 | @State private var viewModel: HomeViewModel? |
7 | @State private var viewModel: HomeViewModel? |
| 7 | private let previewLimit = 4 |
8 | private let previewLimit = 4 |
| 8 | private let projectPreviewLimit = 3 |
9 | private let projectPreviewLimit = 3 |
| @@ -39,6 +40,12 @@ struct HomeView: View { |
| 39 | |
40 | |
| 40 | await vm.loadDashboard() |
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 | @ViewBuilder |
51 | @ViewBuilder |
Hutch/Views/Inbox/InboxView.swift
−3
| @@ -70,9 +70,6 @@ struct InboxView: View { |
| 70 | withAnimation(.easeInOut(duration: 0.2)) { |
70 | withAnimation(.easeInOut(duration: 0.2)) { |
| 71 | viewModel.markAllThreadsRead() |
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 | let lines: [String] |
76 | let lines: [String] |
| 77 | |
77 | |
| 78 | var body: some View { |
78 | var body: some View { |
| 79 | VStack(alignment: .leading, spacing: 0) { |
79 | ScrollView(.horizontal, showsIndicators: false) { |
| 80 | ForEach(Array(lines.enumerated()), id: \.offset) { _, line in |
80 | LazyVStack(alignment: .leading, spacing: 0) { |
| 81 | DiffLineView(line: line) |
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 | .font(.system(.caption, design: .monospaced)) |
87 | .font(.system(.caption, design: .monospaced)) |
| 85 | .frame(maxWidth: .infinity, alignment: .leading) |
| |
| 86 | .background(Color(.secondarySystemBackground)) |
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 | private let codeStackView = UIStackView() |
441 | private let codeStackView = UIStackView() |
| 442 | |
442 | |
| 443 | private var codeContainerWidthConstraint: NSLayoutConstraint? |
443 | private var codeContainerWidthConstraint: NSLayoutConstraint? |
| |
444 | private var codeContainerExplicitWidthConstraint: NSLayoutConstraint? |
| 444 | private var gutterWidthConstraint: NSLayoutConstraint? |
445 | private var gutterWidthConstraint: NSLayoutConstraint? |
| 445 | |
446 | |
| 446 | private var rows: [LineRow] = [] |
447 | private var rows: [LineRow] = [] |
| @@ -524,6 +525,7 @@ private final class CodeFileUIView: UIView { |
| 524 | codeContainerView.addSubview(codeStackView) |
525 | codeContainerView.addSubview(codeStackView) |
| 525 | |
526 | |
| 526 | codeContainerWidthConstraint = codeContainerView.widthAnchor.constraint(equalTo: horizontalScrollView.frameLayoutGuide.widthAnchor) |
527 | codeContainerWidthConstraint = codeContainerView.widthAnchor.constraint(equalTo: horizontalScrollView.frameLayoutGuide.widthAnchor) |
| |
528 | codeContainerExplicitWidthConstraint = codeContainerView.widthAnchor.constraint(equalToConstant: 0) |
| 527 | gutterWidthConstraint = gutterContainerView.widthAnchor.constraint(equalToConstant: 0) |
529 | gutterWidthConstraint = gutterContainerView.widthAnchor.constraint(equalToConstant: 0) |
| 528 | |
530 | |
| 529 | NSLayoutConstraint.activate([ |
531 | NSLayoutConstraint.activate([ |
| @@ -627,12 +629,14 @@ private final class CodeFileUIView: UIView { |
| 627 | |
629 | |
| 628 | private func updateWrapConfiguration(resetHorizontalOffset: Bool) { |
630 | private func updateWrapConfiguration(resetHorizontalOffset: Bool) { |
| 629 | horizontalScrollView.alwaysBounceHorizontal = !wrapLines |
631 | horizontalScrollView.alwaysBounceHorizontal = !wrapLines |
| 630 | horizontalScrollView.isScrollEnabled = true |
632 | horizontalScrollView.isScrollEnabled = !wrapLines |
| 631 | |
633 | |
| 632 | if wrapLines { |
634 | if wrapLines { |
| 633 | codeContainerWidthConstraint?.isActive = true |
635 | codeContainerWidthConstraint?.isActive = true |
| |
636 | codeContainerExplicitWidthConstraint?.isActive = false |
| 634 | } else { |
637 | } else { |
| 635 | codeContainerWidthConstraint?.isActive = false |
638 | codeContainerWidthConstraint?.isActive = false |
| |
639 | codeContainerExplicitWidthConstraint?.isActive = true |
| 636 | } |
640 | } |
| 637 | |
641 | |
| 638 | for row in rows { |
642 | for row in rows { |
| @@ -664,6 +668,18 @@ private final class CodeFileUIView: UIView { |
| 664 | row.codeHeightConstraint.constant = rowHeight |
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 | needsLineLayoutUpdate = false |
683 | needsLineLayoutUpdate = false |
| 668 | } |
684 | } |
| 669 | |
685 | |
Hutch/Views/Settings/SettingsView.swift
+2 −9
| @@ -8,6 +8,7 @@ private let settingsBioMarkdownOptions = AttributedString.MarkdownParsingOptions |
| 8 | struct SettingsView: View { |
8 | struct SettingsView: View { |
| 9 | @Environment(AppState.self) private var appState |
9 | @Environment(AppState.self) private var appState |
| 10 | @Environment(\.colorScheme) private var colorScheme |
10 | @Environment(\.colorScheme) private var colorScheme |
| |
11 | @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true |
| 11 | @State private var viewModel: SettingsViewModel? |
12 | @State private var viewModel: SettingsViewModel? |
| 12 | @State private var pendingDestructiveAction: SettingsDestructiveAction? |
13 | @State private var pendingDestructiveAction: SettingsDestructiveAction? |
| 13 | |
14 | |
| @@ -389,15 +390,7 @@ struct SettingsView: View { |
| 389 | @ViewBuilder |
390 | @ViewBuilder |
| 390 | private func behaviorSection() -> some View { |
391 | private func behaviorSection() -> some View { |
| 391 | Section { |
392 | Section { |
| 392 | Toggle( |
393 | Toggle("Swipe actions", isOn: $swipeActionsEnabled) |
| 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 | ) |
| |
| 401 | } header: { |
394 | } header: { |
| 402 | Text("Behavior") |
395 | Text("Behavior") |
| 403 | } footer: { |
396 | } footer: { |