commentthread.go
| 1 | package views |
| 2 | |
| 3 | import ( |
| 4 | "strconv" |
| 5 | |
| 6 | "hearthforge/internal/config" |
| 7 | "hearthforge/internal/db" |
| 8 | "hearthforge/internal/util" |
| 9 | |
| 10 | g "maragu.dev/gomponents" |
| 11 | . "maragu.dev/gomponents/html" |
| 12 | ) |
| 13 | |
| 14 | // ThreadComment is one comment of an issue or patch, with its rendered body. |
| 15 | type ThreadComment struct { |
| 16 | ID int64 |
| 17 | AuthorID *int64 |
| 18 | AuthorUsername string |
| 19 | AuthorAvatarVersion *int64 |
| 20 | Body string |
| 21 | BodyHTML string |
| 22 | CreatedAt string |
| 23 | EditedAt *string |
| 24 | } |
| 25 | |
| 26 | // CommentThread renders the comment list and the new comment form. |
| 27 | // parentStatus is the status of the issue or patch, "open" allows author edits. |
| 28 | func CommentThread(cfg *config.Config, user *db.SessionUser, comments []ThreadComment, |
| 29 | commentReactions map[int64][]ReactionCount, baseURL, parentStatus string, |
| 30 | ) g.Node { |
| 31 | reactURL := baseURL + "/react" |
| 32 | canEdit := func(c ThreadComment) bool { |
| 33 | if user == nil { |
| 34 | return false |
| 35 | } |
| 36 | return user.IsAdmin || (c.AuthorID != nil && user.ID == *c.AuthorID && parentStatus == "open") |
| 37 | } |
| 38 | return g.Group{ |
| 39 | g.Map(comments, func(comment ThreadComment) g.Node { |
| 40 | return Div(Class("timeline-item"), |
| 41 | Div(Class("timeline-author"), |
| 42 | Avatar(comment.AuthorID, comment.AuthorAvatarVersion, 24), |
| 43 | Strong(g.Text(util.DisplayName(comment.AuthorUsername, cfg.OwnerDisplayName))), |
| 44 | Div(Class("timeline-author-right"), |
| 45 | g.If(canEdit(comment), Details(Class("inline-edit-details"), |
| 46 | Summary(Class("btn btn-xs"), |
| 47 | Span(Class("when-closed"), g.Text("Edit")), |
| 48 | Span(Class("when-open"), g.Text("Stop editing")), |
| 49 | ), |
| 50 | )), |
| 51 | DateWithEdited(comment.CreatedAt, comment.EditedAt), |
| 52 | ), |
| 53 | ), |
| 54 | g.If(canEdit(comment), Div(Class("inline-edit-form-area"), |
| 55 | Form(Method("POST"), |
| 56 | Action(baseURL+"/comments/"+strconv.FormatInt(comment.ID, 10)+"/edit"), |
| 57 | Class("inline-edit-form"), |
| 58 | Div(Class("form-group"), |
| 59 | Textarea(Class("form-input"), Name("edit_body"), Rows("6"), |
| 60 | MaxLength(strconv.Itoa(cfg.MaxTextBodyBytes)), |
| 61 | g.Text(comment.Body)), |
| 62 | ), |
| 63 | Div(Class("form-actions"), |
| 64 | Button(Type("submit"), Class("btn btn-sm btn-primary"), g.Text("Save")), |
| 65 | ), |
| 66 | ), |
| 67 | )), |
| 68 | // The markdown renderer already sanitised this HTML. |
| 69 | Div(Class("timeline-body markdown-body"), g.Raw(comment.BodyHTML)), |
| 70 | ReactionBar(commentReactions[comment.ID], reactURL, comment.ID, user), |
| 71 | ) |
| 72 | }), |
| 73 | g.If(user != nil, Div(Class("timeline-item timeline-item-new"), |
| 74 | H3(Class("section-title"), g.Text("Add a comment")), |
| 75 | Form(Method("POST"), Action(baseURL+"/comments"), |
| 76 | Div(Class("form-group"), |
| 77 | Textarea(Name("body"), Rows("6"), MaxLength(strconv.Itoa(cfg.MaxTextBodyBytes)), |
| 78 | Placeholder("Leave a comment (Markdown supported)"), Required()), |
| 79 | ), |
| 80 | Div(Class("form-actions"), |
| 81 | Button(Type("submit"), Class("btn btn-primary"), g.Text("Comment")), |
| 82 | ), |
| 83 | ), |
| 84 | )), |
| 85 | g.If(user == nil, P(Class("text-muted"), |
| 86 | A(Href("/login"), g.Text("Sign in")), g.Text(" to leave a comment."))), |
| 87 | } |
| 88 | } |
| 89 |