Commit 7718ac2b7f
Unsigned
Layout: unified · split
Hutch/Views/Inbox/ThreadDetailView.swift +64 −29
| @@ -19,6 +19,7 @@ struct ThreadDetailView: View { | ||
| 19 | 19 | @State private var suppressAutoMarkViewed = false |
| 20 | 20 | @State private var isUnread: Bool |
| 21 | 21 | @State private var isOpeningRepository = false |
| 22 | @State private var collapsedMessageIDs: Set<Int> = [] | |
| 22 | 23 | |
| 23 | 24 | init( |
| 24 | 25 | thread: InboxThreadSummary, |
| @@ -51,7 +52,11 @@ struct ThreadDetailView: View { | ||
| 51 | 52 | hasMarkedCurrentThreadViewed = false |
| 52 | 53 | suppressAutoMarkViewed = false |
| 53 | 54 | isUnread = thread.isUnread |
| 55 | collapsedMessageIDs = [] | |
| 54 | 56 | await vm.loadThread() |
| 57 | if let messages = vm.thread?.messages, messages.count > 1 { | |
| 58 | collapsedMessageIDs = Set(messages.dropLast().map(\.id)) | |
| 59 | } | |
| 55 | 60 | } |
| 56 | 61 | .onChange(of: viewModel?.thread?.id) { _, threadID in |
| 57 | 62 | guard threadID != nil, !hasMarkedCurrentThreadViewed, !suppressAutoMarkViewed else { return } |
| @@ -164,7 +169,20 @@ struct ThreadDetailView: View { | ||
| 164 | 169 | } |
| 165 | 170 | |
| 166 | 171 | 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 | ) | |
| 168 | 186 | } |
| 169 | 187 | } |
| 170 | 188 | } |
| @@ -235,43 +253,60 @@ struct ThreadDetailView: View { | ||
| 235 | 253 | |
| 236 | 254 | private struct InboxMessageRow: View { |
| 237 | 255 | let message: InboxMessage |
| 256 | var isCollapsed: Bool = false | |
| 257 | var canCollapse: Bool = false | |
| 258 | var onToggleCollapse: (() -> Void)? = nil | |
| 238 | 259 | |
| 239 | 260 | 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 | } | |
| 250 | 272 | |
| 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() | |
| 252 | 283 | |
| 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 | } | |
| 257 | 289 | } |
| 258 | 290 | } |
| 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 | } | |
| 271 | 306 | } |
| 272 | 307 | } |
| 273 | 308 | } |
| 274 | .padding(.vertical, 6) | |
| 309 | .padding(.vertical, isCollapsed ? 4 : 6) | |
| 275 | 310 | .listRowSeparator(.visible) |
| 276 | 311 | } |
| 277 | 312 | |