Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}