render.gno

10.83 Kb · 450 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
152		menu := getCurrentMenu(req.RawPath)
153		if menu == menuManageBoard {
154			b.WriteString(md.Bold("Manage Board"))
155		} else {
156			b.WriteString(md.Link("Manage Board", menuURL(menuManageBoard)))
157		}
158
159		b.WriteString("  \n")
160
161		if menu == menuManageBoard {
162			b.WriteString("↳")
163			b.WriteString(md.Link("Invite Member", makeInviteMemberURI(board)))
164			b.WriteString(" • ")
165			b.WriteString(md.Link("List Invite Requests", makeBoardURI(board)+"/invites"))
166			b.WriteString(" • ")
167			b.WriteString(md.Link("List Members", boardMembersURL))
168			b.WriteString(" • ")
169			b.WriteString(md.Link("List Banned Users", makeBoardURI(board)+"/banned-users"))
170			b.WriteString(" • ")
171			b.WriteString(md.Link("Freeze Board", makeFreezeBoardURI(board)))
172			b.WriteString("\n")
173		}
174	}
175
176	return b.String()
177}
178
179func renderThread(res *mux.ResponseWriter, req *mux.Request) {
180	renderNotice(res)
181
182	name := req.GetVar("board")
183	v, found := gBoardsByName.Get(name)
184	if !found {
185		res.Write("Board does not exist: " + name)
186		return
187	}
188
189	rawID := req.GetVar("thread")
190	tID, err := strconv.Atoi(rawID)
191	if err != nil {
192		res.Write("Invalid thread ID: " + rawID)
193		return
194	}
195
196	board := v.(*Board)
197	thread, found := board.GetThread(PostID(tID))
198	if !found {
199		res.Write("Thread does not exist with ID: " + rawID)
200	} else if thread.Hidden {
201		res.Write("Thread with ID: " + rawID + " has been flagged as inappropriate")
202	} else {
203		res.Write(thread.Render(req.RawPath, "", 5))
204	}
205}
206
207func renderReply(res *mux.ResponseWriter, req *mux.Request) {
208	renderNotice(res)
209
210	name := req.GetVar("board")
211	v, found := gBoardsByName.Get(name)
212	if !found {
213		res.Write("Board does not exist: " + name)
214		return
215	}
216
217	rawID := req.GetVar("thread")
218	tID, err := strconv.Atoi(rawID)
219	if err != nil {
220		res.Write("Invalid thread ID: " + rawID)
221		return
222	}
223
224	rawID = req.GetVar("reply")
225	rID, err := strconv.Atoi(rawID)
226	if err != nil {
227		res.Write("Invalid reply ID: " + rawID)
228		return
229	}
230
231	board := v.(*Board)
232	thread, found := board.GetThread(PostID(tID))
233	if !found {
234		res.Write("Thread does not exist with ID: " + req.GetVar("thread"))
235		return
236	}
237
238	reply, found := thread.GetReply(PostID(rID))
239	if !found {
240		res.Write("Reply does not exist with ID: " + rawID)
241		return
242	}
243
244	// Call render even for hidden replies to display children.
245	// Original comment content will be hidden under the hood.
246	// See: #3480
247	res.Write(reply.RenderInner())
248}
249
250func renderMembers(res *mux.ResponseWriter, req *mux.Request) {
251	boardID := BoardID(0)
252	perms := gPerms
253	name := req.GetVar("board")
254	if name != "" {
255		v, found := gBoardsByName.Get(name)
256		if !found {
257			res.Write(md.H3("Board not found"))
258			return
259		}
260
261		board := v.(*Board)
262		boardID = board.ID
263		perms = board.perms
264
265		res.Write(md.H1(board.Name + " Members"))
266		res.Write(md.H3("These are the board members"))
267	} else {
268		res.Write(md.H1("Admin Users"))
269		res.Write(md.H3("These are the admin users of the realm"))
270	}
271
272	// Create a pager with a small page size to reduce
273	// the number of username lookups per page.
274	p, err := pager.New(req.RawPath, perms.UsersCount(), pager.WithPageSize(pageSizeDefault))
275	if err != nil {
276		res.Write(err.Error())
277		return
278	}
279
280	table := mdtable.Table{
281		Headers: []string{"Member", "Role", "Actions"},
282	}
283
284	perms.IterateUsers(p.Offset(), p.PageSize(), func(u User) bool {
285		actions := []string{
286			md.Link("remove", gRealmLink.Call(
287				"RemoveMember",
288				"boardID", boardID.String(),
289				"member", u.Address.String(),
290			)),
291			md.Link("change role", gRealmLink.Call(
292				"ChangeMemberRole",
293				"boardID", boardID.String(),
294				"member", u.Address.String(),
295				"role", "",
296			)),
297		}
298
299		table.Append([]string{
300			md.UserLink(u.Address.String()),
301			rolesToString(u.Roles),
302			strings.Join(actions, " • "),
303		})
304		return false
305	})
306	res.Write(table.String())
307
308	if p.HasPages() {
309		res.Write("\n" + pager.Picker(p))
310	}
311}
312
313func renderInvites(res *mux.ResponseWriter, req *mux.Request) {
314	name := req.GetVar("board")
315	v, found := gBoardsByName.Get(name)
316	if !found {
317		res.Write(md.H3("Board not found"))
318		return
319	}
320
321	board := v.(*Board)
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			md.UserLink(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	v, found := gBoardsByName.Get(name)
373	if !found {
374		res.Write(md.H3("Board not found"))
375		return
376	}
377
378	board := v.(*Board)
379	res.Write(md.H1(board.Name + " Banned Users"))
380
381	banned, found := getBannedUsers(board.ID)
382	if !found || banned.Size() == 0 {
383		res.Write(md.H3("Board has no banned users"))
384		return
385	}
386
387	p, err := pager.New(req.RawPath, banned.Size(), pager.WithPageSize(pageSizeDefault))
388	if err != nil {
389		res.Write(err.Error())
390		return
391	}
392
393	table := mdtable.Table{
394		Headers: []string{"User", "Banned Until", "Actions"},
395	}
396
397	res.Write(md.H3("These users have been banned from the board"))
398	banned.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
399		table.Append([]string{
400			md.UserLink(addr),
401			v.(time.Time).Format(dateFormat),
402			md.Link("unban", gRealmLink.Call(
403				"Unban",
404				"boardID", board.ID.String(),
405				"user", addr,
406				"reason", "",
407			)),
408		})
409		return false
410	})
411
412	res.Write(table.String())
413
414	if p.HasPages() {
415		res.Write("\n" + pager.Picker(p))
416	}
417}
418
419func renderNotice(res *mux.ResponseWriter) {
420	if gNotice != "" {
421		res.Write(md.Blockquote(gNotice))
422	}
423}
424
425func rolesToString(roles []Role) string {
426	if len(roles) == 0 {
427		return ""
428	}
429
430	names := make([]string, len(roles))
431	for i, r := range roles {
432		names[i] = string(r)
433	}
434	return strings.Join(names, ", ")
435}
436
437func menuURL(name string) string {
438	// TODO: Menu URL works because no other GET arguments are being used
439	return "?menu=" + name
440}
441
442func getCurrentMenu(rawURL string) string {
443	_, rawQuery, found := strings.Cut(rawURL, "?")
444	if !found {
445		return ""
446	}
447
448	query, _ := url.ParseQuery(rawQuery)
449	return query.Get("menu")
450}