The phone reads the thread — comments arrive on the card's read view, posting and editing behind transactional sheets
The comment thread renders read-only under the card's body (author line, Markdown body through CardBodyView, read-only paperclip rows), read outside the snapshot and re-read on every walk landing via BoardSession.snapshotGeneration. Add Comment posts through the Mac composer's own draft-then-rename bracket — seeding from the card's single synced draft so a thought started on the Mac finishes here — and each row's context menu opens the same sheet in edit mode. Both commit on their trailing button or not at all: the phone's transactional model, dirty-Cancel confirmation and swipe-dismiss disabled while dirty included. UI-tested end to end with disk assertions. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The read screen's comment thread — the phone's rendering of `CommentThread`, oldest first,
|
||||
/// below the card's own content.
|
||||
///
|
||||
/// **The wording is the Mac's** (`CommentsHeader` / `CommentAuthorLine` in `CardCommentsLayout` —
|
||||
/// pure, but homed in `Kanban/UI`, which the phone deliberately does not compile, so the two lines
|
||||
/// are restated rather than imported): the header is "Comments · N" with the zero rendered as a
|
||||
/// count rather than an absence, and the author line is "author · timestamp · edited" with each
|
||||
/// absent segment dropped and the whole line dropped when there is neither a name nor a date to
|
||||
/// hang "edited" from. Absent means absent — no "Unattributed" placeholder, no avatar: the file
|
||||
/// deliberately carries no identity system, and the render must not invent one.
|
||||
///
|
||||
/// Read-only rows plus two doors: **Add Comment** under the thread opens the composer, and each
|
||||
/// row's context menu carries **Edit** — both sheets, both transactional, both the containing
|
||||
/// screen's to present. A posted comment's attachments render as read-only paperclip rows, the
|
||||
/// Mac's own rule for chips outside an authoring surface.
|
||||
struct CardCommentsSection: View {
|
||||
/// `nil` while the first read is in flight — the header and the Add button render, rows wait.
|
||||
let thread: CommentThread?
|
||||
let onAdd: () -> Void
|
||||
let onEdit: (Comment) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Divider()
|
||||
|
||||
Text("Comments · \(thread?.comments.count ?? 0)")
|
||||
.font(.subheadline.smallCaps().weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if let thread {
|
||||
ForEach(thread.comments) { comment in
|
||||
CommentRow(comment: comment)
|
||||
.contextMenu {
|
||||
Button {
|
||||
onEdit(comment)
|
||||
} label: {
|
||||
Label("Edit Comment", systemImage: "pencil")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: onAdd) {
|
||||
Label("Add Comment", systemImage: "plus.bubble")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One posted comment: author line, body through the same block renderer the card's own body
|
||||
/// uses (a comment's Markdown is the card-body subset, so the renderer is shared by construction),
|
||||
/// and the attachment listing.
|
||||
private struct CommentRow: View {
|
||||
let comment: Comment
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if let line = authorLine {
|
||||
Text(line)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
// Only a non-empty body reaches the renderer: `CardBodyView`'s empty case says
|
||||
// "No description", which is a card's word, not a comment's — an attachment-only
|
||||
// comment renders its chips alone.
|
||||
if !comment.body.isEmpty {
|
||||
CardBodyView(body: comment.body)
|
||||
}
|
||||
|
||||
ForEach(comment.attachments, id: \.self) { name in
|
||||
Label(name, systemImage: "paperclip")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
|
||||
/// `CommentAuthorLine.text(author:timestamp:isEdited:)`, restated. An empty `author` string is
|
||||
/// treated as absent for the Mac's reason: the Writer never writes `author: ""` (it omits the
|
||||
/// key), so one on disk is a hand edit, and a blank name beside a separator would be noise.
|
||||
private var authorLine: String? {
|
||||
var parts: [String] = []
|
||||
if let author = comment.author.value?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!author.isEmpty {
|
||||
parts.append(author)
|
||||
}
|
||||
if let created = comment.created.value {
|
||||
parts.append(created.formatted(date: .abbreviated, time: .shortened))
|
||||
}
|
||||
guard !parts.isEmpty else { return nil }
|
||||
if comment.isEdited {
|
||||
parts.append("edited")
|
||||
}
|
||||
return parts.joined(separator: " · ")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user