render.gno
10.92 Kb · 452 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 b.WriteString(md.Link("Request Invite", makeRequestInviteURI(board)))
152 b.WriteString(" • ")
153
154 menu := getCurrentMenu(req.RawPath)
155 if menu == menuManageBoard {
156 b.WriteString(md.Bold("Manage Board"))
157 } else {
158 b.WriteString(md.Link("Manage Board", menuURL(menuManageBoard)))
159 }
160
161 b.WriteString(" \n")
162
163 if menu == menuManageBoard {
164 b.WriteString("↳")
165 b.WriteString(md.Link("Invite Member", makeInviteMemberURI(board)))
166 b.WriteString(" • ")
167 b.WriteString(md.Link("List Invite Requests", makeBoardURI(board)+"/invites"))
168 b.WriteString(" • ")
169 b.WriteString(md.Link("List Members", boardMembersURL))
170 b.WriteString(" • ")
171 b.WriteString(md.Link("List Banned Users", makeBoardURI(board)+"/banned-users"))
172 b.WriteString(" • ")
173 b.WriteString(md.Link("Freeze Board", makeFreezeBoardURI(board)))
174 b.WriteString("\n")
175 }
176 }
177
178 return b.String()
179}
180
181func renderThread(res *mux.ResponseWriter, req *mux.Request) {
182 renderNotice(res)
183
184 name := req.GetVar("board")
185 v, found := gBoardsByName.Get(name)
186 if !found {
187 res.Write("Board does not exist: " + name)
188 return
189 }
190
191 rawID := req.GetVar("thread")
192 tID, err := strconv.Atoi(rawID)
193 if err != nil {
194 res.Write("Invalid thread ID: " + rawID)
195 return
196 }
197
198 board := v.(*Board)
199 thread, found := board.GetThread(PostID(tID))
200 if !found {
201 res.Write("Thread does not exist with ID: " + rawID)
202 } else if thread.Hidden {
203 res.Write("Thread with ID: " + rawID + " has been flagged as inappropriate")
204 } else {
205 res.Write(thread.Render(req.RawPath, "", 5))
206 }
207}
208
209func renderReply(res *mux.ResponseWriter, req *mux.Request) {
210 renderNotice(res)
211
212 name := req.GetVar("board")
213 v, found := gBoardsByName.Get(name)
214 if !found {
215 res.Write("Board does not exist: " + name)
216 return
217 }
218
219 rawID := req.GetVar("thread")
220 tID, err := strconv.Atoi(rawID)
221 if err != nil {
222 res.Write("Invalid thread ID: " + rawID)
223 return
224 }
225
226 rawID = req.GetVar("reply")
227 rID, err := strconv.Atoi(rawID)
228 if err != nil {
229 res.Write("Invalid reply ID: " + rawID)
230 return
231 }
232
233 board := v.(*Board)
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 v, found := gBoardsByName.Get(name)
258 if !found {
259 res.Write(md.H3("Board not found"))
260 return
261 }
262
263 board := v.(*Board)
264 boardID = board.ID
265 perms = board.perms
266
267 res.Write(md.H1(board.Name + " Members"))
268 res.Write(md.H3("These are the board members"))
269 } else {
270 res.Write(md.H1("Admin Users"))
271 res.Write(md.H3("These are the admin users of the realm"))
272 }
273
274 // Create a pager with a small page size to reduce
275 // the number of username lookups per page.
276 p, err := pager.New(req.RawPath, perms.UsersCount(), pager.WithPageSize(pageSizeDefault))
277 if err != nil {
278 res.Write(err.Error())
279 return
280 }
281
282 table := mdtable.Table{
283 Headers: []string{"Member", "Role", "Actions"},
284 }
285
286 perms.IterateUsers(p.Offset(), p.PageSize(), func(u User) bool {
287 actions := []string{
288 md.Link("remove", gRealmLink.Call(
289 "RemoveMember",
290 "boardID", boardID.String(),
291 "member", u.Address.String(),
292 )),
293 md.Link("change role", gRealmLink.Call(
294 "ChangeMemberRole",
295 "boardID", boardID.String(),
296 "member", u.Address.String(),
297 "role", "",
298 )),
299 }
300
301 table.Append([]string{
302 md.UserLink(u.Address.String()),
303 rolesToString(u.Roles),
304 strings.Join(actions, " • "),
305 })
306 return false
307 })
308 res.Write(table.String())
309
310 if p.HasPages() {
311 res.Write("\n" + pager.Picker(p))
312 }
313}
314
315func renderInvites(res *mux.ResponseWriter, req *mux.Request) {
316 name := req.GetVar("board")
317 v, found := gBoardsByName.Get(name)
318 if !found {
319 res.Write(md.H3("Board not found"))
320 return
321 }
322
323 board := v.(*Board)
324 res.Write(md.H1(board.Name + " Invite Requests"))
325
326 requests, found := getInviteRequests(board.ID)
327 if !found || requests.Size() == 0 {
328 res.Write(md.H3("Board has no invite requests"))
329 return
330 }
331
332 p, err := pager.New(req.RawPath, requests.Size(), pager.WithPageSize(pageSizeDefault))
333 if err != nil {
334 res.Write(err.Error())
335 return
336 }
337
338 table := mdtable.Table{
339 Headers: []string{"User", "Request Date", "Actions"},
340 }
341
342 res.Write(md.H3("These users have requested to be invited to the board"))
343 requests.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
344 actions := []string{
345 md.Link("accept", gRealmLink.Call(
346 "AcceptInvite",
347 "boardID", board.ID.String(),
348 "user", addr,
349 )),
350 md.Link("revoke", gRealmLink.Call(
351 "RevokeInvite",
352 "boardID", board.ID.String(),
353 "user", addr,
354 )),
355 }
356
357 table.Append([]string{
358 md.UserLink(addr),
359 v.(time.Time).Format(dateFormat),
360 strings.Join(actions, " • "),
361 })
362 return false
363 })
364
365 res.Write(table.String())
366
367 if p.HasPages() {
368 res.Write("\n" + pager.Picker(p))
369 }
370}
371
372func renderBannedUsers(res *mux.ResponseWriter, req *mux.Request) {
373 name := req.GetVar("board")
374 v, found := gBoardsByName.Get(name)
375 if !found {
376 res.Write(md.H3("Board not found"))
377 return
378 }
379
380 board := v.(*Board)
381 res.Write(md.H1(board.Name + " Banned Users"))
382
383 banned, found := getBannedUsers(board.ID)
384 if !found || banned.Size() == 0 {
385 res.Write(md.H3("Board has no banned users"))
386 return
387 }
388
389 p, err := pager.New(req.RawPath, banned.Size(), pager.WithPageSize(pageSizeDefault))
390 if err != nil {
391 res.Write(err.Error())
392 return
393 }
394
395 table := mdtable.Table{
396 Headers: []string{"User", "Banned Until", "Actions"},
397 }
398
399 res.Write(md.H3("These users have been banned from the board"))
400 banned.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
401 table.Append([]string{
402 md.UserLink(addr),
403 v.(time.Time).Format(dateFormat),
404 md.Link("unban", gRealmLink.Call(
405 "Unban",
406 "boardID", board.ID.String(),
407 "user", addr,
408 "reason", "",
409 )),
410 })
411 return false
412 })
413
414 res.Write(table.String())
415
416 if p.HasPages() {
417 res.Write("\n" + pager.Picker(p))
418 }
419}
420
421func renderNotice(res *mux.ResponseWriter) {
422 if gNotice != "" {
423 res.Write(md.Blockquote(gNotice))
424 }
425}
426
427func rolesToString(roles []Role) string {
428 if len(roles) == 0 {
429 return ""
430 }
431
432 names := make([]string, len(roles))
433 for i, r := range roles {
434 names[i] = string(r)
435 }
436 return strings.Join(names, ", ")
437}
438
439func menuURL(name string) string {
440 // TODO: Menu URL works because no other GET arguments are being used
441 return "?menu=" + name
442}
443
444func getCurrentMenu(rawURL string) string {
445 _, rawQuery, found := strings.Cut(rawURL, "?")
446 if !found {
447 return ""
448 }
449
450 query, _ := url.ParseQuery(rawQuery)
451 return query.Get("menu")
452}