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