krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

209155dc431cf70d9c8dba20625bab7e3b1a5085

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-01T19:04:27Z

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.
 Hutch/Views/Home/HomeView.swift             |  7 +++++++
 Hutch/Views/Inbox/InboxView.swift           |  3 ---
 Hutch/Views/Repositories/DiffView.swift     | 10 ++++++----
 Hutch/Views/Repositories/FileTreeView.swift | 18 +++++++++++++++++-
 Hutch/Views/Settings/SettingsView.swift     | 11 ++---------
 5 files changed, 32 insertions(+), 17 deletions(-)

diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift
index 35e8f7c..f9d3de5 100644
--- a/Hutch/Views/Home/HomeView.swift
+++ b/Hutch/Views/Home/HomeView.swift
@@ -3,6 +3,7 @@ import SwiftUI
 struct HomeView: View {
     @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true
     @Environment(AppState.self) private var appState
+    @Environment(\.scenePhase) private var scenePhase
     @State private var viewModel: HomeViewModel?
     private let previewLimit = 4
     private let projectPreviewLimit = 3
@@ -39,6 +40,12 @@ struct HomeView: View {
 
             await vm.loadDashboard()
         }
+        .onChange(of: scenePhase) { _, newPhase in
+            guard newPhase == .active, let viewModel else { return }
+            Task {
+                await viewModel.loadDashboard()
+            }
+        }
     }
 
     @ViewBuilder
diff --git a/Hutch/Views/Inbox/InboxView.swift b/Hutch/Views/Inbox/InboxView.swift
index 6ca66ac..aa3765c 100644
--- a/Hutch/Views/Inbox/InboxView.swift
+++ b/Hutch/Views/Inbox/InboxView.swift
@@ -70,9 +70,6 @@ struct InboxView: View {
                         withAnimation(.easeInOut(duration: 0.2)) {
                             viewModel.markAllThreadsRead()
                         }
-                        Task {
-                            await viewModel.loadThreads()
-                        }
                     }
                 }
             }
diff --git a/Hutch/Views/Repositories/DiffView.swift b/Hutch/Views/Repositories/DiffView.swift
index 4b8e512..a370dfb 100644
--- a/Hutch/Views/Repositories/DiffView.swift
+++ b/Hutch/Views/Repositories/DiffView.swift
@@ -76,13 +76,15 @@ private struct DiffBlockView: View {
     let lines: [String]
 
     var body: some View {
-        VStack(alignment: .leading, spacing: 0) {
-            ForEach(Array(lines.enumerated()), id: \.offset) { _, line in
-                DiffLineView(line: line)
+        ScrollView(.horizontal, showsIndicators: false) {
+            LazyVStack(alignment: .leading, spacing: 0) {
+                ForEach(Array(lines.enumerated()), id: \.offset) { _, line in
+                    DiffLineView(line: line)
+                }
             }
+            .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
         }
         .font(.system(.caption, design: .monospaced))
-        .frame(maxWidth: .infinity, alignment: .leading)
         .background(Color(.secondarySystemBackground))
     }
 }
diff --git a/Hutch/Views/Repositories/FileTreeView.swift b/Hutch/Views/Repositories/FileTreeView.swift
index 8bedb73..adacb2e 100644
--- a/Hutch/Views/Repositories/FileTreeView.swift
+++ b/Hutch/Views/Repositories/FileTreeView.swift
@@ -441,6 +441,7 @@ private final class CodeFileUIView: UIView {
     private let codeStackView = UIStackView()
 
     private var codeContainerWidthConstraint: NSLayoutConstraint?
+    private var codeContainerExplicitWidthConstraint: NSLayoutConstraint?
     private var gutterWidthConstraint: NSLayoutConstraint?
 
     private var rows: [LineRow] = []
@@ -524,6 +525,7 @@ private final class CodeFileUIView: UIView {
         codeContainerView.addSubview(codeStackView)
 
         codeContainerWidthConstraint = codeContainerView.widthAnchor.constraint(equalTo: horizontalScrollView.frameLayoutGuide.widthAnchor)
+        codeContainerExplicitWidthConstraint = codeContainerView.widthAnchor.constraint(equalToConstant: 0)
         gutterWidthConstraint = gutterContainerView.widthAnchor.constraint(equalToConstant: 0)
 
         NSLayoutConstraint.activate([
@@ -627,12 +629,14 @@ private final class CodeFileUIView: UIView {
 
     private func updateWrapConfiguration(resetHorizontalOffset: Bool) {
         horizontalScrollView.alwaysBounceHorizontal = !wrapLines
-        horizontalScrollView.isScrollEnabled = true
+        horizontalScrollView.isScrollEnabled = !wrapLines
 
         if wrapLines {
             codeContainerWidthConstraint?.isActive = true
+            codeContainerExplicitWidthConstraint?.isActive = false
         } else {
             codeContainerWidthConstraint?.isActive = false
+            codeContainerExplicitWidthConstraint?.isActive = true
         }
 
         for row in rows {
@@ -664,6 +668,18 @@ private final class CodeFileUIView: UIView {
             row.codeHeightConstraint.constant = rowHeight
         }
 
+        if wrapLines {
+            codeContainerExplicitWidthConstraint?.constant = 0
+        } else {
+            let maxLineWidth = rows.reduce(CGFloat(0)) { partialResult, row in
+                let measuredWidth = row.codeLabel.sizeThatFits(
+                    CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
+                ).width
+                return max(partialResult, ceil(measuredWidth))
+            }
+            codeContainerExplicitWidthConstraint?.constant = max(maxLineWidth, availableWidth)
+        }
+
         needsLineLayoutUpdate = false
     }
 
diff --git a/Hutch/Views/Settings/SettingsView.swift b/Hutch/Views/Settings/SettingsView.swift
index e90f4e3..237c076 100644
--- a/Hutch/Views/Settings/SettingsView.swift
+++ b/Hutch/Views/Settings/SettingsView.swift
@@ -8,6 +8,7 @@ private let settingsBioMarkdownOptions = AttributedString.MarkdownParsingOptions
 struct SettingsView: View {
     @Environment(AppState.self) private var appState
     @Environment(\.colorScheme) private var colorScheme
+    @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true
     @State private var viewModel: SettingsViewModel?
     @State private var pendingDestructiveAction: SettingsDestructiveAction?
 
@@ -389,15 +390,7 @@ struct SettingsView: View {
     @ViewBuilder
     private func behaviorSection() -> some View {
         Section {
-            Toggle(
-                "Swipe actions",
-                isOn: Binding(
-                    get: {
-                        UserDefaults.standard.object(forKey: AppStorageKeys.swipeActionsEnabled) as? Bool ?? true
-                    },
-                    set: { UserDefaults.standard.set($0, forKey: AppStorageKeys.swipeActionsEnabled) }
-                )
-            )
+            Toggle("Swipe actions", isOn: $swipeActionsEnabled)
         } header: {
             Text("Behavior")
         } footer: {