Commit 7718ac2b7f

7718ac2b7ff58146e71dd065cd81cbb5beea512a

parent: 34e12c4d2c

Unsigned

cmc <hello@cleberg.net> · 2026-04-13 00:30 UTC

feat: improve inbox reply flow and basic threading

Implements: https://todo.sr.ht/~ccleberg/hutch/23
Implements: https://todo.sr.ht/~ccleberg/hutch/34

Layout: unified · split

Hutch/Views/Inbox/ThreadDetailView.swift +64 −29
@@ -19,6 +19,7 @@ struct ThreadDetailView: View {
1919 @State private var suppressAutoMarkViewed = false
2020 @State private var isUnread: Bool
2121 @State private var isOpeningRepository = false
22 @State private var collapsedMessageIDs: Set<Int> = []
2223
2324 init(
2425 thread: InboxThreadSummary,
@@ -51,7 +52,11 @@ struct ThreadDetailView: View {
5152 hasMarkedCurrentThreadViewed = false
5253 suppressAutoMarkViewed = false
5354 isUnread = thread.isUnread
55 collapsedMessageIDs = []
5456 await vm.loadThread()
57 if let messages = vm.thread?.messages, messages.count > 1 {
58 collapsedMessageIDs = Set(messages.dropLast().map(\.id))
59 }
5560 }
5661 .onChange(of: viewModel?.thread?.id) { _, threadID in
5762 guard threadID != nil, !hasMarkedCurrentThreadViewed, !suppressAutoMarkViewed else { return }
@@ -164,7 +169,20 @@ struct ThreadDetailView: View {
164169 }
165170
166171 ForEach(thread.messages) { message in
167 InboxMessageRow(message: message)
172 InboxMessageRow(
173 message: message,
174 isCollapsed: collapsedMessageIDs.contains(message.id),
175 canCollapse: thread.messages.count > 1,
176 onToggleCollapse: {
177 withAnimation(.easeInOut(duration: 0.2)) {
178 if collapsedMessageIDs.contains(message.id) {
179 collapsedMessageIDs.remove(message.id)
180 } else {
181 collapsedMessageIDs.insert(message.id)
182 }
183 }
184 }
185 )
168186 }
169187 }
170188 }
@@ -235,43 +253,60 @@ struct ThreadDetailView: View {
235253
236254private struct InboxMessageRow: View {
237255 let message: InboxMessage
256 var isCollapsed: Bool = false
257 var canCollapse: Bool = false
258 var onToggleCollapse: (() -> Void)? = nil
238259
239260 var body: some View {
240 VStack(alignment: .leading, spacing: 10) {
241 HStack(alignment: .top, spacing: 12) {
242 VStack(alignment: .leading, spacing: 2) {
243 Text(senderLine)
244 .font(.subheadline.weight(.medium))
245 .lineLimit(2)
246 Text(message.date.formatted(date: .abbreviated, time: .shortened))
247 .font(.caption)
248 .foregroundStyle(.secondary)
249 }
261 VStack(alignment: .leading, spacing: isCollapsed ? 0 : 10) {
262 Button {
263 onToggleCollapse?()
264 } label: {
265 HStack(alignment: .top, spacing: 12) {
266 if canCollapse {
267 Image(systemName: isCollapsed ? "chevron.right" : "chevron.down")
268 .font(.caption)
269 .foregroundStyle(.tertiary)
270 .padding(.top, 4)
271 }
250272
251 Spacer()
273 VStack(alignment: .leading, spacing: 2) {
274 Text(isCollapsed ? message.senderDisplayName : senderLine)
275 .font(.subheadline.weight(.medium))
276 .lineLimit(isCollapsed ? 1 : 2)
277 Text(message.date.formatted(date: .abbreviated, time: .shortened))
278 .font(.caption)
279 .foregroundStyle(.secondary)
280 }
281
282 Spacer()
252283
253 if message.isPatch {
254 Text("Patch")
255 .font(.caption2.weight(.medium))
256 .foregroundStyle(.secondary)
284 if message.isPatch {
285 Text("Patch")
286 .font(.caption2.weight(.medium))
287 .foregroundStyle(.secondary)
288 }
257289 }
258290 }
259
260 ForEach(Array(message.contentBlocks.enumerated()), id: \.offset) { _, block in
261 switch block {
262 case .plainText(let text):
263 Text(text)
264 .font(.body)
265 .textSelection(.enabled)
266 .frame(maxWidth: .infinity, alignment: .leading)
267 .fixedSize(horizontal: false, vertical: true)
268 case .diff(let diff):
269 DiffView(diff: diff)
270 .textSelection(.enabled)
291 .buttonStyle(.plain)
292
293 if !isCollapsed {
294 ForEach(Array(message.contentBlocks.enumerated()), id: \.offset) { _, block in
295 switch block {
296 case .plainText(let text):
297 Text(text)
298 .font(.body)
299 .textSelection(.enabled)
300 .frame(maxWidth: .infinity, alignment: .leading)
301 .fixedSize(horizontal: false, vertical: true)
302 case .diff(let diff):
303 DiffView(diff: diff)
304 .textSelection(.enabled)
305 }
271306 }
272307 }
273308 }
274 .padding(.vertical, 6)
309 .padding(.vertical, isCollapsed ? 4 : 6)
275310 .listRowSeparator(.visible)
276311 }
277312