Kind Realm (renderable)
Name v0
Namespace gnoland / boards2
Exported functions 51
Module gno.land/r/gnoland/boards2/v0
gno 0.9 render_post.gno gno
⧉
1 package boards2 2 3 import ( 4 "strconv" 5 "strings" 6 7 "gno.land/p/gnoland/boards/v0" 8 AcceptInvite (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, user string)
AreRealmMembersLocked () bool
Ban (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, user string, hours uint, reason string)
BoardCount () int
ChangeMemberRole (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, member string, role string)
CreateBoard (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, listed bool, open bool) uint64
CreateReply (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threadID uint64, replyID uint64, body string) uint64
CreateRepost (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threadID uint64, destinationBoardID uint64, title string, body string) uint64
CreateThread (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, title string, body string) uint64
DeleteReply (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threadID uint64, replyID uint64)
DeleteThread (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threadID uint64)
EditReply (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threadID uint64, replyID uint64, body string)
EditThread (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threadID uint64, title string, body string)
FlagReply (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threadID uint64, replyID uint64, reason string)
FlagThread (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threadID uint64, reason string)
FreezeBoard (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64)
FreezeThread (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threadID uint64)
GetBoard (id uint64) (struct{id uint64; name string; aliases []string; readonly bool; threadCount int; memberCount int; creator .uverse.address; createdAt int64; updatedAt int64}, bool)
GetBoardIDFromName (interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string) (uint64, found bool)
GetBoards (start int, count int) []gno.land/p/gnoland/boards/exts/hub/v0.Board
GetComment (boardID uint64, threadID uint64, commentID uint64) (struct{id uint64; boardID uint64; threadID uint64; parentID uint64; body string; hidden bool; replyCount int; flagCount int; creator .uverse.address; createdAt int64; updatedAt int64}, bool)
GetComments (boardID uint64, threadID uint64, start int, count int) []gno.land/p/gnoland/boards/exts/hub/v0.Comment
GetFlaggingThreshold (boardID uint64) int
GetFlags (boardID uint64, threadID uint64, commentID uint64, start int, count int) []gno.land/p/gnoland/boards/exts/hub/v0.Flag
GetMembers (boardID uint64, start int, count int) []gno.land/p/gnoland/boards/exts/hub/v0.Member
GetReplies (boardID uint64, threadID uint64, commentID uint64, start int, count int) []gno.land/p/gnoland/boards/exts/hub/v0.Comment
GetReposts (boardID uint64, threadID uint64, start int, count int) []gno.land/p/gnoland/boards/exts/hub/v0.Thread
GetThread (boardID uint64, threadID uint64) (struct{id uint64; originalBoardID uint64; originalThreadID uint64; boardID uint64; title string; body string; hidden bool; readonly bool; commentCount int; repostCount int; flagCount int; creator .uverse.address; createdAt int64; updatedAt int64}, bool)
GetThreads (boardID uint64, start int, count int) []gno.land/p/gnoland/boards/exts/hub/v0.Thread
HasMemberRole (boardID uint64, member string, role string) bool
InviteMember (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, user string, role string)
InviteMembers (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, invites ...gno.land/r/gnoland/boards2/v0.Invite)
IsBanned (boardID uint64, user string) bool
IsBoardFrozen (boardID uint64) bool
IsMember (boardID uint64, user string) bool
IsRealmLocked () bool
IsThreadFrozen (boardID uint64, threadID uint64) bool
LockRealm (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, lockRealmMembers bool)
RemoveMember (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, member string)
RenameBoard (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, newName string)
Render (path string) string
RequestInvite (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64)
RevokeInvite (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, user string)
SetFlaggingThreshold (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threshold int)
SetHelp (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, content string)
SetPermissions (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, p interface {HasPermission func(.uverse.address, gno.land/p/gnoland/boards/v0.Permission) bool; HasRole func(.uverse.address, gno.land/p/gnoland/boards/v0.Role) bool; HasUser func(.uverse.address) bool; IterateUsers func(int, int, gno.land/p/gnoland/boards/v0.UsersIterFn) bool; RemoveUser func(.uverse.address) bool; SetUserRoles func(.uverse.address, ...gno.land/p/gnoland/boards/v0.Role); UsersCount func() int; WithPermission func(.uverse.address, gno.land/p/gnoland/boards/v0.Permission, gno.land/p/gnoland/boards/v0.Args, func())})
SetRealmNotice (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, message string)
SetRequiredAccountAmount (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, amount int64)
Unban (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, user string, reason string)
UnfreezeBoard (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64)
UnfreezeThread (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, boardID uint64, threadID uint64)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.
"gno.land/p/jeronimoalbi/mdform/v0"
9 "gno.land/p/leon/svgbtn/v0"
10 "gno.land/p/moul/md/v0"
11 "gno.land/p/moul/mdtable/v0"
12 "gno.land/p/nt/markdown/foreign/v0"
13 "gno.land/p/nt/mdalert/v0"
14 "gno.land/p/nt/mux/v0"
15 "gno.land/p/nt/ufmt/v0"
16 )
17
18 // renderPost renders a post and (unless it's a leaf or capped) its replies.
19 // desc is the page's sort order, forwarded so a comment's nested inline
20 // children (renderSubReplies) match the order of the view they appear in. It
21 // is unused for the top-level/re-root call (path != "" routes to
22 // renderTopLevelReplies, which derives the order from the path); those
23 // callers pass false.
24 func renderPost(post *boards.Post, path, indent string , levels int , budget * int , desc bool ) string {
25 var b strings.Builder
26
27 // Thread reposts might not have a title, if so get title from source thread
28 title := post.Title
29 if boards.IsRepost(post) && title == "" {
30 if board, ok := gBoards.Get(post.OriginalBoardID); ok {
31 if src, ok := getThread(board, post.ParentID); ok {
32 title = src.Title
33 }
34 }
35 }
36
37 if title != "" { // Replies don't have a title
38 b.WriteString(md.H2(md.EscapeText(title)))
39 }
40
41 b.WriteString(indent + "\n" )
42 b.WriteString(renderPostContent(post, indent, levels, budget))
43
44 if post.Replies.Size() == 0 {
45 return b.String()
46 }
47
48 // In practice this only fires for the re-rooted view's context-parent,
49 // which renderPostInner renders with an explicit levels==0. The thread
50 // recursion does levels-1 in BOTH renderPost and renderTopLevelReplies/
51 // renderSubReplies, so levels drops by 2 per nesting level and skips 0 —
52 // i.e. levels does NOT bound depth in the thread view; the render budget
53 // and the per-node breadth cap (renderSubReplies) are the real bounds.
54 if levels == 0 {
55 b.WriteString(indent + "\n" )
56 return b.String()
57 }
58
59 if path != "" {
60 b.WriteString(renderTopLevelReplies(post, path, indent, levels- 1 , budget))
61 } else {
62 b.WriteString(renderSubReplies(post, indent, levels- 1 , budget, desc))
63 }
64 return b.String()
65 }
66
67 func renderPostContent(post *boards.Post, indent string , levels int , budget * int ) string {
68 var b strings.Builder
69
70 // Author and date header
71 creatorLink := userLink(post.Creator)
72 roleBadge := getRoleBadge(post)
73 date := post.CreatedAt.Format(dateFormat)
74 b.WriteString(indent)
75 b.WriteString(md.Bold(creatorLink) + roleBadge + " · " + date)
76 if !boards.IsThread(post) {
77 b.WriteString( " " + md.Link( "#" +post.ID.String(), makeReplyURI(post)))
78 }
79 b.WriteString( " \n" )
80
81 // Flagged comment should be hidden, but replies still visible (see: #3480)
82 // Flagged threads will be hidden by render function caller.
83 if post.Hidden {
84 link := md.Link( "inappropriate" , makeFlaggingReasonsURI(post))
85 b.WriteString(indentBody(indent, "⚠ Reply is hidden as it has been flagged as " +link))
86 b.WriteString( "\n" )
87 return b.String()
88 }
89
90 srcContent, srcPost := renderSourcePost(post, indent, budget)
91 if boards.IsRepost(post) && srcPost != nil {
92 msg := ufmt.Sprintf(
93 "Original thread is %s \nCreated by %s on %s" ,
94 md.Link(srcPost.Title, makeThreadURI(srcPost)),
95 userLink(srcPost.Creator),
96 srcPost.CreatedAt.Format(dateFormat),
97 )
98
99 b.WriteString(mdalert.New(mdalert.TypeInfo, "Thread Repost" , msg, true ).String())
100 b.WriteString( "\n" )
101 }
102
103 // Render repost body before original thread's body
104 if post.Body != "" {
105 b.WriteString(indentForeignBody(indent, post.Body, budget) + "\n" )
106 if srcContent != "" {
107 // Add extra line to separate repost content from original thread content
108 b.WriteString( "\n" )
109 }
110 }
111
112 b.WriteString(srcContent)
113
114 // Add a newline to separate source deleted message from repost body content
115 if boards.IsRepost(post) && srcPost == nil && len(post.Body) > 0 {
116 b.WriteString( "\n\n" )
117 }
118
119 // Split thread content and actions
120 if boards.IsThread(post) && !boards.IsRepost(post) {
121 b.WriteString( "\n" )
122 }
123
124 // Action buttons
125 b.WriteString(indent)
126 if !boards.IsThread(post) { // is comment
127 b.WriteString( " \n" )
128 b.WriteString(indent)
129 }
130
131 actions := [] string {
132 md.Link( "Flag" , makeFlagURI(post)),
133 }
134
135 if boards.IsThread(post) {
136 repostAction := md.Link( "Repost" , makeCreateRepostURI(post))
137 if post.Reposts.Size() > 0 {
138 repostAction += " [" + strconv.Itoa(post.Reposts.Size()) + "]"
139 }
140 actions = append(actions, repostAction)
141 }
142
143 isReadonly := post.Readonly || post.Board.Readonly
144 // A reply doesn't carry the thread's frozen flag (FreezeThread sets
145 // Readonly on the thread post only), so check the enclosing thread too —
146 // otherwise a frozen thread's replies show Reply/Edit/Delete links that
147 // the backend rejects. Mirrors the IsReadonly helper (board || thread).
148 if !isReadonly && !boards.IsThread(post) {
149 if t, ok := getThread(post.Board, post.ThreadID); ok {
150 isReadonly = t.Readonly
151 }
152 }
153 if !isReadonly {
154 replyLabel := "Reply"
155 if boards.IsThread(post) {
156 replyLabel = "Comment"
157 }
158 replyAction := md.Link(replyLabel, makeCreateReplyURI(post))
159 // Add reply count if any
160 if post.Replies.Size() > 0 {
161 replyAction += " [" + strconv.Itoa(post.Replies.Size()) + "]"
162 }
163
164 actions = append(
165 actions,
166 replyAction,
167 md.Link( "Edit" , makeEditPostURI(post)),
168 md.Link( "Delete" , makeDeletePostURI(post)),
169 )
170 }
171
172 if levels == 0 {
173 switch {
174 case boards.IsThread(post):
175 actions = append(actions, md.Link( "Show all Replies" , makeThreadURI(post)))
176 case post.Replies.Size() > 0 :
177 // Reached at levels==0 — in practice the re-rooted view's
178 // context-parent (see renderPost). It still has replies below, so
179 // re-root here (Reddit/HN "continue this thread") to keep the
180 // subtree drillable instead of bouncing to the thread root.
181 actions = append(actions, md.Link( "Continue this thread →" , makeReplyURI(post)))
182 }
183 }
184
185 b.WriteString( "↳ " + strings.Join(actions, " • " ) + "\n" )
186 return b.String()
187 }
188
189 func renderPostInner(post *boards.Post, path string ) string {
190 if boards.IsThread(post) {
191 return ""
192 }
193
194 var (
195 s string
196 threadID = post.ThreadID
197 thread, _ = getThread(post.Board, threadID)
198 budget = maxRenderedBodies()
199 )
200
201 // Fully render parent if it's not a repost.
202 if !boards.IsRepost(post) {
203 parentID := post.ParentID
204 parent := thread
205
206 if thread.ID != parentID {
207 parent, _ = getReply(thread, parentID)
208 }
209
210 s += renderPost(parent, "" , "" , 0 , &budget, false ) + "\n"
211 }
212
213 // Pass the reply's own path so renderPost routes to renderTopLevelReplies
214 // and paginates this comment's direct replies (the re-root has its own
215 // ?page= — no collision with the thread view, which is a different path).
216 // desc=false: order is derived from path by renderTopLevelReplies.
217 s += renderPost(post, path, "> " , 5 , &budget, false )
218 return s
219 }
220
221 func renderSourcePost(post *boards.Post, indent string , budget * int ) ( string , *boards.Post) {
222 if !boards.IsRepost(post) {
223 return "" , nil
224 }
225
226 indent += "> "
227
228 // TODO: figure out a way to decouple posts from a global storage.
229 board, ok := gBoards.Get(post.OriginalBoardID)
230 if !ok {
231 // TODO: Boards can't be deleted so this might be redundant
232 return indentBody(indent, "⚠ Source board has been deleted" ), nil
233 }
234
235 srcPost, ok := getThread(board, post.ParentID)
236 if !ok {
237 return indentBody(indent, "⚠ Source post has been deleted" ), nil
238 }
239
240 if srcPost.Hidden {
241 return indentBody(indent, "⚠ Source post has been flagged as inappropriate" ), nil
242 }
243
244 return indentForeignBody(indent, srcPost.Body, budget) + "\n\n" , srcPost
245 }
246
247 func renderFlagPost(res *mux.ResponseWriter, req *mux.Request) {
248 name := req.GetVar( "board" )
249 board, found := gBoards.GetByName(name)
250 if !found {
251 res.Write( "Board not found" )
252 return
253 }
254
255 // Thread ID must always be available
256 rawID := req.GetVar( "thread" )
257 threadID, err := strconv.Atoi(rawID)
258 if err != nil {
259 res.Write( "Invalid thread ID: " + md.EscapeText(rawID))
260 return
261 }
262
263 thread, found := getThread(board, boards.ID(threadID))
264 if !found {
265 res.Write( "Thread not found" )
266 return
267 }
268
269 // Parse reply ID when post is a reply
270 var reply *boards.Post
271 rawID = req.GetVar( "reply" )
272 isReply := rawID != ""
273 if isReply {
274 replyID, err := strconv.Atoi(rawID)
275 if err != nil {
276 res.Write( "Invalid reply ID: " + md.EscapeText(rawID))
277 return
278 }
279
280 reply, _ = getReply(thread, boards.ID(replyID))
281 if reply == nil {
282 res.Write( "Reply not found" )
283 return
284 }
285 }
286
287 exec := "FlagThread"
288 if isReply {
289 exec = "FlagReply"
290 }
291
292 form := mdform.New( "exec" , exec)
293 form.Input(
294 "boardID" ,
295 "placeholder" , "Board ID" ,
296 "value" , board.ID.String(),
297 "readonly" , "true" ,
298 )
299 form.Input(
300 "threadID" ,
301 "placeholder" , "Thread ID" ,
302 "value" , thread.ID.String(),
303 "readonly" , "true" ,
304 )
305
306 if isReply {
307 form.Input(
308 "replyID" ,
309 "placeholder" , "Reply ID" ,
310 "value" , reply.ID.String(),
311 "readonly" , "true" ,
312 )
313 }
314
315 form.Input(
316 "reason" ,
317 "placeholder" , "Flagging Reason" ,
318 )
319
320 // Breadcrumb navigation
321 backLink := md.Link( "← Back to thread" , makeThreadURI(thread))
322
323 if isReply {
324 res.Write(md.H1(board.Name + ": Flag Comment" ))
325 } else {
326 res.Write(md.H1(board.Name + ": Flag Thread" ))
327 }
328 res.Write(backLink + "\n\n" )
329
330 res.Write(
331 md.Paragraph(
332 "Thread or comment moderation is done through flagging, which is usually done " +
333 "by board members with the moderator role, though other roles could also potentially flag." ,
334 ) +
335 md.Paragraph(
336 "Flagging relies on a configurable threshold, which by default is of one flag, that when " +
337 "reached leads to the flagged thread or comment to be hidden." ,
338 ) +
339 md.Paragraph(
340 "Flagging thresholds can be different within each board." ,
341 ),
342 )
343
344 if isReply {
345 res.Write(
346 md.Paragraph(
347 ufmt.Sprintf(
348 "⚠ You are flagging a %s from %s ⚠" ,
349 md.Link( "comment" , makeReplyURI(reply)),
350 userLink(reply.Creator),
351 ),
352 ),
353 )
354 } else {
355 res.Write(
356 md.Paragraph(
357 ufmt.Sprintf(
358 "⚠ You are flagging the thread: %s ⚠" ,
359 md.Link(thread.Title, makeThreadURI(thread)),
360 ),
361 ),
362 )
363 }
364
365 res.Write(form.String())
366 res.Write( "\n\n**Done?** " + svgbtn.ButtonWithRadius( 136 , 32 , 4 , "#E2E2E2" , "#54595D" , "Return to thread" , makeThreadURI(thread)) + "\n" )
367 }
368
369 func renderFlaggingReasonsPost(res *mux.ResponseWriter, req *mux.Request) {
370 name := req.GetVar( "board" )
371 board, found := gBoards.GetByName(name)
372 if !found {
373 res.Write( "Board not found" )
374 return
375 }
376
377 // Thread ID must always be available
378 rawID := req.GetVar( "thread" )
379 threadID, err := strconv.Atoi(rawID)
380 if err != nil {
381 res.Write( "Invalid thread ID: " + md.EscapeText(rawID))
382 return
383 }
384
385 thread, found := getThread(board, boards.ID(threadID))
386 if !found {
387 res.Write( "Thread not found" )
388 return
389 }
390
391 flags := thread.Flags
392
393 // Parse reply ID when post is a reply
394 var reply *boards.Post
395 rawID = req.GetVar( "reply" )
396 isReply := rawID != ""
397 if isReply {
398 replyID, err := strconv.Atoi(rawID)
399 if err != nil {
400 res.Write( "Invalid reply ID: " + md.EscapeText(rawID))
401 return
402 }
403
404 reply, found = getReply(thread, boards.ID(replyID))
405 if !found {
406 res.Write( "Reply not found" )
407 return
408 }
409
410 flags = reply.Flags
411 }
412
413 table := mdtable.Table{
414 Headers: [] string { "Moderator" , "Reason" },
415 }
416
417 flags.Iterate( 0 , flags.Size(), func (f boards.Flag) bool {
418 // f.Reason is user-supplied (only trimmed at write); escape it so a
419 // flag reason can't inject markdown (links/images) or HTML into the
420 // reasons table. md.EscapeText leaves '|' for mdtable to escape.
421 table.Append([] string {userLink(f.User), md.EscapeText(f.Reason)})
422 return false
423 })
424
425 // Breadcrumb navigation
426 backLink := md.Link( "← Back to thread" , makeThreadURI(thread))
427
428 res.Write(md.H1( "Flagging Reasons" ))
429 res.Write(backLink + "\n\n" )
430 if isReply {
431 res.Write(
432 md.Paragraph(
433 ufmt.Sprintf(
434 "Moderation flags for a %s submitted by %s" ,
435 md.Link( "comment" , makeReplyURI(reply)),
436 userLink(reply.Creator),
437 ),
438 ),
439 )
440 } else {
441 res.Write(
442 md.Paragraph(
443 // Intentionally hide flagged thread title
444 ufmt.Sprintf( "Moderation flags for %s" , md.Link( "thread" , makeThreadURI(thread))),
445 ),
446 )
447 }
448 res.Write(table.String())
449 }
450
451 func renderReplyPost(res *mux.ResponseWriter, req *mux.Request) {
452 name := req.GetVar( "board" )
453 board, found := gBoards.GetByName(name)
454 if !found {
455 res.Write( "Board not found" )
456 return
457 }
458
459 // Thread ID must always be available
460 rawID := req.GetVar( "thread" )
461 threadID, err := strconv.Atoi(rawID)
462 if err != nil {
463 res.Write( "Invalid thread ID: " + md.EscapeText(rawID))
464 return
465 }
466
467 thread, found := board.Threads.Get(boards.ID(threadID))
468 if !found {
469 res.Write( "Thread not found" )
470 return
471 }
472
473 // Parse reply ID when post is a reply
474 var reply *boards.Post
475 rawID = req.GetVar( "reply" )
476 isReply := rawID != ""
477 if isReply {
478 replyID, err := strconv.Atoi(rawID)
479 if err != nil {
480 res.Write( "Invalid reply ID: " + md.EscapeText(rawID))
481 return
482 }
483
484 reply, _ = getReply(thread, boards.ID(replyID))
485 if reply == nil {
486 res.Write( "Reply not found" )
487 return
488 }
489 }
490
491 form := mdform.New( "exec" , "CreateReply" )
492 form.Input(
493 "boardID" ,
494 "placeholder" , "Board ID" ,
495 "value" , board.ID.String(),
496 "readonly" , "true" ,
497 )
498 form.Input(
499 "threadID" ,
500 "placeholder" , "Thread ID" ,
501 "value" , thread.ID.String(),
502 "readonly" , "true" ,
503 )
504
505 if isReply {
506 form.Input(
507 "replyID" ,
508 "placeholder" , "Reply ID" ,
509 "value" , reply.ID.String(),
510 "readonly" , "true" ,
511 )
512 } else {
513 form.Input(
514 "replyID" ,
515 "placeholder" , "Reply ID" ,
516 "value" , "0" ,
517 "readonly" , "true" ,
518 )
519 }
520
521 form.Textarea(
522 "body" ,
523 "placeholder" , "Comment" ,
524 "required" , "true" ,
525 )
526
527 // Breadcrumb navigation
528 backLink := md.Link( "← Back to thread" , makeThreadURI(thread))
529
530 if isReply {
531 res.Write(md.H1(board.Name + ": Reply" ))
532 res.Write(backLink + "\n\n" )
533 res.Write(
534 md.Paragraph(ufmt.Sprintf( "Replying to a comment posted by %s:" , userLink(reply.Creator))) +
535 foreign.ForeignWithLabel( "Quoted comment" , reply.Body),
536 )
537 } else {
538 res.Write(md.H1(board.Name + ": Comment" ))
539 res.Write(backLink + "\n\n" )
540 res.Write(
541 md.Paragraph(
542 ufmt.Sprintf( "Commenting on the thread: %s" , md.Link(thread.Title, makeThreadURI(thread))),
543 ),
544 )
545 }
546
547 res.Write(form.String())
548 res.Write( "\n\n**Done?** " + svgbtn.ButtonWithRadius( 136 , 32 , 4 , "#E2E2E2" , "#54595D" , "Return to thread" , makeThreadURI(thread)) + "\n" )
549 }
550