Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

10.98 Kb · 448 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	var (
 24		b      strings.Builder
 25		router = mux.NewRouter()
 26	)
 27
 28	router.HandleFunc("", renderBoardsList)
 29	router.HandleFunc("help", renderHelp)
 30	router.HandleFunc("admin-users", renderMembers)
 31	router.HandleFunc("{board}", renderBoard)
 32	router.HandleFunc("{board}/members", renderMembers)
 33	router.HandleFunc("{board}/invites", renderInvites)
 34	router.HandleFunc("{board}/banned-users", renderBannedUsers)
 35	router.HandleFunc("{board}/{thread}", renderThread)
 36	router.HandleFunc("{board}/{thread}/{reply}", renderReply)
 37
 38	router.NotFoundHandler = func(res *mux.ResponseWriter, _ *mux.Request) {
 39		res.Write(md.Blockquote("Path not found"))
 40	}
 41
 42	// Render common realm header before resolving render path
 43	if gNotice != "" {
 44		b.WriteString(infoAlert("Notice", gNotice))
 45	}
 46
 47	// Render view for current path
 48	b.WriteString(router.Render(path))
 49
 50	return b.String()
 51}
 52
 53func renderHelp(res *mux.ResponseWriter, _ *mux.Request) {
 54	res.Write(md.H1("Boards Help"))
 55	if gHelp != "" {
 56		res.Write(gHelp)
 57		return
 58	}
 59
 60	link := gRealmLink.Call("SetHelp", "content", "")
 61	res.Write(md.H3("Help content has not been uploaded"))
 62	res.Write("Do you want to " + md.Link("upload boards help", link) + " ?")
 63}
 64
 65func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) {
 66	res.Write(md.H1("Boards"))
 67	renderBoardListMenu(res, req)
 68	res.Write(md.HorizontalRule())
 69
 70	boards := gListedBoardsByID
 71	if boards.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, boards.Size(), pager.WithPageSize(pageSizeDefault))
 79	if err != nil {
 80		panic(err)
 81	}
 82
 83	render := func(_ string, v any) bool {
 84		board := v.(*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.ThreadsCount()) + " 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		boards.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		boards.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 renderBoard(res *mux.ResponseWriter, req *mux.Request) {
130	name := req.GetVar("board")
131	board, found := getBoardByName(name)
132	if !found {
133		link := md.Link("create a new board", gRealmLink.Call("CreateBoard", "name", name, "listed", "true"))
134		res.Write(md.H3("The board you are looking for does not exist"))
135		res.Write("Do you want to " + link + " ?")
136		return
137	}
138
139	menu := renderBoardMenu(board, req)
140	res.Write(board.Render(req.RawPath, menu))
141}
142
143func renderBoardMenu(board *Board, req *mux.Request) string {
144	var (
145		b               strings.Builder
146		boardMembersURL = makeBoardURI(board) + "/members"
147	)
148
149	if board.Readonly {
150		b.WriteString(md.Link("List Members", boardMembersURL))
151		b.WriteString(" • ")
152		b.WriteString(md.Link("Unfreeze Board", makeUnfreezeBoardURI(board)))
153		b.WriteString("\n")
154	} else {
155		b.WriteString(md.Link("Create Thread", makeCreateThreadURI(board)))
156		b.WriteString(" • ")
157		b.WriteString(md.Link("Request Invite", makeRequestInviteURI(board)))
158		b.WriteString(" • ")
159
160		menu := getCurrentMenu(req.RawPath)
161		if menu == menuManageBoard {
162			b.WriteString(md.Bold("Manage Board"))
163		} else {
164			b.WriteString(md.Link("Manage Board", menuURL(menuManageBoard)))
165		}
166
167		b.WriteString("  \n")
168
169		if menu == menuManageBoard {
170			b.WriteString("↳")
171			b.WriteString(md.Link("Invite Member", makeInviteMemberURI(board)))
172			b.WriteString(" • ")
173			b.WriteString(md.Link("List Invite Requests", makeBoardURI(board)+"/invites"))
174			b.WriteString(" • ")
175			b.WriteString(md.Link("List Members", boardMembersURL))
176			b.WriteString(" • ")
177			b.WriteString(md.Link("List Banned Users", makeBoardURI(board)+"/banned-users"))
178			b.WriteString(" • ")
179			b.WriteString(md.Link("Freeze Board", makeFreezeBoardURI(board)))
180			b.WriteString("\n")
181		}
182	}
183
184	return b.String()
185}
186
187func renderThread(res *mux.ResponseWriter, req *mux.Request) {
188	name := req.GetVar("board")
189	board, found := getBoardByName(name)
190	if !found {
191		res.Write("Board does not exist: " + name)
192		return
193	}
194
195	rawID := req.GetVar("thread")
196	tID, err := strconv.Atoi(rawID)
197	if err != nil {
198		res.Write("Invalid thread ID: " + rawID)
199		return
200	}
201
202	thread, found := board.GetThread(PostID(tID))
203	if !found {
204		res.Write("Thread does not exist with ID: " + rawID)
205	} else if thread.Hidden {
206		res.Write("Thread with ID: " + rawID + " has been flagged as inappropriate")
207	} else {
208		res.Write(thread.Render(req.RawPath, "", 5))
209	}
210}
211
212func renderReply(res *mux.ResponseWriter, req *mux.Request) {
213	name := req.GetVar("board")
214	board, found := getBoardByName(name)
215	if !found {
216		res.Write("Board does not exist: " + name)
217		return
218	}
219
220	rawID := req.GetVar("thread")
221	tID, err := strconv.Atoi(rawID)
222	if err != nil {
223		res.Write("Invalid thread ID: " + rawID)
224		return
225	}
226
227	rawID = req.GetVar("reply")
228	rID, err := strconv.Atoi(rawID)
229	if err != nil {
230		res.Write("Invalid reply ID: " + rawID)
231		return
232	}
233
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		board, found := getBoardByName(name)
258		if !found {
259			res.Write(md.H3("Board not found"))
260			return
261		}
262
263		boardID = board.ID
264		perms = board.perms
265
266		res.Write(md.H1(board.Name + " Members"))
267		res.Write(md.H3("These are the board members"))
268	} else {
269		res.Write(md.H1("Admin Users"))
270		res.Write(md.H3("These are the admin users of the realm"))
271	}
272
273	// Create a pager with a small page size to reduce
274	// the number of username lookups per page.
275	p, err := pager.New(req.RawPath, perms.UsersCount(), pager.WithPageSize(pageSizeDefault))
276	if err != nil {
277		res.Write(err.Error())
278		return
279	}
280
281	table := mdtable.Table{
282		Headers: []string{"Member", "Role", "Actions"},
283	}
284
285	perms.IterateUsers(p.Offset(), p.PageSize(), func(u User) bool {
286		actions := []string{
287			md.Link("remove", gRealmLink.Call(
288				"RemoveMember",
289				"boardID", boardID.String(),
290				"member", u.Address.String(),
291			)),
292			md.Link("change role", gRealmLink.Call(
293				"ChangeMemberRole",
294				"boardID", boardID.String(),
295				"member", u.Address.String(),
296				"role", "",
297			)),
298		}
299
300		table.Append([]string{
301			userLink(u.Address),
302			rolesToString(u.Roles),
303			strings.Join(actions, " • "),
304		})
305		return false
306	})
307	res.Write(table.String())
308
309	if p.HasPages() {
310		res.Write("\n" + pager.Picker(p))
311	}
312}
313
314func renderInvites(res *mux.ResponseWriter, req *mux.Request) {
315	name := req.GetVar("board")
316	board, found := getBoardByName(name)
317	if !found {
318		res.Write(md.H3("Board not found"))
319		return
320	}
321
322	res.Write(md.H1(board.Name + " Invite Requests"))
323
324	requests, found := getInviteRequests(board.ID)
325	if !found || requests.Size() == 0 {
326		res.Write(md.H3("Board has no invite requests"))
327		return
328	}
329
330	p, err := pager.New(req.RawPath, requests.Size(), pager.WithPageSize(pageSizeDefault))
331	if err != nil {
332		res.Write(err.Error())
333		return
334	}
335
336	table := mdtable.Table{
337		Headers: []string{"User", "Request Date", "Actions"},
338	}
339
340	res.Write(md.H3("These users have requested to be invited to the board"))
341	requests.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
342		actions := []string{
343			md.Link("accept", gRealmLink.Call(
344				"AcceptInvite",
345				"boardID", board.ID.String(),
346				"user", addr,
347			)),
348			md.Link("revoke", gRealmLink.Call(
349				"RevokeInvite",
350				"boardID", board.ID.String(),
351				"user", addr,
352			)),
353		}
354
355		table.Append([]string{
356			userLink(address(addr)),
357			v.(time.Time).Format(dateFormat),
358			strings.Join(actions, " • "),
359		})
360		return false
361	})
362
363	res.Write(table.String())
364
365	if p.HasPages() {
366		res.Write("\n" + pager.Picker(p))
367	}
368}
369
370func renderBannedUsers(res *mux.ResponseWriter, req *mux.Request) {
371	name := req.GetVar("board")
372	board, found := getBoardByName(name)
373	if !found {
374		res.Write(md.H3("Board not found"))
375		return
376	}
377
378	res.Write(md.H1(board.Name + " Banned Users"))
379
380	banned, found := getBannedUsers(board.ID)
381	if !found || banned.Size() == 0 {
382		res.Write(md.H3("Board has no banned users"))
383		return
384	}
385
386	p, err := pager.New(req.RawPath, banned.Size(), pager.WithPageSize(pageSizeDefault))
387	if err != nil {
388		res.Write(err.Error())
389		return
390	}
391
392	table := mdtable.Table{
393		Headers: []string{"User", "Banned Until", "Actions"},
394	}
395
396	res.Write(md.H3("These users have been banned from the board"))
397	banned.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
398		table.Append([]string{
399			userLink(address(addr)),
400			v.(time.Time).Format(dateFormat),
401			md.Link("unban", gRealmLink.Call(
402				"Unban",
403				"boardID", board.ID.String(),
404				"user", addr,
405				"reason", "",
406			)),
407		})
408		return false
409	})
410
411	res.Write(table.String())
412
413	if p.HasPages() {
414		res.Write("\n" + pager.Picker(p))
415	}
416}
417
418func infoAlert(title, msg string) string {
419	header := strings.TrimSpace("[!INFO] " + title)
420	return md.Blockquote(header + "\n" + msg)
421}
422
423func rolesToString(roles []Role) string {
424	if len(roles) == 0 {
425		return ""
426	}
427
428	names := make([]string, len(roles))
429	for i, r := range roles {
430		names[i] = string(r)
431	}
432	return strings.Join(names, ", ")
433}
434
435func menuURL(name string) string {
436	// TODO: Menu URL works because no other GET arguments are being used
437	return "?menu=" + name
438}
439
440func getCurrentMenu(rawURL string) string {
441	_, rawQuery, found := strings.Cut(rawURL, "?")
442	if !found {
443		return ""
444	}
445
446	query, _ := url.ParseQuery(rawQuery)
447	return query.Get("menu")
448}