Commit 7718ac2b7f
Unsigned
Layout: unified · split
Hutch/Views/Inbox/ThreadDetailView.swift +64 −29
| @@ -19,6 +19,7 @@ struct ThreadDetailView: View { | |||
| 19 | @State private var suppressAutoMarkViewed = false | 19 | @State private var suppressAutoMarkViewed = false |
| 20 | @State private var isUnread: Bool | 20 | @State private var isUnread: Bool |
| 21 | @State private var isOpeningRepository = false | 21 | @State private var isOpeningRepository = false |
| 22 | @State private var collapsedMessageIDs: Set<Int> = [] | ||
| 22 | 23 | ||
| 23 | init( | 24 | init( |
| 24 | thread: InboxThreadSummary, | 25 | thread: InboxThreadSummary, |
| @@ -51,7 +52,11 @@ struct ThreadDetailView: View { | |||
| 51 | hasMarkedCurrentThreadViewed = false | 52 | hasMarkedCurrentThreadViewed = false |
| 52 | suppressAutoMarkViewed = false | 53 | suppressAutoMarkViewed = false |
| 53 | isUnread = thread.isUnread | 54 | isUnread = thread.isUnread |
| 55 | collapsedMessageIDs = [] | ||
| 54 | await vm.loadThread() | 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 | .onChange(of: viewModel?.thread?.id) { _, threadID in | 61 | .onChange(of: viewModel?.thread?.id) { _, threadID in |
| 57 | guard threadID != nil, !hasMarkedCurrentThreadViewed, !suppressAutoMarkViewed else { return } | 62 | guard threadID != nil, !hasMarkedCurrentThreadViewed, !suppressAutoMarkViewed else { return } |
| @@ -164,7 +169,20 @@ struct ThreadDetailView: View { | |||
| 164 | } | 169 | } |
| 165 | 170 | ||
| 166 | ForEach(thread.messages) { message in | 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 | private struct InboxMessageRow: View { | 254 | private struct InboxMessageRow: View { |
| 237 | let message: InboxMessage | 255 | let message: InboxMessage |
| 256 | var isCollapsed: Bool = false | ||
| 257 | var canCollapse: Bool = false | ||
| 258 | var onToggleCollapse: (() -> Void)? = nil | ||
| 238 | 259 | ||
| 239 | var body: some View { | 260 | var body: some View { |
| 240 | VStack(alignment: .leading, spacing: 10) { | 261 | VStack(alignment: .leading, spacing: isCollapsed ? 0 : 10) { |
| 241 | HStack(alignment: .top, spacing: 12) { | 262 | Button { |
| 242 | VStack(alignment: .leading, spacing: 2) { | 263 | onToggleCollapse?() |
| 243 | Text(senderLine) | 264 | } label: { |
| 244 | .font(.subheadline.weight(.medium)) | 265 | HStack(alignment: .top, spacing: 12) { |
| 245 | .lineLimit(2) | 266 | if canCollapse { |
| 246 | Text(message.date.formatted(date: .abbreviated, time: .shortened)) | 267 | Image(systemName: isCollapsed ? "chevron.right" : "chevron.down") |
| 247 | .font(.caption) | 268 | .font(.caption) |
| 248 | .foregroundStyle(.secondary) | 269 | .foregroundStyle(.tertiary) |
| 249 | } | 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 { | 284 | if message.isPatch { |
| 254 | Text("Patch") | 285 | Text("Patch") |
| 255 | .font(.caption2.weight(.medium)) | 286 | .font(.caption2.weight(.medium)) |
| 256 | .foregroundStyle(.secondary) | 287 | .foregroundStyle(.secondary) |
| 288 | } | ||
| 257 | } | 289 | } |
| 258 | } | 290 | } |
| 259 | 291 | .buttonStyle(.plain) | |
| 260 | ForEach(Array(message.contentBlocks.enumerated()), id: \.offset) { _, block in | 292 | |
| 261 | switch block { | 293 | if !isCollapsed { |
| 262 | case .plainText(let text): | 294 | ForEach(Array(message.contentBlocks.enumerated()), id: \.offset) { _, block in |
| 263 | Text(text) | 295 | switch block { |
| 264 | .font(.body) | 296 | case .plainText(let text): |
| 265 | .textSelection(.enabled) | 297 | Text(text) |
| 266 | .frame(maxWidth: .infinity, alignment: .leading) | 298 | .font(.body) |
| 267 | .fixedSize(horizontal: false, vertical: true) | 299 | .textSelection(.enabled) |
| 268 | case .diff(let diff): | 300 | .frame(maxWidth: .infinity, alignment: .leading) |
| 269 | DiffView(diff: diff) | 301 | .fixedSize(horizontal: false, vertical: true) |
| 270 | .textSelection(.enabled) | 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 | .listRowSeparator(.visible) | 310 | .listRowSeparator(.visible) |
| 276 | } | 311 | } |
| 277 | 312 | ||