From 4ad1b74c5887ddf0445fde29ffacfc6eb5d49b3f Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sun, 12 Apr 2026 19:30:39 -0500 Subject: feat: improve inbox reply flow and basic threading Implements: https://todo.sr.ht/~ccleberg/hutch/23 Implements: https://todo.sr.ht/~ccleberg/hutch/34 --- Hutch/Views/Inbox/ThreadDetailView.swift | 93 ++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/Hutch/Views/Inbox/ThreadDetailView.swift b/Hutch/Views/Inbox/ThreadDetailView.swift index cdb0104..bde8172 100644 --- a/Hutch/Views/Inbox/ThreadDetailView.swift +++ b/Hutch/Views/Inbox/ThreadDetailView.swift @@ -19,6 +19,7 @@ struct ThreadDetailView: View { @State private var suppressAutoMarkViewed = false @State private var isUnread: Bool @State private var isOpeningRepository = false + @State private var collapsedMessageIDs: Set = [] init( thread: InboxThreadSummary, @@ -51,7 +52,11 @@ struct ThreadDetailView: View { hasMarkedCurrentThreadViewed = false suppressAutoMarkViewed = false isUnread = thread.isUnread + collapsedMessageIDs = [] await vm.loadThread() + if let messages = vm.thread?.messages, messages.count > 1 { + collapsedMessageIDs = Set(messages.dropLast().map(\.id)) + } } .onChange(of: viewModel?.thread?.id) { _, threadID in guard threadID != nil, !hasMarkedCurrentThreadViewed, !suppressAutoMarkViewed else { return } @@ -164,7 +169,20 @@ struct ThreadDetailView: View { } ForEach(thread.messages) { message in - InboxMessageRow(message: message) + InboxMessageRow( + message: message, + isCollapsed: collapsedMessageIDs.contains(message.id), + canCollapse: thread.messages.count > 1, + onToggleCollapse: { + withAnimation(.easeInOut(duration: 0.2)) { + if collapsedMessageIDs.contains(message.id) { + collapsedMessageIDs.remove(message.id) + } else { + collapsedMessageIDs.insert(message.id) + } + } + } + ) } } } @@ -235,43 +253,60 @@ struct ThreadDetailView: View { private struct InboxMessageRow: View { let message: InboxMessage + var isCollapsed: Bool = false + var canCollapse: Bool = false + var onToggleCollapse: (() -> Void)? = nil var body: some View { - VStack(alignment: .leading, spacing: 10) { - HStack(alignment: .top, spacing: 12) { - VStack(alignment: .leading, spacing: 2) { - Text(senderLine) - .font(.subheadline.weight(.medium)) - .lineLimit(2) - Text(message.date.formatted(date: .abbreviated, time: .shortened)) - .font(.caption) - .foregroundStyle(.secondary) - } + VStack(alignment: .leading, spacing: isCollapsed ? 0 : 10) { + Button { + onToggleCollapse?() + } label: { + HStack(alignment: .top, spacing: 12) { + if canCollapse { + Image(systemName: isCollapsed ? "chevron.right" : "chevron.down") + .font(.caption) + .foregroundStyle(.tertiary) + .padding(.top, 4) + } - Spacer() + VStack(alignment: .leading, spacing: 2) { + Text(isCollapsed ? message.senderDisplayName : senderLine) + .font(.subheadline.weight(.medium)) + .lineLimit(isCollapsed ? 1 : 2) + Text(message.date.formatted(date: .abbreviated, time: .shortened)) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() - if message.isPatch { - Text("Patch") - .font(.caption2.weight(.medium)) - .foregroundStyle(.secondary) + if message.isPatch { + Text("Patch") + .font(.caption2.weight(.medium)) + .foregroundStyle(.secondary) + } } } - - ForEach(Array(message.contentBlocks.enumerated()), id: \.offset) { _, block in - switch block { - case .plainText(let text): - Text(text) - .font(.body) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) - case .diff(let diff): - DiffView(diff: diff) - .textSelection(.enabled) + .buttonStyle(.plain) + + if !isCollapsed { + ForEach(Array(message.contentBlocks.enumerated()), id: \.offset) { _, block in + switch block { + case .plainText(let text): + Text(text) + .font(.body) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + case .diff(let diff): + DiffView(diff: diff) + .textSelection(.enabled) + } } } } - .padding(.vertical, 6) + .padding(.vertical, isCollapsed ? 4 : 6) .listRowSeparator(.visible) } -- cgit v1.2.3