render.gno
9.17 Kb · 371 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/pager"
11 "gno.land/p/moul/md"
12 "gno.land/p/moul/mdtable"
13 "gno.land/p/nt/mux"
14)
15
16const (
17 pageSizeDefault = 6
18 pageSizeReplies = 10
19)
20
21const menuManageBoard = "manageBoard"
22
23func Render(path string) string {
24 var (
25 b strings.Builder
26 router = mux.NewRouter()
27 )
28
29 router.HandleFunc("", renderBoardsList)
30 router.HandleFunc("help", renderHelp)
31 router.HandleFunc("admin-users", renderMembers)
32 router.HandleFunc("{board}", renderBoard)
33 router.HandleFunc("{board}/members", renderMembers)
34 router.HandleFunc("{board}/invites", renderInvites)
35 router.HandleFunc("{board}/banned-users", renderBannedUsers)
36 router.HandleFunc("{board}/{thread}", renderThread)
37 router.HandleFunc("{board}/{thread}/{reply}", renderReply)
38
39 router.NotFoundHandler = func(res *mux.ResponseWriter, _ *mux.Request) {
40 res.Write(md.Blockquote("Path not found"))
41 }
42
43 // Render common realm header before resolving render path
44 if gNotice != "" {
45 b.WriteString(infoAlert("Notice", gNotice))
46 }
47
48 // Render view for current path
49 b.WriteString(router.Render(path))
50
51 return b.String()
52}
53
54func renderHelp(res *mux.ResponseWriter, _ *mux.Request) {
55 res.Write(md.H1("Boards Help"))
56 if gHelp != "" {
57 res.Write(gHelp)
58 return
59 }
60
61 link := gRealmLink.Call("SetHelp", "content", "")
62 res.Write(md.H3("Help content has not been uploaded"))
63 res.Write("Do you want to " + md.Link("upload boards help", link) + " ?")
64}
65
66func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) {
67 res.Write(md.H1("Boards"))
68 renderBoardListMenu(res, req)
69 res.Write(md.HorizontalRule())
70
71 if gListedBoardsByID.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, gListedBoardsByID.Size(), pager.WithPageSize(pageSizeDefault))
79 if err != nil {
80 panic(err)
81 }
82
83 render := func(_ string, v any) bool {
84 board := v.(*boards.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.Threads.Size()) + " 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 gListedBoardsByID.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 gListedBoardsByID.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 renderBoardMenu(board *boards.Board, req *mux.Request) string {
130 var (
131 b strings.Builder
132 boardMembersURL = makeBoardURI(board) + "/members"
133 )
134
135 b.WriteString("\n")
136 if board.Readonly {
137 b.WriteString(md.Link("List Members", boardMembersURL))
138 b.WriteString(" • ")
139 b.WriteString(md.Link("Unfreeze Board", makeUnfreezeBoardURI(board)))
140 b.WriteString("\n")
141 } else {
142 b.WriteString(md.Link("Create Thread", makeCreateThreadURI(board)))
143 b.WriteString(" • ")
144 b.WriteString(md.Link("Request Invite", makeRequestInviteURI(board)))
145 b.WriteString(" • ")
146
147 menu := getCurrentMenu(req.RawPath)
148 if menu == menuManageBoard {
149 b.WriteString(md.Bold("Manage Board"))
150 } else {
151 b.WriteString(md.Link("Manage Board", menuURL(menuManageBoard)))
152 }
153
154 b.WriteString(" \n")
155
156 if menu == menuManageBoard {
157 b.WriteString("↳")
158 b.WriteString(md.Link("Invite Member", makeInviteMemberURI(board)))
159 b.WriteString(" • ")
160 b.WriteString(md.Link("List Invite Requests", makeBoardURI(board)+"/invites"))
161 b.WriteString(" • ")
162 b.WriteString(md.Link("List Members", boardMembersURL))
163 b.WriteString(" • ")
164 b.WriteString(md.Link("List Banned Users", makeBoardURI(board)+"/banned-users"))
165 b.WriteString(" • ")
166 b.WriteString(md.Link("Freeze Board", makeFreezeBoardURI(board)))
167 b.WriteString("\n")
168 }
169 }
170
171 b.WriteString("\n")
172 return b.String()
173}
174
175func renderMembers(res *mux.ResponseWriter, req *mux.Request) {
176 boardID := boards.ID(0)
177 perms := gPerms
178 name := req.GetVar("board")
179 if name != "" {
180 board, found := gBoards.GetByName(name)
181 if !found {
182 res.Write(md.H3("Board not found"))
183 return
184 }
185
186 boardID = board.ID
187 perms = board.Permissions
188
189 res.Write(md.H1(board.Name + " Members"))
190 res.Write(md.H3("These are the board members"))
191 } else {
192 res.Write(md.H1("Admin Users"))
193 res.Write(md.H3("These are the admin users of the realm"))
194 }
195
196 // Create a pager with a small page size to reduce
197 // the number of username lookups per page.
198 p, err := pager.New(req.RawPath, perms.UsersCount(), pager.WithPageSize(pageSizeDefault))
199 if err != nil {
200 res.Write(err.Error())
201 return
202 }
203
204 table := mdtable.Table{
205 Headers: []string{"Member", "Role", "Actions"},
206 }
207
208 perms.IterateUsers(p.Offset(), p.PageSize(), func(u boards.User) bool {
209 actions := []string{
210 md.Link("remove", gRealmLink.Call(
211 "RemoveMember",
212 "boardID", boardID.String(),
213 "member", u.Address.String(),
214 )),
215 md.Link("change role", gRealmLink.Call(
216 "ChangeMemberRole",
217 "boardID", boardID.String(),
218 "member", u.Address.String(),
219 "role", "",
220 )),
221 }
222
223 table.Append([]string{
224 userLink(u.Address),
225 rolesToString(u.Roles),
226 strings.Join(actions, " • "),
227 })
228 return false
229 })
230 res.Write(table.String())
231
232 if p.HasPages() {
233 res.Write("\n" + pager.Picker(p))
234 }
235}
236
237func renderInvites(res *mux.ResponseWriter, req *mux.Request) {
238 name := req.GetVar("board")
239 board, found := gBoards.GetByName(name)
240 if !found {
241 res.Write(md.H3("Board not found"))
242 return
243 }
244
245 res.Write(md.H1(board.Name + " Invite Requests"))
246
247 requests, found := getInviteRequests(board.ID)
248 if !found || requests.Size() == 0 {
249 res.Write(md.H3("Board has no invite requests"))
250 return
251 }
252
253 p, err := pager.New(req.RawPath, requests.Size(), pager.WithPageSize(pageSizeDefault))
254 if err != nil {
255 res.Write(err.Error())
256 return
257 }
258
259 table := mdtable.Table{
260 Headers: []string{"User", "Request Date", "Actions"},
261 }
262
263 res.Write(md.H3("These users have requested to be invited to the board"))
264 requests.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
265 actions := []string{
266 md.Link("accept", gRealmLink.Call(
267 "AcceptInvite",
268 "boardID", board.ID.String(),
269 "user", addr,
270 )),
271 md.Link("revoke", gRealmLink.Call(
272 "RevokeInvite",
273 "boardID", board.ID.String(),
274 "user", addr,
275 )),
276 }
277
278 table.Append([]string{
279 userLink(address(addr)),
280 v.(time.Time).Format(dateFormat),
281 strings.Join(actions, " • "),
282 })
283 return false
284 })
285
286 res.Write(table.String())
287
288 if p.HasPages() {
289 res.Write("\n" + pager.Picker(p))
290 }
291}
292
293func renderBannedUsers(res *mux.ResponseWriter, req *mux.Request) {
294 name := req.GetVar("board")
295 board, found := gBoards.GetByName(name)
296 if !found {
297 res.Write(md.H3("Board not found"))
298 return
299 }
300
301 res.Write(md.H1(board.Name + " Banned Users"))
302
303 banned, found := getBannedUsers(board.ID)
304 if !found || banned.Size() == 0 {
305 res.Write(md.H3("Board has no banned users"))
306 return
307 }
308
309 p, err := pager.New(req.RawPath, banned.Size(), pager.WithPageSize(pageSizeDefault))
310 if err != nil {
311 res.Write(err.Error())
312 return
313 }
314
315 table := mdtable.Table{
316 Headers: []string{"User", "Banned Until", "Actions"},
317 }
318
319 res.Write(md.H3("These users have been banned from the board"))
320 banned.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
321 table.Append([]string{
322 userLink(address(addr)),
323 v.(time.Time).Format(dateFormat),
324 md.Link("unban", gRealmLink.Call(
325 "Unban",
326 "boardID", board.ID.String(),
327 "user", addr,
328 "reason", "",
329 )),
330 })
331 return false
332 })
333
334 res.Write(table.String())
335
336 if p.HasPages() {
337 res.Write("\n" + pager.Picker(p))
338 }
339}
340
341func infoAlert(title, msg string) string {
342 header := strings.TrimSpace("[!INFO] " + title)
343 return md.Blockquote(header + "\n" + msg)
344}
345
346func rolesToString(roles []boards.Role) string {
347 if len(roles) == 0 {
348 return ""
349 }
350
351 names := make([]string, len(roles))
352 for i, r := range roles {
353 names[i] = string(r)
354 }
355 return strings.Join(names, ", ")
356}
357
358func menuURL(name string) string {
359 // TODO: Menu URL works because no other GET arguments are being used
360 return "?menu=" + name
361}
362
363func getCurrentMenu(rawURL string) string {
364 _, rawQuery, found := strings.Cut(rawURL, "?")
365 if !found {
366 return ""
367 }
368
369 query, _ := url.ParseQuery(rawQuery)
370 return query.Get("menu")
371}