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:
@@ -12,6 +12,13 @@ import SwiftUI
|
||||
/// the whole point of the transactional model: a screen that only ever renders `session.snapshot`
|
||||
/// cannot itself go stale relative to disk, and there is no auto-save timing to reason about
|
||||
/// because there is no save at all on this side of the Edit button.
|
||||
///
|
||||
/// **Comments are the one thing here the snapshot does not carry.** The thread lives outside the
|
||||
/// board walk (the Mac's arrangement, kept), so this screen reads it itself — re-read whenever
|
||||
/// `session.snapshotGeneration` moves, which covers both a comment sheet's own write (whose
|
||||
/// `perform` awaits the reload) and a foreign change landing. The read-only posture survives
|
||||
/// intact: the thread renders read-only, and the two comment writes live behind their own
|
||||
/// transactional sheets (`CommentEditorScreen`), exactly as card edits live behind Edit.
|
||||
struct CardDetailScreen: View {
|
||||
let boardRoot: URL
|
||||
let laneID: ItemID
|
||||
@@ -21,6 +28,9 @@ struct CardDetailScreen: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var isPresentingEdit = false
|
||||
@State private var thread: CommentThread?
|
||||
@State private var isComposingComment = false
|
||||
@State private var editingComment: Comment?
|
||||
|
||||
private var session: BoardSession { index.session(forBoardAt: boardRoot) }
|
||||
|
||||
@@ -40,9 +50,21 @@ struct CardDetailScreen: View {
|
||||
}
|
||||
}
|
||||
.task { session.open() }
|
||||
// The thread's read, keyed on the walk counter: runs at appearance (generation 0 or
|
||||
// whatever the session has reached), then again every time a walk lands — which is
|
||||
// every landing point a thread on this screen can have gone stale at.
|
||||
.task(id: session.snapshotGeneration) {
|
||||
thread = await session.loadCommentThread(laneID: laneID, cardID: cardID)
|
||||
}
|
||||
.fullScreenCover(isPresented: $isPresentingEdit) {
|
||||
CardEditScreen(boardRoot: boardRoot, laneID: laneID, cardID: cardID)
|
||||
}
|
||||
.sheet(isPresented: $isComposingComment) {
|
||||
CommentEditorScreen(boardRoot: boardRoot, laneID: laneID, cardID: cardID, mode: .compose)
|
||||
}
|
||||
.sheet(item: $editingComment) { comment in
|
||||
CommentEditorScreen(boardRoot: boardRoot, laneID: laneID, cardID: cardID, mode: .edit(comment))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -59,6 +81,11 @@ struct CardDetailScreen: View {
|
||||
titleBlock(for: card)
|
||||
bodyBlock(for: card)
|
||||
detailsFooter(for: card)
|
||||
CardCommentsSection(
|
||||
thread: thread,
|
||||
onAdd: { isComposingComment = true },
|
||||
onEdit: { editingComment = $0 }
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Reference in New Issue
Block a user