Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}