render.gno
10.98 Kb · 448 lines
1package boards2
2
3import (
4 "net/url"
5 "strconv"
6 "strings"
7 "time"
8
9 "gno.land/p/jeronimoalbi/pager"
10 "gno.land/p/moul/md"
11 "gno.land/p/moul/mdtable"
12 "gno.land/p/nt/mux"
13)
14
15const (
16 pageSizeDefault = 6
17 pageSizeReplies = 10
18)
19
20const menuManageBoard = "manageBoard"
21
22func Render(path string) string {
23 var (
24 b strings.Builder
25 router = mux.NewRouter()
26 )
27
28 router.HandleFunc("", renderBoardsList)
29 router.HandleFunc("help", renderHelp)
30 router.HandleFunc("admin-users", renderMembers)
31 router.HandleFunc("{board}", renderBoard)
32 router.HandleFunc("{board}/members", renderMembers)
33 router.HandleFunc("{board}/invites", renderInvites)
34 router.HandleFunc("{board}/banned-users", renderBannedUsers)
35 router.HandleFunc("{board}/{thread}", renderThread)
36 router.HandleFunc("{board}/{thread}/{reply}", renderReply)
37
38 router.NotFoundHandler = func(res *mux.ResponseWriter, _ *mux.Request) {
39 res.Write(md.Blockquote("Path not found"))
40 }
41
42 // Render common realm header before resolving render path
43 if gNotice != "" {
44 b.WriteString(infoAlert("Notice", gNotice))
45 }
46
47 // Render view for current path
48 b.WriteString(router.Render(path))
49
50 return b.String()
51}
52
53func renderHelp(res *mux.ResponseWriter, _ *mux.Request) {
54 res.Write(md.H1("Boards Help"))
55 if gHelp != "" {
56 res.Write(gHelp)
57 return
58 }
59
60 link := gRealmLink.Call("SetHelp", "content", "")
61 res.Write(md.H3("Help content has not been uploaded"))
62 res.Write("Do you want to " + md.Link("upload boards help", link) + " ?")
63}
64
65func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) {
66 res.Write(md.H1("Boards"))
67 renderBoardListMenu(res, req)
68 res.Write(md.HorizontalRule())
69
70 boards := gListedBoardsByID
71 if boards.Size() == 0 {
72 link := gRealmLink.Call("CreateBoard", "name", "", "listed", "true")
73 res.Write(md.H3("Currently there are no boards"))
74 res.Write("Be the first to " + md.Link("create a new board", link) + " !")
75 return
76 }
77
78 p, err := pager.New(req.RawPath, boards.Size(), pager.WithPageSize(pageSizeDefault))
79 if err != nil {
80 panic(err)
81 }
82
83 render := func(_ string, v any) bool {
84 board := v.(*Board)
85 userLink := userLink(board.Creator)
86 date := board.CreatedAt().Format(dateFormat)
87
88 res.Write(md.Bold(md.Link(board.Name, makeBoardURI(board))) + " \n")
89 res.Write("Created by " + userLink + " on " + date + ", #" + board.ID.String() + " \n")
90
91 status := strconv.Itoa(board.ThreadsCount()) + " threads"
92 if board.Readonly {
93 status += ", read-only"
94 }
95
96 res.Write(md.Bold(status) + "\n\n")
97 return false
98 }
99
100 res.Write("Sort by: ")
101 r := parseRealmPath(req.RawPath)
102 if r.Query.Get("order") == "desc" {
103 r.Query.Set("order", "asc")
104 res.Write(md.Link("newest first", r.String()) + "\n\n")
105 boards.ReverseIterateByOffset(p.Offset(), p.PageSize(), render)
106 } else {
107 r.Query.Set("order", "desc")
108 res.Write(md.Link("oldest first", r.String()) + "\n\n")
109 boards.IterateByOffset(p.Offset(), p.PageSize(), render)
110 }
111
112 if p.HasPages() {
113 res.Write(md.HorizontalRule())
114 res.Write(pager.Picker(p))
115 }
116}
117
118func renderBoardListMenu(res *mux.ResponseWriter, req *mux.Request) {
119 path := strings.TrimPrefix(string(gRealmLink), "gno.land")
120
121 res.Write(md.Link("Create Board", gRealmLink.Call("CreateBoard", "name", "", "listed", "true")))
122 res.Write(" • ")
123 res.Write(md.Link("List Admin Users", path+":admin-users"))
124 res.Write(" • ")
125 res.Write(md.Link("Help", path+":help"))
126 res.Write("\n\n")
127}
128
129func renderBoard(res *mux.ResponseWriter, req *mux.Request) {
130 name := req.GetVar("board")
131 board, found := getBoardByName(name)
132 if !found {
133 link := md.Link("create a new board", gRealmLink.Call("CreateBoard", "name", name, "listed", "true"))
134 res.Write(md.H3("The board you are looking for does not exist"))
135 res.Write("Do you want to " + link + " ?")
136 return
137 }
138
139 menu := renderBoardMenu(board, req)
140 res.Write(board.Render(req.RawPath, menu))
141}
142
143func renderBoardMenu(board *Board, req *mux.Request) string {
144 var (
145 b strings.Builder
146 boardMembersURL = makeBoardURI(board) + "/members"
147 )
148
149 if board.Readonly {
150 b.WriteString(md.Link("List Members", boardMembersURL))
151 b.WriteString(" • ")
152 b.WriteString(md.Link("Unfreeze Board", makeUnfreezeBoardURI(board)))
153 b.WriteString("\n")
154 } else {
155 b.WriteString(md.Link("Create Thread", makeCreateThreadURI(board)))
156 b.WriteString(" • ")
157 b.WriteString(md.Link("Request Invite", makeRequestInviteURI(board)))
158 b.WriteString(" • ")
159
160 menu := getCurrentMenu(req.RawPath)
161 if menu == menuManageBoard {
162 b.WriteString(md.Bold("Manage Board"))
163 } else {
164 b.WriteString(md.Link("Manage Board", menuURL(menuManageBoard)))
165 }
166
167 b.WriteString(" \n")
168
169 if menu == menuManageBoard {
170 b.WriteString("↳")
171 b.WriteString(md.Link("Invite Member", makeInviteMemberURI(board)))
172 b.WriteString(" • ")
173 b.WriteString(md.Link("List Invite Requests", makeBoardURI(board)+"/invites"))
174 b.WriteString(" • ")
175 b.WriteString(md.Link("List Members", boardMembersURL))
176 b.WriteString(" • ")
177 b.WriteString(md.Link("List Banned Users", makeBoardURI(board)+"/banned-users"))
178 b.WriteString(" • ")
179 b.WriteString(md.Link("Freeze Board", makeFreezeBoardURI(board)))
180 b.WriteString("\n")
181 }
182 }
183
184 return b.String()
185}
186
187func renderThread(res *mux.ResponseWriter, req *mux.Request) {
188 name := req.GetVar("board")
189 board, found := getBoardByName(name)
190 if !found {
191 res.Write("Board does not exist: " + name)
192 return
193 }
194
195 rawID := req.GetVar("thread")
196 tID, err := strconv.Atoi(rawID)
197 if err != nil {
198 res.Write("Invalid thread ID: " + rawID)
199 return
200 }
201
202 thread, found := board.GetThread(PostID(tID))
203 if !found {
204 res.Write("Thread does not exist with ID: " + rawID)
205 } else if thread.Hidden {
206 res.Write("Thread with ID: " + rawID + " has been flagged as inappropriate")
207 } else {
208 res.Write(thread.Render(req.RawPath, "", 5))
209 }
210}
211
212func renderReply(res *mux.ResponseWriter, req *mux.Request) {
213 name := req.GetVar("board")
214 board, found := getBoardByName(name)
215 if !found {
216 res.Write("Board does not exist: " + name)
217 return
218 }
219
220 rawID := req.GetVar("thread")
221 tID, err := strconv.Atoi(rawID)
222 if err != nil {
223 res.Write("Invalid thread ID: " + rawID)
224 return
225 }
226
227 rawID = req.GetVar("reply")
228 rID, err := strconv.Atoi(rawID)
229 if err != nil {
230 res.Write("Invalid reply ID: " + rawID)
231 return
232 }
233
234 thread, found := board.GetThread(PostID(tID))
235 if !found {
236 res.Write("Thread does not exist with ID: " + req.GetVar("thread"))
237 return
238 }
239
240 reply, found := thread.GetReply(PostID(rID))
241 if !found {
242 res.Write("Reply does not exist with ID: " + rawID)
243 return
244 }
245
246 // Call render even for hidden replies to display children.
247 // Original comment content will be hidden under the hood.
248 // See: #3480
249 res.Write(reply.RenderInner())
250}
251
252func renderMembers(res *mux.ResponseWriter, req *mux.Request) {
253 boardID := BoardID(0)
254 perms := gPerms
255 name := req.GetVar("board")
256 if name != "" {
257 board, found := getBoardByName(name)
258 if !found {
259 res.Write(md.H3("Board not found"))
260 return
261 }
262
263 boardID = board.ID
264 perms = board.perms
265
266 res.Write(md.H1(board.Name + " Members"))
267 res.Write(md.H3("These are the board members"))
268 } else {
269 res.Write(md.H1("Admin Users"))
270 res.Write(md.H3("These are the admin users of the realm"))
271 }
272
273 // Create a pager with a small page size to reduce
274 // the number of username lookups per page.
275 p, err := pager.New(req.RawPath, perms.UsersCount(), pager.WithPageSize(pageSizeDefault))
276 if err != nil {
277 res.Write(err.Error())
278 return
279 }
280
281 table := mdtable.Table{
282 Headers: []string{"Member", "Role", "Actions"},
283 }
284
285 perms.IterateUsers(p.Offset(), p.PageSize(), func(u User) bool {
286 actions := []string{
287 md.Link("remove", gRealmLink.Call(
288 "RemoveMember",
289 "boardID", boardID.String(),
290 "member", u.Address.String(),
291 )),
292 md.Link("change role", gRealmLink.Call(
293 "ChangeMemberRole",
294 "boardID", boardID.String(),
295 "member", u.Address.String(),
296 "role", "",
297 )),
298 }
299
300 table.Append([]string{
301 userLink(u.Address),
302 rolesToString(u.Roles),
303 strings.Join(actions, " • "),
304 })
305 return false
306 })
307 res.Write(table.String())
308
309 if p.HasPages() {
310 res.Write("\n" + pager.Picker(p))
311 }
312}
313
314func renderInvites(res *mux.ResponseWriter, req *mux.Request) {
315 name := req.GetVar("board")
316 board, found := getBoardByName(name)
317 if !found {
318 res.Write(md.H3("Board not found"))
319 return
320 }
321
322 res.Write(md.H1(board.Name + " Invite Requests"))
323
324 requests, found := getInviteRequests(board.ID)
325 if !found || requests.Size() == 0 {
326 res.Write(md.H3("Board has no invite requests"))
327 return
328 }
329
330 p, err := pager.New(req.RawPath, requests.Size(), pager.WithPageSize(pageSizeDefault))
331 if err != nil {
332 res.Write(err.Error())
333 return
334 }
335
336 table := mdtable.Table{
337 Headers: []string{"User", "Request Date", "Actions"},
338 }
339
340 res.Write(md.H3("These users have requested to be invited to the board"))
341 requests.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
342 actions := []string{
343 md.Link("accept", gRealmLink.Call(
344 "AcceptInvite",
345 "boardID", board.ID.String(),
346 "user", addr,
347 )),
348 md.Link("revoke", gRealmLink.Call(
349 "RevokeInvite",
350 "boardID", board.ID.String(),
351 "user", addr,
352 )),
353 }
354
355 table.Append([]string{
356 userLink(address(addr)),
357 v.(time.Time).Format(dateFormat),
358 strings.Join(actions, " • "),
359 })
360 return false
361 })
362
363 res.Write(table.String())
364
365 if p.HasPages() {
366 res.Write("\n" + pager.Picker(p))
367 }
368}
369
370func renderBannedUsers(res *mux.ResponseWriter, req *mux.Request) {
371 name := req.GetVar("board")
372 board, found := getBoardByName(name)
373 if !found {
374 res.Write(md.H3("Board not found"))
375 return
376 }
377
378 res.Write(md.H1(board.Name + " Banned Users"))
379
380 banned, found := getBannedUsers(board.ID)
381 if !found || banned.Size() == 0 {
382 res.Write(md.H3("Board has no banned users"))
383 return
384 }
385
386 p, err := pager.New(req.RawPath, banned.Size(), pager.WithPageSize(pageSizeDefault))
387 if err != nil {
388 res.Write(err.Error())
389 return
390 }
391
392 table := mdtable.Table{
393 Headers: []string{"User", "Banned Until", "Actions"},
394 }
395
396 res.Write(md.H3("These users have been banned from the board"))
397 banned.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
398 table.Append([]string{
399 userLink(address(addr)),
400 v.(time.Time).Format(dateFormat),
401 md.Link("unban", gRealmLink.Call(
402 "Unban",
403 "boardID", board.ID.String(),
404 "user", addr,
405 "reason", "",
406 )),
407 })
408 return false
409 })
410
411 res.Write(table.String())
412
413 if p.HasPages() {
414 res.Write("\n" + pager.Picker(p))
415 }
416}
417
418func infoAlert(title, msg string) string {
419 header := strings.TrimSpace("[!INFO] " + title)
420 return md.Blockquote(header + "\n" + msg)
421}
422
423func rolesToString(roles []Role) string {
424 if len(roles) == 0 {
425 return ""
426 }
427
428 names := make([]string, len(roles))
429 for i, r := range roles {
430 names[i] = string(r)
431 }
432 return strings.Join(names, ", ")
433}
434
435func menuURL(name string) string {
436 // TODO: Menu URL works because no other GET arguments are being used
437 return "?menu=" + name
438}
439
440func getCurrentMenu(rawURL string) string {
441 _, rawQuery, found := strings.Cut(rawURL, "?")
442 if !found {
443 return ""
444 }
445
446 query, _ := url.ParseQuery(rawQuery)
447 return query.Get("menu")
448}