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