Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}