import SwiftUI struct CommentEntry: Decodable, Identifiable { let ID: UInt64 let Author: String let Body: String var id: UInt64 { self.ID } } struct CommentThread: Decodable, Identifiable { let RowPos: Int let ColPos: Int let Ref: String let Entries: [CommentEntry] let Resolved: Bool var id: String { Ref } } struct CommentsPage: Decodable { let Threads: [CommentThread]? } struct SheetComments: View { @Bindable var model: SheetModel @State private var threads: [CommentThread] = [] @State private var bodyText = "" var body: some View { Group { if model.page?.MayComment == true { Text("Comment on " + model.selection.start.ref) TextField("Comment", text: $bodyText, axis: .vertical) Button("Add comment") { Task { await model.command( "/comments", [ "r": String(model.selection.start.r), "c": String(model.selection.start.c), "body": bodyText, ]) if model.lastNotice == "added" { bodyText = "" } await load() } }.disabled( bodyText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || model.dirty || model.busy) } ForEach(threads) { thread in Section(thread.Ref + (thread.Resolved ? " ยท Resolved" : "")) { ForEach(thread.Entries) { comment in VStack(alignment: .leading) { Text(comment.Author).font(.caption) Text(comment.Body).textSelection(.enabled) } } if !thread.Resolved && model.page?.MayComment == true { Button("Resolve thread") { Task { await model.command( "/comments/resolve", ["r": String(thread.RowPos), "c": String(thread.ColPos)]) await load() } } } } } }.task { await load() } } private func load() async { do { let response: CommentsPage = try await model.client.request(model.base + "/comments") threads = response.Threads ?? [] } catch { model.error = error.localizedDescription } } }