render.gno
9.53 Kb · 386 lines
1package boards2
2
3import (
4 "net/url"
5 "strconv"
6 "strings"
7 "time"
8
9 "gno.land/p/gnoland/boards"
10 "gno.land/p/jeronimoalbi/mdform"
11 "gno.land/p/jeronimoalbi/pager"
12 "gno.land/p/leon/svgbtn"
13 "gno.land/p/moul/md"
14 "gno.land/p/moul/mdtable"
15 "gno.land/p/nt/mux"
16)
17
18const (
19 pageSizeDefault = 6
20 pageSizeReplies = 10
21)
22
23const menuManageBoard = "manageBoard"
24
25var (
26 createBoardURI = gRealmPath + ":create-board"
27 adminUsersURI = gRealmPath + ":admin-users"
28 helpURI = gRealmPath + ":help"
29)
30
31func Render(path string) string {
32 var (
33 b strings.Builder
34 router = mux.NewRouter()
35 )
36
37 router.HandleFunc("", renderBoardsList)
38 router.HandleFunc("help", renderHelp)
39 router.HandleFunc("admin-users", renderMembers)
40 router.HandleFunc("create-board", renderCreateBoard)
41 router.HandleFunc("{board}", renderBoard)
42 router.HandleFunc("{board}/members", renderMembers)
43 router.HandleFunc("{board}/invites", renderInvites)
44 router.HandleFunc("{board}/banned-users", renderBannedUsers)
45 router.HandleFunc("{board}/create-thread", renderCreateThread)
46 router.HandleFunc("{board}/invite-member", renderInviteMember)
47 router.HandleFunc("{board}/{thread}", renderThread)
48 router.HandleFunc("{board}/{thread}/flag", renderFlagPost)
49 router.HandleFunc("{board}/{thread}/reply", renderReplyPost)
50 router.HandleFunc("{board}/{thread}/edit", renderEditThread)
51 router.HandleFunc("{board}/{thread}/repost", renderRepostThread)
52 router.HandleFunc("{board}/{thread}/{reply}", renderReply)
53 router.HandleFunc("{board}/{thread}/{reply}/flag", renderFlagPost)
54 router.HandleFunc("{board}/{thread}/{reply}/reply", renderReplyPost)
55 router.HandleFunc("{board}/{thread}/{reply}/edit", renderEditReply)
56
57 router.NotFoundHandler = func(res *mux.ResponseWriter, _ *mux.Request) {
58 res.Write(md.Blockquote("Path not found"))
59 }
60
61 // Render common realm header before resolving render path
62 if gNotice != "" {
63 b.WriteString(infoAlert("Notice", gNotice))
64 }
65
66 // Render view for current path
67 b.WriteString(router.Render(path))
68
69 return b.String()
70}
71
72func renderHelp(res *mux.ResponseWriter, _ *mux.Request) {
73 res.Write(md.H1("Boards Help"))
74 if gHelp != "" {
75 res.Write(gHelp)
76 return
77 }
78
79 link := gRealmLink.Call("SetHelp", "content", "")
80 res.Write(md.H3("Help content has not been uploaded"))
81 res.Write("Do you want to " + md.Link("upload boards help", link) + " ?")
82}
83
84func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) {
85 res.Write(md.H1("Boards"))
86 renderBoardListMenu(res, req)
87 res.Write(md.HorizontalRule())
88
89 if gListedBoardsByID.Size() == 0 {
90 res.Write(md.H3("Currently there are no boards"))
91 res.Write("Be the first to " + md.Link("create a new board", createBoardURI) + " !")
92 return
93 }
94
95 p, err := pager.New(req.RawPath, gListedBoardsByID.Size(), pager.WithPageSize(pageSizeDefault))
96 if err != nil {
97 panic(err)
98 }
99
100 render := func(_ string, v any) bool {
101 board := v.(*boards.Board)
102 userLink := userLink(board.Creator)
103 date := board.CreatedAt.Format(dateFormat)
104
105 res.Write(md.H6(md.Link(board.Name, makeBoardURI(board))))
106 res.Write("Created by " + userLink + " on " + date + ", #" + board.ID.String() + " \n")
107
108 status := strconv.Itoa(board.Threads.Size()) + " threads"
109 if board.Readonly {
110 status += ", read-only"
111 }
112
113 res.Write(md.Bold(status) + "\n\n")
114 return false
115 }
116
117 res.Write("Sort by: ")
118 r := parseRealmPath(req.RawPath)
119 if r.Query.Get("order") == "desc" {
120 r.Query.Set("order", "asc")
121 res.Write(md.Link("newest first", r.String()) + "\n\n")
122 gListedBoardsByID.ReverseIterateByOffset(p.Offset(), p.PageSize(), render)
123 } else {
124 r.Query.Set("order", "desc")
125 res.Write(md.Link("oldest first", r.String()) + "\n\n")
126 gListedBoardsByID.IterateByOffset(p.Offset(), p.PageSize(), render)
127 }
128
129 if p.HasPages() {
130 res.Write(md.HorizontalRule())
131 res.Write(pager.Picker(p))
132 }
133}
134
135func renderBoardListMenu(res *mux.ResponseWriter, req *mux.Request) {
136 res.Write(md.Link("Create Board", createBoardURI))
137 res.Write(" • ")
138 res.Write(md.Link("List Admin Users", adminUsersURI))
139 res.Write(" • ")
140 res.Write(md.Link("Help", helpURI))
141 res.Write("\n\n")
142}
143
144func renderCreateBoard(res *mux.ResponseWriter, _ *mux.Request) {
145 form := mdform.New("exec", "CreateBoard")
146 form.Input(
147 "name",
148 "placeholder", "Board name",
149 "required", "true",
150 )
151 form.Radio(
152 "listed",
153 "true",
154 "checked", "true",
155 "description", "Should board be publically listed?",
156 )
157 form.Radio(
158 "listed",
159 "false",
160 )
161 form.Radio(
162 "open",
163 "true",
164 "description", "Should anyone be allowed to create threads and comments?",
165 )
166 form.Radio(
167 "open",
168 "false",
169 "checked", "true",
170 )
171
172 res.Write(md.H1("Boards: Create Board"))
173 res.Write(md.Link("← Back to boards", gRealmPath) + "\n\n")
174 res.Write(
175 md.Paragraph(
176 "Boards are by default listed by the realm but they can optionally " +
177 "be created so they are only found by their URL.",
178 ),
179 )
180 res.Write(
181 md.Paragraph(
182 "They can also be created to be open so anyone is allowed to create " +
183 "new threads and also to comment on any thread within the open board.",
184 ),
185 )
186 res.Write(form.String())
187 res.Write("\n\n**Done?** " + svgbtn.ButtonWithRadius(136, 32, 4, "#E2E2E2", "#54595D", "Return to boards", gRealmPath) + "\n")
188}
189
190func renderMembers(res *mux.ResponseWriter, req *mux.Request) {
191 boardID := boards.ID(0)
192 perms := gPerms
193 name := req.GetVar("board")
194 if name != "" {
195 board, found := gBoards.GetByName(name)
196 if !found {
197 res.Write(md.H3("Board not found"))
198 return
199 }
200
201 boardID = board.ID
202 perms = board.Permissions
203
204 res.Write(md.H1(board.Name + " Members"))
205 res.Write(md.H3("These are the board members"))
206 } else {
207 res.Write(md.H1("Admin Users"))
208 res.Write(md.H3("These are the admin users of the realm"))
209 }
210
211 // Create a pager with a small page size to reduce
212 // the number of username lookups per page.
213 p, err := pager.New(req.RawPath, perms.UsersCount(), pager.WithPageSize(pageSizeDefault))
214 if err != nil {
215 res.Write(err.Error())
216 return
217 }
218
219 table := mdtable.Table{
220 Headers: []string{"Member", "Role", "Actions"},
221 }
222
223 perms.IterateUsers(p.Offset(), p.PageSize(), func(u boards.User) bool {
224 actions := []string{
225 md.Link("remove", gRealmLink.Call(
226 "RemoveMember",
227 "boardID", boardID.String(),
228 "member", u.Address.String(),
229 )),
230 md.Link("change role", gRealmLink.Call(
231 "ChangeMemberRole",
232 "boardID", boardID.String(),
233 "member", u.Address.String(),
234 "role", "",
235 )),
236 }
237
238 table.Append([]string{
239 userLink(u.Address),
240 rolesToString(u.Roles),
241 strings.Join(actions, " • "),
242 })
243 return false
244 })
245 res.Write(table.String())
246
247 if p.HasPages() {
248 res.Write("\n" + pager.Picker(p))
249 }
250}
251
252func renderInvites(res *mux.ResponseWriter, req *mux.Request) {
253 name := req.GetVar("board")
254 board, found := gBoards.GetByName(name)
255 if !found {
256 res.Write(md.H3("Board not found"))
257 return
258 }
259
260 res.Write(md.H1(board.Name + " Invite Requests"))
261
262 requests, found := getInviteRequests(board.ID)
263 if !found || requests.Size() == 0 {
264 res.Write(md.H3("Board has no invite requests"))
265 return
266 }
267
268 p, err := pager.New(req.RawPath, requests.Size(), pager.WithPageSize(pageSizeDefault))
269 if err != nil {
270 res.Write(err.Error())
271 return
272 }
273
274 table := mdtable.Table{
275 Headers: []string{"User", "Request Date", "Actions"},
276 }
277
278 res.Write(md.H3("These users have requested to be invited to the board"))
279 requests.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
280 actions := []string{
281 md.Link("accept", gRealmLink.Call(
282 "AcceptInvite",
283 "boardID", board.ID.String(),
284 "user", addr,
285 )),
286 md.Link("revoke", gRealmLink.Call(
287 "RevokeInvite",
288 "boardID", board.ID.String(),
289 "user", addr,
290 )),
291 }
292
293 table.Append([]string{
294 userLink(address(addr)),
295 v.(time.Time).Format(dateFormat),
296 strings.Join(actions, " • "),
297 })
298 return false
299 })
300
301 res.Write(table.String())
302
303 if p.HasPages() {
304 res.Write("\n" + pager.Picker(p))
305 }
306}
307
308func renderBannedUsers(res *mux.ResponseWriter, req *mux.Request) {
309 name := req.GetVar("board")
310 board, found := gBoards.GetByName(name)
311 if !found {
312 res.Write(md.H3("Board not found"))
313 return
314 }
315
316 res.Write(md.H1(board.Name + " Banned Users"))
317
318 banned, found := getBannedUsers(board.ID)
319 if !found || banned.Size() == 0 {
320 res.Write(md.H3("Board has no banned users"))
321 return
322 }
323
324 p, err := pager.New(req.RawPath, banned.Size(), pager.WithPageSize(pageSizeDefault))
325 if err != nil {
326 res.Write(err.Error())
327 return
328 }
329
330 table := mdtable.Table{
331 Headers: []string{"User", "Banned Until", "Actions"},
332 }
333
334 res.Write(md.H3("These users have been banned from the board"))
335 banned.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
336 table.Append([]string{
337 userLink(address(addr)),
338 v.(time.Time).Format(dateFormat),
339 md.Link("unban", gRealmLink.Call(
340 "Unban",
341 "boardID", board.ID.String(),
342 "user", addr,
343 "reason", "",
344 )),
345 })
346 return false
347 })
348
349 res.Write(table.String())
350
351 if p.HasPages() {
352 res.Write("\n" + pager.Picker(p))
353 }
354}
355
356func infoAlert(title, msg string) string {
357 header := strings.TrimSpace("[!INFO] " + title)
358 return md.Blockquote(header + "\n" + msg)
359}
360
361func rolesToString(roles []boards.Role) string {
362 if len(roles) == 0 {
363 return ""
364 }
365
366 names := make([]string, len(roles))
367 for i, r := range roles {
368 names[i] = string(r)
369 }
370 return strings.Join(names, ", ")
371}
372
373func menuURL(name string) string {
374 // TODO: Menu URL works because no other GET arguments are being used
375 return "?menu=" + name
376}
377
378func getCurrentMenu(rawURL string) string {
379 _, rawQuery, found := strings.Cut(rawURL, "?")
380 if !found {
381 return ""
382 }
383
384 query, _ := url.ParseQuery(rawQuery)
385 return query.Get("menu")
386}