CommentThread.tsx
| 1 | import config from "../config.ts"; |
| 2 | import { displayName } from "../lib/users.ts"; |
| 3 | import type { SessionUser } from "../middleware/session.ts"; |
| 4 | import { Avatar } from "./Avatar.tsx"; |
| 5 | import { DateWithEdited } from "./DateWithEdited.tsx"; |
| 6 | import { ReactionBar, type ReactionCount } from "./ReactionBar.tsx"; |
| 7 | |
| 8 | export interface ThreadComment { |
| 9 | id: number; |
| 10 | author_id: number | null; |
| 11 | author_username: string; |
| 12 | author_avatar_version: number | null; |
| 13 | body: string; |
| 14 | bodyHtml: string; |
| 15 | created_at: string; |
| 16 | edited_at: string | null; |
| 17 | } |
| 18 | |
| 19 | interface CommentThreadProps { |
| 20 | user: SessionUser | null; |
| 21 | comments: ThreadComment[]; |
| 22 | commentReactions: Map<number, ReactionCount[]>; |
| 23 | baseUrl: string; |
| 24 | parentStatus: string; |
| 25 | } |
| 26 | |
| 27 | export function CommentThread({ |
| 28 | user, |
| 29 | comments, |
| 30 | commentReactions, |
| 31 | baseUrl, |
| 32 | parentStatus, |
| 33 | }: CommentThreadProps) { |
| 34 | const reactUrl = `${baseUrl}/react`; |
| 35 | const canEditComment = (c: ThreadComment) => |
| 36 | user != null && |
| 37 | (user.isAdmin || (user.id === c.author_id && parentStatus === "open")); |
| 38 | return ( |
| 39 | <> |
| 40 | {comments.map((comment) => ( |
| 41 | <div class="timeline-item"> |
| 42 | <div class="timeline-author"> |
| 43 | <Avatar |
| 44 | userId={comment.author_id} |
| 45 | version={comment.author_avatar_version} |
| 46 | size={24} |
| 47 | /> |
| 48 | <strong safe> |
| 49 | {displayName(comment.author_username)} |
| 50 | </strong> |
| 51 | <div class="timeline-author-right"> |
| 52 | {canEditComment(comment) && ( |
| 53 | <details class="inline-edit-details"> |
| 54 | <summary class="btn btn-xs"> |
| 55 | <span class="when-closed">Edit</span> |
| 56 | <span class="when-open"> |
| 57 | Stop editing |
| 58 | </span> |
| 59 | </summary> |
| 60 | </details> |
| 61 | )} |
| 62 | <DateWithEdited |
| 63 | date={comment.created_at} |
| 64 | editedAt={comment.edited_at} |
| 65 | /> |
| 66 | </div> |
| 67 | </div> |
| 68 | {canEditComment(comment) && ( |
| 69 | <div class="inline-edit-form-area"> |
| 70 | <form |
| 71 | method="POST" |
| 72 | action={`${baseUrl}/comments/${comment.id}/edit`} |
| 73 | class="inline-edit-form" |
| 74 | > |
| 75 | <div class="form-group"> |
| 76 | <textarea |
| 77 | class="form-input" |
| 78 | name="edit_body" |
| 79 | rows="6" |
| 80 | maxlength={config.MAX_TEXT_BODY_BYTES} |
| 81 | safe |
| 82 | > |
| 83 | {comment.body} |
| 84 | </textarea> |
| 85 | </div> |
| 86 | <div class="form-actions"> |
| 87 | <button |
| 88 | type="submit" |
| 89 | class="btn btn-sm btn-primary" |
| 90 | > |
| 91 | Save |
| 92 | </button> |
| 93 | </div> |
| 94 | </form> |
| 95 | </div> |
| 96 | )} |
| 97 | <div class="timeline-body markdown-body"> |
| 98 | {comment.bodyHtml as "safe"} |
| 99 | </div> |
| 100 | <ReactionBar |
| 101 | reactions={commentReactions.get(comment.id) ?? []} |
| 102 | postUrl={reactUrl} |
| 103 | commentId={comment.id} |
| 104 | user={user} |
| 105 | /> |
| 106 | </div> |
| 107 | ))} |
| 108 | |
| 109 | {!!user && ( |
| 110 | <div class="timeline-item timeline-item-new"> |
| 111 | <h3 class="section-title">Add a comment</h3> |
| 112 | <form method="POST" action={`${baseUrl}/comments`}> |
| 113 | <div class="form-group"> |
| 114 | <textarea |
| 115 | name="body" |
| 116 | rows="6" |
| 117 | maxlength={config.MAX_TEXT_BODY_BYTES} |
| 118 | placeholder="Leave a comment (Markdown supported)" |
| 119 | required |
| 120 | /> |
| 121 | </div> |
| 122 | <div class="form-actions"> |
| 123 | <button type="submit" class="btn btn-primary"> |
| 124 | Comment |
| 125 | </button> |
| 126 | </div> |
| 127 | </form> |
| 128 | </div> |
| 129 | )} |
| 130 | {!user && ( |
| 131 | <p class="text-muted"> |
| 132 | <a href="/login">Sign in</a> to leave a comment. |
| 133 | </p> |
| 134 | )} |
| 135 | </> |
| 136 | ); |
| 137 | } |
| 138 |