package boards2 import ( "strconv" "strings" "gno.land/p/gnoland/boards" "gno.land/p/jeronimoalbi/mdform" "gno.land/p/leon/svgbtn" "gno.land/p/moul/md" "gno.land/p/moul/mdtable" "gno.land/p/nt/mdalert/v0" "gno.land/p/nt/mux/v0" "gno.land/p/nt/ufmt/v0" ) func renderPost(post *boards.Post, path, indent string, levels int) string { var b strings.Builder // Thread reposts might not have a title, if so get title from source thread title := post.Title if boards.IsRepost(post) && title == "" { if board, ok := gBoards.Get(post.OriginalBoardID); ok { if src, ok := getThread(board, post.ParentID); ok { title = src.Title } } } if title != "" { // Replies don't have a title b.WriteString(md.H2(title)) } b.WriteString(indent + "\n") b.WriteString(renderPostContent(post, indent, levels)) if post.Replies.Size() == 0 { return b.String() } // XXX: This triggers for reply views if levels == 0 { b.WriteString(indent + "\n") return b.String() } if path != "" { b.WriteString(renderTopLevelReplies(post, path, indent, levels-1)) } else { b.WriteString(renderSubReplies(post, indent, levels-1)) } return b.String() } func renderPostContent(post *boards.Post, indent string, levels int) string { var b strings.Builder // Author and date header creatorLink := userLink(post.Creator) roleBadge := getRoleBadge(post) date := post.CreatedAt.Format(dateFormat) b.WriteString(indent) b.WriteString(md.Bold(creatorLink) + roleBadge + " · " + date) if !boards.IsThread(post) { b.WriteString(" " + md.Link("#"+post.ID.String(), makeReplyURI(post))) } b.WriteString(" \n") // Flagged comment should be hidden, but replies still visible (see: #3480) // Flagged threads will be hidden by render function caller. if post.Hidden { link := md.Link("inappropriate", makeFlaggingReasonsURI(post)) b.WriteString(indentBody(indent, "⚠ Reply is hidden as it has been flagged as "+link)) b.WriteString("\n") return b.String() } srcContent, srcPost := renderSourcePost(post, indent) if boards.IsRepost(post) && srcPost != nil { msg := ufmt.Sprintf( "Original thread is %s \nCreated by %s on %s", md.Link(srcPost.Title, makeThreadURI(srcPost)), userLink(srcPost.Creator), srcPost.CreatedAt.Format(dateFormat), ) b.WriteString(mdalert.New(mdalert.TypeInfo, "Thread Repost", msg, true).String()) b.WriteString("\n") } // Render repost body before original thread's body if post.Body != "" { b.WriteString(indentBody(indent, post.Body) + "\n") if srcContent != "" { // Add extra line to separate repost content from original thread content b.WriteString("\n") } } b.WriteString(srcContent) // Add a newline to separate source deleted message from repost body content if boards.IsRepost(post) && srcPost == nil && len(post.Body) > 0 { b.WriteString("\n\n") } // Split thread content and actions if boards.IsThread(post) && !boards.IsRepost(post) { b.WriteString("\n") } // Action buttons b.WriteString(indent) if !boards.IsThread(post) { // is comment b.WriteString(" \n") b.WriteString(indent) } actions := []string{ md.Link("Flag", makeFlagURI(post)), } if boards.IsThread(post) { repostAction := md.Link("Repost", makeCreateRepostURI(post)) if post.Reposts.Size() > 0 { repostAction += " [" + strconv.Itoa(post.Reposts.Size()) + "]" } actions = append(actions, repostAction) } isReadonly := post.Readonly || post.Board.Readonly if !isReadonly { replyLabel := "Reply" if boards.IsThread(post) { replyLabel = "Comment" } replyAction := md.Link(replyLabel, makeCreateReplyURI(post)) // Add reply count if any if post.Replies.Size() > 0 { replyAction += " [" + strconv.Itoa(post.Replies.Size()) + "]" } actions = append( actions, replyAction, md.Link("Edit", makeEditPostURI(post)), md.Link("Delete", makeDeletePostURI(post)), ) } if levels == 0 { if boards.IsThread(post) { actions = append(actions, md.Link("Show all Replies", makeThreadURI(post))) } else { actions = append(actions, md.Link("View Thread", makeThreadURI(post))) } } b.WriteString("↳ " + strings.Join(actions, " • ") + "\n") return b.String() } func renderPostInner(post *boards.Post) string { if boards.IsThread(post) { return "" } var ( s string threadID = post.ThreadID thread, _ = getThread(post.Board, threadID) ) // Fully render parent if it's not a repost. if !boards.IsRepost(post) { parentID := post.ParentID parent := thread if thread.ID != parentID { parent, _ = getReply(thread, parentID) } s += renderPost(parent, "", "", 0) + "\n" } s += renderPost(post, "", "> ", 5) return s } func renderSourcePost(post *boards.Post, indent string) (string, *boards.Post) { if !boards.IsRepost(post) { return "", nil } indent += "> " // TODO: figure out a way to decouple posts from a global storage. board, ok := gBoards.Get(post.OriginalBoardID) if !ok { // TODO: Boards can't be deleted so this might be redundant return indentBody(indent, "⚠ Source board has been deleted"), nil } srcPost, ok := getThread(board, post.ParentID) if !ok { return indentBody(indent, "⚠ Source post has been deleted"), nil } if srcPost.Hidden { return indentBody(indent, "⚠ Source post has been flagged as inappropriate"), nil } return indentBody(indent, srcPost.Body) + "\n\n", srcPost } func renderFlagPost(res *mux.ResponseWriter, req *mux.Request) { name := req.GetVar("board") board, found := gBoards.GetByName(name) if !found { res.Write("Board not found") return } // Thread ID must always be available rawID := req.GetVar("thread") threadID, err := strconv.Atoi(rawID) if err != nil { res.Write("Invalid thread ID: " + rawID) return } thread, found := getThread(board, boards.ID(threadID)) if !found { res.Write("Thread not found") return } // Parse reply ID when post is a reply var reply *boards.Post rawID = req.GetVar("reply") isReply := rawID != "" if isReply { replyID, err := strconv.Atoi(rawID) if err != nil { res.Write("Invalid reply ID: " + rawID) return } reply, _ = getReply(thread, boards.ID(replyID)) if reply == nil { res.Write("Reply not found") return } } exec := "FlagThread" if isReply { exec = "FlagReply" } form := mdform.New("exec", exec) form.Input( "boardID", "placeholder", "Board ID", "value", board.ID.String(), "readonly", "true", ) form.Input( "threadID", "placeholder", "Thread ID", "value", thread.ID.String(), "readonly", "true", ) if isReply { form.Input( "replyID", "placeholder", "Reply ID", "value", reply.ID.String(), "readonly", "true", ) } form.Input( "reason", "placeholder", "Flagging Reason", ) // Breadcrumb navigation backLink := md.Link("← Back to thread", makeThreadURI(thread)) if isReply { res.Write(md.H1(board.Name + ": Flag Comment")) } else { res.Write(md.H1(board.Name + ": Flag Thread")) } res.Write(backLink + "\n\n") res.Write( md.Paragraph( "Thread or comment moderation is done through flagging, which is usually done "+ "by board members with the moderator role, though other roles could also potentially flag.", ) + md.Paragraph( "Flagging relies on a configurable threshold, which by default is of one flag, that when "+ "reached leads to the flagged thread or comment to be hidden.", ) + md.Paragraph( "Flagging thresholds can be different within each board.", ), ) if isReply { res.Write( md.Paragraph( ufmt.Sprintf( "⚠ You are flagging a %s from %s ⚠", md.Link("comment", makeReplyURI(reply)), userLink(reply.Creator), ), ), ) } else { res.Write( md.Paragraph( ufmt.Sprintf( "⚠ You are flagging the thread: %s ⚠", md.Link(thread.Title, makeThreadURI(thread)), ), ), ) } res.Write(form.String()) res.Write("\n\n**Done?** " + svgbtn.ButtonWithRadius(136, 32, 4, "#E2E2E2", "#54595D", "Return to thread", makeThreadURI(thread)) + "\n") } func renderFlaggingReasonsPost(res *mux.ResponseWriter, req *mux.Request) { name := req.GetVar("board") board, found := gBoards.GetByName(name) if !found { res.Write("Board not found") return } // Thread ID must always be available rawID := req.GetVar("thread") threadID, err := strconv.Atoi(rawID) if err != nil { res.Write("Invalid thread ID: " + rawID) return } thread, found := getThread(board, boards.ID(threadID)) if !found { res.Write("Thread not found") return } flags := thread.Flags // Parse reply ID when post is a reply var reply *boards.Post rawID = req.GetVar("reply") isReply := rawID != "" if isReply { replyID, err := strconv.Atoi(rawID) if err != nil { res.Write("Invalid reply ID: " + rawID) return } reply, found = getReply(thread, boards.ID(replyID)) if !found { res.Write("Reply not found") return } flags = reply.Flags } table := mdtable.Table{ Headers: []string{"Moderator", "Reason"}, } flags.Iterate(0, flags.Size(), func(f boards.Flag) bool { table.Append([]string{userLink(f.User), f.Reason}) return false }) // Breadcrumb navigation backLink := md.Link("← Back to thread", makeThreadURI(thread)) res.Write(md.H1("Flagging Reasons")) res.Write(backLink + "\n\n") if isReply { res.Write( md.Paragraph( ufmt.Sprintf( "Moderation flags for a %s submitted by %s", md.Link("comment", makeReplyURI(reply)), userLink(reply.Creator), ), ), ) } else { res.Write( md.Paragraph( // Intentionally hide flagged thread title ufmt.Sprintf("Moderation flags for %s", md.Link("thread", makeThreadURI(thread))), ), ) } res.Write(table.String()) } func renderReplyPost(res *mux.ResponseWriter, req *mux.Request) { name := req.GetVar("board") board, found := gBoards.GetByName(name) if !found { res.Write("Board not found") return } // Thread ID must always be available rawID := req.GetVar("thread") threadID, err := strconv.Atoi(rawID) if err != nil { res.Write("Invalid thread ID: " + rawID) return } thread, found := board.Threads.Get(boards.ID(threadID)) if !found { res.Write("Thread not found") return } // Parse reply ID when post is a reply var reply *boards.Post rawID = req.GetVar("reply") isReply := rawID != "" if isReply { replyID, err := strconv.Atoi(rawID) if err != nil { res.Write("Invalid reply ID: " + rawID) return } reply, _ = getReply(thread, boards.ID(replyID)) if reply == nil { res.Write("Reply not found") return } } form := mdform.New("exec", "CreateReply") form.Input( "boardID", "placeholder", "Board ID", "value", board.ID.String(), "readonly", "true", ) form.Input( "threadID", "placeholder", "Thread ID", "value", thread.ID.String(), "readonly", "true", ) if isReply { form.Input( "replyID", "placeholder", "Reply ID", "value", reply.ID.String(), "readonly", "true", ) } else { form.Input( "replyID", "placeholder", "Reply ID", "value", "0", "readonly", "true", ) } form.Textarea( "body", "placeholder", "Comment", "required", "true", ) // Breadcrumb navigation backLink := md.Link("← Back to thread", makeThreadURI(thread)) if isReply { res.Write(md.H1(board.Name + ": Reply")) res.Write(backLink + "\n\n") res.Write( md.Paragraph(ufmt.Sprintf("Replying to a comment posted by %s:", userLink(reply.Creator))) + md.Blockquote(reply.Body), ) } else { res.Write(md.H1(board.Name + ": Comment")) res.Write(backLink + "\n\n") res.Write( md.Paragraph( ufmt.Sprintf("Commenting on the thread: %s", md.Link(thread.Title, makeThreadURI(thread))), ), ) } res.Write(form.String()) res.Write("\n\n**Done?** " + svgbtn.ButtonWithRadius(136, 32, 4, "#E2E2E2", "#54595D", "Return to thread", makeThreadURI(thread)) + "\n") }