render.gno

10.92 Kb · 452 lines
  1package boards2
  2
  3import (
  4	"net/url"
  5	"strconv"
  6	"strings"
  7	"time"
  8
  9	"gno.land/p/jeronimoalbi/pager"
 10	"gno.land/p/moul/md"
 11	"gno.land/p/moul/mdtable"
 12	"gno.land/p/nt/mux"
 13)
 14
 15const (
 16	pageSizeDefault = 6
 17	pageSizeReplies = 10
 18)
 19
 20const menuManageBoard = "manageBoard"
 21
 22func Render(path string) string {
 23	router := mux.NewRouter()
 24	router.HandleFunc("", renderBoardsList)
 25	router.HandleFunc("help", renderHelp)
 26	router.HandleFunc("admin-users", renderMembers)
 27	router.HandleFunc("{board}", renderBoard)
 28	router.HandleFunc("{board}/members", renderMembers)
 29	router.HandleFunc("{board}/invites", renderInvites)
 30	router.HandleFunc("{board}/banned-users", renderBannedUsers)
 31	router.HandleFunc("{board}/{thread}", renderThread)
 32	router.HandleFunc("{board}/{thread}/{reply}", renderReply)
 33
 34	router.NotFoundHandler = func(res *mux.ResponseWriter, _ *mux.Request) {
 35		res.Write("Path not found")
 36	}
 37
 38	return router.Render(path)
 39}
 40
 41func renderHelp(res *mux.ResponseWriter, _ *mux.Request) {
 42	res.Write(md.H1("Boards Help"))
 43	if gHelp != "" {
 44		res.Write(gHelp)
 45		return
 46	}
 47
 48	link := gRealmLink.Call("SetHelp", "content", "")
 49	res.Write(md.H3("Help content has not been uploaded"))
 50	res.Write("Do you want to " + md.Link("upload boards help", link) + " ?")
 51}
 52
 53func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) {
 54	renderNotice(res)
 55
 56	res.Write(md.H1("Boards"))
 57	renderBoardListMenu(res, req)
 58	res.Write(md.HorizontalRule())
 59
 60	boards := gListedBoardsByID
 61	if boards.Size() == 0 {
 62		link := gRealmLink.Call("CreateBoard", "name", "", "listed", "true")
 63		res.Write(md.H3("Currently there are no boards"))
 64		res.Write("Be the first to " + md.Link("create a new board", link) + " !")
 65		return
 66	}
 67
 68	p, err := pager.New(req.RawPath, boards.Size(), pager.WithPageSize(pageSizeDefault))
 69	if err != nil {
 70		panic(err)
 71	}
 72
 73	render := func(_ string, v any) bool {
 74		board := v.(*Board)
 75		userLink := md.UserLink(board.Creator.String())
 76		date := board.CreatedAt().Format(dateFormat)
 77
 78		res.Write(md.Bold(md.Link(board.Name, makeBoardURI(board))) + "  \n")
 79		res.Write("Created by " + userLink + " on " + date + ", #" + board.ID.String() + "  \n")
 80
 81		status := strconv.Itoa(board.ThreadsCount()) + " threads"
 82		if board.Readonly {
 83			status += ", read-only"
 84		}
 85
 86		res.Write(md.Bold(status) + "\n\n")
 87		return false
 88	}
 89
 90	res.Write("Sort by: ")
 91	r := parseRealmPath(req.RawPath)
 92	if r.Query.Get("order") == "desc" {
 93		r.Query.Set("order", "asc")
 94		res.Write(md.Link("newest first", r.String()) + "\n\n")
 95		boards.ReverseIterateByOffset(p.Offset(), p.PageSize(), render)
 96	} else {
 97		r.Query.Set("order", "desc")
 98		res.Write(md.Link("oldest first", r.String()) + "\n\n")
 99		boards.IterateByOffset(p.Offset(), p.PageSize(), render)
100	}
101
102	if p.HasPages() {
103		res.Write(md.HorizontalRule())
104		res.Write(pager.Picker(p))
105	}
106}
107
108func renderBoardListMenu(res *mux.ResponseWriter, req *mux.Request) {
109	path := strings.TrimPrefix(string(gRealmLink), "gno.land")
110
111	res.Write(md.Link("Create Board", gRealmLink.Call("CreateBoard", "name", "", "listed", "true")))
112	res.Write(" • ")
113	res.Write(md.Link("List Admin Users", path+":admin-users"))
114	res.Write(" • ")
115	res.Write(md.Link("Help", path+":help"))
116	res.Write("\n\n")
117}
118
119func renderBoard(res *mux.ResponseWriter, req *mux.Request) {
120	renderNotice(res)
121
122	name := req.GetVar("board")
123	v, found := gBoardsByName.Get(name)
124	if !found {
125		link := md.Link("create a new board", gRealmLink.Call("CreateBoard", "name", name, "listed", "true"))
126		res.Write(md.H3("The board you are looking for does not exist"))
127		res.Write("Do you want to " + link + " ?")
128		return
129	}
130
131	board := v.(*Board)
132	menu := renderBoardMenu(board, req)
133
134	res.Write(board.Render(req.RawPath, menu))
135}
136
137func renderBoardMenu(board *Board, req *mux.Request) string {
138	var (
139		b               strings.Builder
140		boardMembersURL = makeBoardURI(board) + "/members"
141	)
142
143	if board.Readonly {
144		b.WriteString(md.Link("List Members", boardMembersURL))
145		b.WriteString(" • ")
146		b.WriteString(md.Link("Unfreeze Board", makeUnfreezeBoardURI(board)))
147		b.WriteString("\n")
148	} else {
149		b.WriteString(md.Link("Create Thread", makeCreateThreadURI(board)))
150		b.WriteString(" • ")
151		b.WriteString(md.Link("Request Invite", makeRequestInviteURI(board)))
152		b.WriteString(" • ")
153
154		menu := getCurrentMenu(req.RawPath)
155		if menu == menuManageBoard {
156			b.WriteString(md.Bold("Manage Board"))
157		} else {
158			b.WriteString(md.Link("Manage Board", menuURL(menuManageBoard)))
159		}
160
161		b.WriteString("  \n")
162
163		if menu == menuManageBoard {
164			b.WriteString("↳")
165			b.WriteString(md.Link("Invite Member", makeInviteMemberURI(board)))
166			b.WriteString(" • ")
167			b.WriteString(md.Link("List Invite Requests", makeBoardURI(board)+"/invites"))
168			b.WriteString(" • ")
169			b.WriteString(md.Link("List Members", boardMembersURL))
170			b.WriteString(" • ")
171			b.WriteString(md.Link("List Banned Users", makeBoardURI(board)+"/banned-users"))
172			b.WriteString(" • ")
173			b.WriteString(md.Link("Freeze Board", makeFreezeBoardURI(board)))
174			b.WriteString("\n")
175		}
176	}
177
178	return b.String()
179}
180
181func renderThread(res *mux.ResponseWriter, req *mux.Request) {
182	renderNotice(res)
183
184	name := req.GetVar("board")
185	v, found := gBoardsByName.Get(name)
186	if !found {
187		res.Write("Board does not exist: " + name)
188		return
189	}
190
191	rawID := req.GetVar("thread")
192	tID, err := strconv.Atoi(rawID)
193	if err != nil {
194		res.Write("Invalid thread ID: " + rawID)
195		return
196	}
197
198	board := v.(*Board)
199	thread, found := board.GetThread(PostID(tID))
200	if !found {
201		res.Write("Thread does not exist with ID: " + rawID)
202	} else if thread.Hidden {
203		res.Write("Thread with ID: " + rawID + " has been flagged as inappropriate")
204	} else {
205		res.Write(thread.Render(req.RawPath, "", 5))
206	}
207}
208
209func renderReply(res *mux.ResponseWriter, req *mux.Request) {
210	renderNotice(res)
211
212	name := req.GetVar("board")
213	v, found := gBoardsByName.Get(name)
214	if !found {
215		res.Write("Board does not exist: " + name)
216		return
217	}
218
219	rawID := req.GetVar("thread")
220	tID, err := strconv.Atoi(rawID)
221	if err != nil {
222		res.Write("Invalid thread ID: " + rawID)
223		return
224	}
225
226	rawID = req.GetVar("reply")
227	rID, err := strconv.Atoi(rawID)
228	if err != nil {
229		res.Write("Invalid reply ID: " + rawID)
230		return
231	}
232
233	board := v.(*Board)
234	thread, found := board.GetThread(PostID(tID))
235	if !found {
236		res.Write("Thread does not exist with ID: " + req.GetVar("thread"))
237		return
238	}
239
240	reply, found := thread.GetReply(PostID(rID))
241	if !found {
242		res.Write("Reply does not exist with ID: " + rawID)
243		return
244	}
245
246	// Call render even for hidden replies to display children.
247	// Original comment content will be hidden under the hood.
248	// See: #3480
249	res.Write(reply.RenderInner())
250}
251
252func renderMembers(res *mux.ResponseWriter, req *mux.Request) {
253	boardID := BoardID(0)
254	perms := gPerms
255	name := req.GetVar("board")
256	if name != "" {
257		v, found := gBoardsByName.Get(name)
258		if !found {
259			res.Write(md.H3("Board not found"))
260			return
261		}
262
263		board := v.(*Board)
264		boardID = board.ID
265		perms = board.perms
266
267		res.Write(md.H1(board.Name + " Members"))
268		res.Write(md.H3("These are the board members"))
269	} else {
270		res.Write(md.H1("Admin Users"))
271		res.Write(md.H3("These are the admin users of the realm"))
272	}
273
274	// Create a pager with a small page size to reduce
275	// the number of username lookups per page.
276	p, err := pager.New(req.RawPath, perms.UsersCount(), pager.WithPageSize(pageSizeDefault))
277	if err != nil {
278		res.Write(err.Error())
279		return
280	}
281
282	table := mdtable.Table{
283		Headers: []string{"Member", "Role", "Actions"},
284	}
285
286	perms.IterateUsers(p.Offset(), p.PageSize(), func(u User) bool {
287		actions := []string{
288			md.Link("remove", gRealmLink.Call(
289				"RemoveMember",
290				"boardID", boardID.String(),
291				"member", u.Address.String(),
292			)),
293			md.Link("change role", gRealmLink.Call(
294				"ChangeMemberRole",
295				"boardID", boardID.String(),
296				"member", u.Address.String(),
297				"role", "",
298			)),
299		}
300
301		table.Append([]string{
302			md.UserLink(u.Address.String()),
303			rolesToString(u.Roles),
304			strings.Join(actions, " • "),
305		})
306		return false
307	})
308	res.Write(table.String())
309
310	if p.HasPages() {
311		res.Write("\n" + pager.Picker(p))
312	}
313}
314
315func renderInvites(res *mux.ResponseWriter, req *mux.Request) {
316	name := req.GetVar("board")
317	v, found := gBoardsByName.Get(name)
318	if !found {
319		res.Write(md.H3("Board not found"))
320		return
321	}
322
323	board := v.(*Board)
324	res.Write(md.H1(board.Name + " Invite Requests"))
325
326	requests, found := getInviteRequests(board.ID)
327	if !found || requests.Size() == 0 {
328		res.Write(md.H3("Board has no invite requests"))
329		return
330	}
331
332	p, err := pager.New(req.RawPath, requests.Size(), pager.WithPageSize(pageSizeDefault))
333	if err != nil {
334		res.Write(err.Error())
335		return
336	}
337
338	table := mdtable.Table{
339		Headers: []string{"User", "Request Date", "Actions"},
340	}
341
342	res.Write(md.H3("These users have requested to be invited to the board"))
343	requests.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
344		actions := []string{
345			md.Link("accept", gRealmLink.Call(
346				"AcceptInvite",
347				"boardID", board.ID.String(),
348				"user", addr,
349			)),
350			md.Link("revoke", gRealmLink.Call(
351				"RevokeInvite",
352				"boardID", board.ID.String(),
353				"user", addr,
354			)),
355		}
356
357		table.Append([]string{
358			md.UserLink(addr),
359			v.(time.Time).Format(dateFormat),
360			strings.Join(actions, " • "),
361		})
362		return false
363	})
364
365	res.Write(table.String())
366
367	if p.HasPages() {
368		res.Write("\n" + pager.Picker(p))
369	}
370}
371
372func renderBannedUsers(res *mux.ResponseWriter, req *mux.Request) {
373	name := req.GetVar("board")
374	v, found := gBoardsByName.Get(name)
375	if !found {
376		res.Write(md.H3("Board not found"))
377		return
378	}
379
380	board := v.(*Board)
381	res.Write(md.H1(board.Name + " Banned Users"))
382
383	banned, found := getBannedUsers(board.ID)
384	if !found || banned.Size() == 0 {
385		res.Write(md.H3("Board has no banned users"))
386		return
387	}
388
389	p, err := pager.New(req.RawPath, banned.Size(), pager.WithPageSize(pageSizeDefault))
390	if err != nil {
391		res.Write(err.Error())
392		return
393	}
394
395	table := mdtable.Table{
396		Headers: []string{"User", "Banned Until", "Actions"},
397	}
398
399	res.Write(md.H3("These users have been banned from the board"))
400	banned.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
401		table.Append([]string{
402			md.UserLink(addr),
403			v.(time.Time).Format(dateFormat),
404			md.Link("unban", gRealmLink.Call(
405				"Unban",
406				"boardID", board.ID.String(),
407				"user", addr,
408				"reason", "",
409			)),
410		})
411		return false
412	})
413
414	res.Write(table.String())
415
416	if p.HasPages() {
417		res.Write("\n" + pager.Picker(p))
418	}
419}
420
421func renderNotice(res *mux.ResponseWriter) {
422	if gNotice != "" {
423		res.Write(md.Blockquote(gNotice))
424	}
425}
426
427func rolesToString(roles []Role) string {
428	if len(roles) == 0 {
429		return ""
430	}
431
432	names := make([]string, len(roles))
433	for i, r := range roles {
434		names[i] = string(r)
435	}
436	return strings.Join(names, ", ")
437}
438
439func menuURL(name string) string {
440	// TODO: Menu URL works because no other GET arguments are being used
441	return "?menu=" + name
442}
443
444func getCurrentMenu(rawURL string) string {
445	_, rawQuery, found := strings.Cut(rawURL, "?")
446	if !found {
447		return ""
448	}
449
450	query, _ := url.ParseQuery(rawQuery)
451	return query.Get("menu")
452}