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
3struct HomeView: View { 3struct 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
8struct SettingsView: View { 8struct 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: {