render_post.gno
9.65 Kb · 418 lines
1package boards2
2
3import (
4 "strconv"
5 "strings"
6
7 "gno.land/p/gnoland/boards"
8 "gno.land/p/jeronimoalbi/mdform"
9 "gno.land/p/leon/svgbtn"
10 "gno.land/p/moul/md"
11 "gno.land/p/nt/mux"
12 "gno.land/p/nt/ufmt"
13)
14
15func renderPost(post *boards.Post, path, indent string, levels int) string {
16 var b strings.Builder
17
18 // Thread reposts might not have a title, if so get title from source thread
19 title := post.Title
20 if boards.IsRepost(post) && title == "" {
21 if board, ok := gBoards.Get(post.OriginalBoardID); ok {
22 if src, ok := board.Threads.Get(post.ParentID); ok {
23 title = src.Title
24 }
25 }
26 }
27
28 if title != "" { // Replies don't have a title
29 b.WriteString(md.H2(title))
30 }
31
32 b.WriteString(indent + "\n")
33 b.WriteString(renderPostContent(post, indent, levels))
34
35 if post.Replies.Size() == 0 {
36 return b.String()
37 }
38
39 // XXX: This triggers for reply views
40 if levels == 0 {
41 b.WriteString(indent + "\n")
42 return b.String()
43 }
44
45 if path != "" {
46 b.WriteString(renderTopLevelReplies(post, path, indent, levels-1))
47 } else {
48 b.WriteString(renderSubReplies(post, indent, levels-1))
49 }
50 return b.String()
51}
52
53func renderPostContent(post *boards.Post, indent string, levels int) string {
54 var b strings.Builder
55
56 if post.Hidden {
57 // Flagged comment should be hidden, but replies still visible (see: #3480)
58 // Flagged threads will be hidden by render function caller.
59 return indentBody(indent, md.Italic("⚠ Reply is hidden as it has been flagged as inappropriate")) + "\n"
60 }
61
62 // Author and date header
63 creatorLink := userLink(post.Creator)
64 roleBadge := getRoleBadge(post)
65 date := post.CreatedAt.Format(dateFormat)
66 b.WriteString(indent)
67 b.WriteString(md.Bold(creatorLink) + roleBadge + " · " + date)
68 if !boards.IsThread(post) {
69 b.WriteString(" " + md.Link("#"+post.ID.String(), makeReplyURI(post)))
70 }
71 b.WriteString(" \n")
72
73 srcContent, srcPost := renderSourcePost(post, indent)
74 if boards.IsRepost(post) && srcPost != nil {
75 originLink := md.Link("another thread", makeThreadURI(srcPost))
76 b.WriteString(" \nThis thread is a repost of " + originLink + ": \n")
77 }
78
79 b.WriteString(srcContent)
80
81 if boards.IsRepost(post) && srcPost == nil && len(post.Body) > 0 {
82 // Add a newline to separate source deleted message from repost body content
83 b.WriteString("\n")
84 }
85
86 b.WriteString(indentBody(indent, post.Body))
87 b.WriteString("\n")
88
89 if boards.IsThread(post) {
90 // Split content and controls for threads.
91 b.WriteString("\n")
92 }
93
94 // Action buttons
95 b.WriteString(indent)
96 if !boards.IsThread(post) {
97 b.WriteString(" \n")
98 b.WriteString(indent)
99 }
100
101 actions := []string{
102 md.Link("Flag", makeFlagURI(post)),
103 }
104
105 if boards.IsThread(post) {
106 repostAction := md.Link("Repost", makeCreateRepostURI(post))
107 if post.Reposts.Size() > 0 {
108 repostAction += " [" + strconv.Itoa(post.Reposts.Size()) + "]"
109 }
110 actions = append(actions, repostAction)
111 }
112
113 isReadonly := post.Readonly || post.Board.Readonly
114 if !isReadonly {
115 replyLabel := "Reply"
116 if boards.IsThread(post) {
117 replyLabel = "Comment"
118 }
119 replyAction := md.Link(replyLabel, makeCreateReplyURI(post))
120 // Add reply count if any
121 if post.Replies.Size() > 0 {
122 replyAction += " [" + strconv.Itoa(post.Replies.Size()) + "]"
123 }
124
125 actions = append(
126 actions,
127 replyAction,
128 md.Link("Edit", makeEditPostURI(post)),
129 md.Link("Delete", makeDeletePostURI(post)),
130 )
131 }
132
133 if levels == 0 {
134 if boards.IsThread(post) {
135 actions = append(actions, md.Link("Show all Replies", makeThreadURI(post)))
136 } else {
137 actions = append(actions, md.Link("View Thread", makeThreadURI(post)))
138 }
139 }
140
141 b.WriteString("↳ " + strings.Join(actions, " • ") + "\n")
142 return b.String()
143}
144
145func renderPostInner(post *boards.Post) string {
146 if boards.IsThread(post) {
147 return ""
148 }
149
150 var (
151 s string
152 threadID = post.ThreadID
153 thread, _ = post.Board.Threads.Get(threadID)
154 )
155
156 // Fully render parent if it's not a repost.
157 if !boards.IsRepost(post) {
158 parentID := post.ParentID
159 parent := thread
160
161 if thread.ID != parentID {
162 parent, _ = thread.Replies.Get(parentID)
163 }
164
165 s += renderPost(parent, "", "", 0) + "\n"
166 }
167
168 s += renderPost(post, "", "> ", 5)
169 return s
170}
171
172func renderSourcePost(post *boards.Post, indent string) (string, *boards.Post) {
173 if !boards.IsRepost(post) {
174 return "", nil
175 }
176
177 indent += "> "
178
179 // TODO: figure out a way to decouple posts from a global storage.
180 board, ok := gBoards.Get(post.OriginalBoardID)
181 if !ok {
182 // TODO: Boards can't be deleted so this might be redundant
183 return indentBody(indent, md.Italic("⚠ Source board has been deleted")+"\n"), nil
184 }
185
186 srcPost, ok := board.Threads.Get(post.ParentID)
187 if !ok {
188 return indentBody(indent, md.Italic("⚠ Source post has been deleted")+"\n"), nil
189 }
190
191 if srcPost.Hidden {
192 return indentBody(indent, md.Italic("⚠ Source post has been flagged as inappropriate")+"\n"), nil
193 }
194
195 return indentBody(indent, srcPost.Summary()) + "\n\n", srcPost
196}
197
198func renderFlagPost(res *mux.ResponseWriter, req *mux.Request) {
199 name := req.GetVar("board")
200 board, found := gBoards.GetByName(name)
201 if !found {
202 res.Write("Board does not exist: " + name)
203 return
204 }
205
206 // Thread ID must always be available
207 rawID := req.GetVar("thread")
208 threadID, err := strconv.Atoi(rawID)
209 if err != nil {
210 res.Write("Invalid thread ID: " + rawID)
211 return
212 }
213
214 thread, found := board.Threads.Get(boards.ID(threadID))
215 if !found {
216 res.Write("Thread does not exist with ID: " + rawID)
217 return
218 }
219
220 // Parse reply ID when post is a reply
221 var reply *boards.Post
222 rawID = req.GetVar("reply")
223 isReply := rawID != ""
224 if isReply {
225 replyID, err := strconv.Atoi(rawID)
226 if err != nil {
227 res.Write("Invalid reply ID: " + rawID)
228 return
229 }
230
231 reply, _ = thread.Replies.Get(boards.ID(replyID))
232 if reply == nil {
233 res.Write("Reply does not exist with ID: " + rawID)
234 return
235 }
236 }
237
238 exec := "FlagThread"
239 if isReply {
240 exec = "FlagReply"
241 }
242
243 form := mdform.New("exec", exec)
244 form.Input(
245 "boardID",
246 "placeholder", "Board ID",
247 "value", board.ID.String(),
248 "readonly", "true",
249 )
250 form.Input(
251 "threadID",
252 "placeholder", "Thread ID",
253 "value", thread.ID.String(),
254 "readonly", "true",
255 )
256
257 if isReply {
258 form.Input(
259 "replyID",
260 "placeholder", "Reply ID",
261 "value", reply.ID.String(),
262 "readonly", "true",
263 )
264 }
265
266 form.Input(
267 "reason",
268 "placeholder", "Flagging Reason",
269 )
270
271 // Breadcrumb navigation
272 backLink := md.Link("← Back to thread", makeThreadURI(thread))
273
274 if isReply {
275 res.Write(md.H1(board.Name + ": Flag Comment"))
276 } else {
277 res.Write(md.H1(board.Name + ": Flag Thread"))
278 }
279 res.Write(backLink + "\n\n")
280
281 res.Write(
282 md.Paragraph(
283 "Thread or comment moderation is done though flagging, which is usually done "+
284 "by board members with the moderator role, though other roles could also potentially flag.",
285 ) +
286 md.Paragraph(
287 "Flagging relies on a configurable threshold, which by default is of one flag, that when "+
288 "reached leads to the flagged thread or comment to be hidden.",
289 ) +
290 md.Paragraph(
291 "Flagging thresholds can be different within each board.",
292 ),
293 )
294
295 if isReply {
296 res.Write(
297 md.Paragraph(
298 ufmt.Sprintf(
299 "⚠ Your are flagging a %s from %s ⚠",
300 md.Link("comment", makeReplyURI(reply)),
301 userLink(reply.Creator),
302 ),
303 ),
304 )
305 } else {
306 res.Write(
307 md.Paragraph(
308 ufmt.Sprintf(
309 "⚠ Your are flagging %s thread ⚠",
310 md.Link(thread.Title, makeThreadURI(thread)),
311 ),
312 ),
313 )
314 }
315
316 res.Write(form.String())
317 res.Write("\n\n**Done?** " + svgbtn.ButtonWithRadius(136, 32, 4, "#E2E2E2", "#54595D", "Return to thread", makeThreadURI(thread)) + "\n")
318}
319
320func renderReplyPost(res *mux.ResponseWriter, req *mux.Request) {
321 name := req.GetVar("board")
322 board, found := gBoards.GetByName(name)
323 if !found {
324 res.Write("Board does not exist: " + name)
325 return
326 }
327
328 // Thread ID must always be available
329 rawID := req.GetVar("thread")
330 threadID, err := strconv.Atoi(rawID)
331 if err != nil {
332 res.Write("Invalid thread ID: " + rawID)
333 return
334 }
335
336 thread, found := board.Threads.Get(boards.ID(threadID))
337 if !found {
338 res.Write("Thread does not exist with ID: " + rawID)
339 return
340 }
341
342 // Parse reply ID when post is a reply
343 var reply *boards.Post
344 rawID = req.GetVar("reply")
345 isReply := rawID != ""
346 if isReply {
347 replyID, err := strconv.Atoi(rawID)
348 if err != nil {
349 res.Write("Invalid reply ID: " + rawID)
350 return
351 }
352
353 reply, _ = thread.Replies.Get(boards.ID(replyID))
354 if reply == nil {
355 res.Write("Reply does not exist with ID: " + rawID)
356 return
357 }
358 }
359
360 form := mdform.New("exec", "CreateReply")
361 form.Input(
362 "boardID",
363 "placeholder", "Board ID",
364 "value", board.ID.String(),
365 "readonly", "true",
366 )
367 form.Input(
368 "threadID",
369 "placeholder", "Thread ID",
370 "value", thread.ID.String(),
371 "readonly", "true",
372 )
373
374 if isReply {
375 form.Input(
376 "replyID",
377 "placeholder", "Reply ID",
378 "value", reply.ID.String(),
379 "readonly", "true",
380 )
381 } else {
382 form.Input(
383 "replyID",
384 "placeholder", "Reply ID",
385 "value", "0",
386 "readonly", "true",
387 )
388 }
389
390 form.Textarea(
391 "body",
392 "placeholder", "Comment",
393 "required", "true",
394 )
395
396 // Breadcrumb navigation
397 backLink := md.Link("← Back to thread", makeThreadURI(thread))
398
399 if isReply {
400 res.Write(md.H1(board.Name + ": Reply"))
401 res.Write(backLink + "\n\n")
402 res.Write(
403 md.Paragraph(ufmt.Sprintf("Replying to a comment posted by %s:", userLink(reply.Creator))) +
404 md.Blockquote(reply.Body),
405 )
406 } else {
407 res.Write(md.H1(board.Name + ": Comment"))
408 res.Write(backLink + "\n\n")
409 res.Write(
410 md.Paragraph(
411 ufmt.Sprintf("Commenting on %s thread", md.Link(thread.Title, makeThreadURI(thread))),
412 ),
413 )
414 }
415
416 res.Write(form.String())
417 res.Write("\n\n**Done?** " + svgbtn.ButtonWithRadius(136, 32, 4, "#E2E2E2", "#54595D", "Return to thread", makeThreadURI(thread)) + "\n")
418}