Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

9.17 Kb · 371 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/pager"
 11	"gno.land/p/moul/md"
 12	"gno.land/p/moul/mdtable"
 13	"gno.land/p/nt/mux"
 14)
 15
 16const (
 17	pageSizeDefault = 6
 18	pageSizeReplies = 10
 19)
 20
 21const menuManageBoard = "manageBoard"
 22
 23func Render(path string) string {
 24	var (
 25		b      strings.Builder
 26		router = mux.NewRouter()
 27	)
 28
 29	router.HandleFunc("", renderBoardsList)
 30	router.HandleFunc("help", renderHelp)
 31	router.HandleFunc("admin-users", renderMembers)
 32	router.HandleFunc("{board}", renderBoard)
 33	router.HandleFunc("{board}/members", renderMembers)
 34	router.HandleFunc("{board}/invites", renderInvites)
 35	router.HandleFunc("{board}/banned-users", renderBannedUsers)
 36	router.HandleFunc("{board}/{thread}", renderThread)
 37	router.HandleFunc("{board}/{thread}/{reply}", renderReply)
 38
 39	router.NotFoundHandler = func(res *mux.ResponseWriter, _ *mux.Request) {
 40		res.Write(md.Blockquote("Path not found"))
 41	}
 42
 43	// Render common realm header before resolving render path
 44	if gNotice != "" {
 45		b.WriteString(infoAlert("Notice", gNotice))
 46	}
 47
 48	// Render view for current path
 49	b.WriteString(router.Render(path))
 50
 51	return b.String()
 52}
 53
 54func renderHelp(res *mux.ResponseWriter, _ *mux.Request) {
 55	res.Write(md.H1("Boards Help"))
 56	if gHelp != "" {
 57		res.Write(gHelp)
 58		return
 59	}
 60
 61	link := gRealmLink.Call("SetHelp", "content", "")
 62	res.Write(md.H3("Help content has not been uploaded"))
 63	res.Write("Do you want to " + md.Link("upload boards help", link) + " ?")
 64}
 65
 66func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) {
 67	res.Write(md.H1("Boards"))
 68	renderBoardListMenu(res, req)
 69	res.Write(md.HorizontalRule())
 70
 71	if gListedBoardsByID.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, gListedBoardsByID.Size(), pager.WithPageSize(pageSizeDefault))
 79	if err != nil {
 80		panic(err)
 81	}
 82
 83	render := func(_ string, v any) bool {
 84		board := v.(*boards.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.Threads.Size()) + " 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		gListedBoardsByID.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		gListedBoardsByID.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 renderBoardMenu(board *boards.Board, req *mux.Request) string {
130	var (
131		b               strings.Builder
132		boardMembersURL = makeBoardURI(board) + "/members"
133	)
134
135	b.WriteString("\n")
136	if board.Readonly {
137		b.WriteString(md.Link("List Members", boardMembersURL))
138		b.WriteString(" • ")
139		b.WriteString(md.Link("Unfreeze Board", makeUnfreezeBoardURI(board)))
140		b.WriteString("\n")
141	} else {
142		b.WriteString(md.Link("Create Thread", makeCreateThreadURI(board)))
143		b.WriteString(" • ")
144		b.WriteString(md.Link("Request Invite", makeRequestInviteURI(board)))
145		b.WriteString(" • ")
146
147		menu := getCurrentMenu(req.RawPath)
148		if menu == menuManageBoard {
149			b.WriteString(md.Bold("Manage Board"))
150		} else {
151			b.WriteString(md.Link("Manage Board", menuURL(menuManageBoard)))
152		}
153
154		b.WriteString("  \n")
155
156		if menu == menuManageBoard {
157			b.WriteString("↳")
158			b.WriteString(md.Link("Invite Member", makeInviteMemberURI(board)))
159			b.WriteString(" • ")
160			b.WriteString(md.Link("List Invite Requests", makeBoardURI(board)+"/invites"))
161			b.WriteString(" • ")
162			b.WriteString(md.Link("List Members", boardMembersURL))
163			b.WriteString(" • ")
164			b.WriteString(md.Link("List Banned Users", makeBoardURI(board)+"/banned-users"))
165			b.WriteString(" • ")
166			b.WriteString(md.Link("Freeze Board", makeFreezeBoardURI(board)))
167			b.WriteString("\n")
168		}
169	}
170
171	b.WriteString("\n")
172	return b.String()
173}
174
175func renderMembers(res *mux.ResponseWriter, req *mux.Request) {
176	boardID := boards.ID(0)
177	perms := gPerms
178	name := req.GetVar("board")
179	if name != "" {
180		board, found := gBoards.GetByName(name)
181		if !found {
182			res.Write(md.H3("Board not found"))
183			return
184		}
185
186		boardID = board.ID
187		perms = board.Permissions
188
189		res.Write(md.H1(board.Name + " Members"))
190		res.Write(md.H3("These are the board members"))
191	} else {
192		res.Write(md.H1("Admin Users"))
193		res.Write(md.H3("These are the admin users of the realm"))
194	}
195
196	// Create a pager with a small page size to reduce
197	// the number of username lookups per page.
198	p, err := pager.New(req.RawPath, perms.UsersCount(), pager.WithPageSize(pageSizeDefault))
199	if err != nil {
200		res.Write(err.Error())
201		return
202	}
203
204	table := mdtable.Table{
205		Headers: []string{"Member", "Role", "Actions"},
206	}
207
208	perms.IterateUsers(p.Offset(), p.PageSize(), func(u boards.User) bool {
209		actions := []string{
210			md.Link("remove", gRealmLink.Call(
211				"RemoveMember",
212				"boardID", boardID.String(),
213				"member", u.Address.String(),
214			)),
215			md.Link("change role", gRealmLink.Call(
216				"ChangeMemberRole",
217				"boardID", boardID.String(),
218				"member", u.Address.String(),
219				"role", "",
220			)),
221		}
222
223		table.Append([]string{
224			userLink(u.Address),
225			rolesToString(u.Roles),
226			strings.Join(actions, " • "),
227		})
228		return false
229	})
230	res.Write(table.String())
231
232	if p.HasPages() {
233		res.Write("\n" + pager.Picker(p))
234	}
235}
236
237func renderInvites(res *mux.ResponseWriter, req *mux.Request) {
238	name := req.GetVar("board")
239	board, found := gBoards.GetByName(name)
240	if !found {
241		res.Write(md.H3("Board not found"))
242		return
243	}
244
245	res.Write(md.H1(board.Name + " Invite Requests"))
246
247	requests, found := getInviteRequests(board.ID)
248	if !found || requests.Size() == 0 {
249		res.Write(md.H3("Board has no invite requests"))
250		return
251	}
252
253	p, err := pager.New(req.RawPath, requests.Size(), pager.WithPageSize(pageSizeDefault))
254	if err != nil {
255		res.Write(err.Error())
256		return
257	}
258
259	table := mdtable.Table{
260		Headers: []string{"User", "Request Date", "Actions"},
261	}
262
263	res.Write(md.H3("These users have requested to be invited to the board"))
264	requests.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
265		actions := []string{
266			md.Link("accept", gRealmLink.Call(
267				"AcceptInvite",
268				"boardID", board.ID.String(),
269				"user", addr,
270			)),
271			md.Link("revoke", gRealmLink.Call(
272				"RevokeInvite",
273				"boardID", board.ID.String(),
274				"user", addr,
275			)),
276		}
277
278		table.Append([]string{
279			userLink(address(addr)),
280			v.(time.Time).Format(dateFormat),
281			strings.Join(actions, " • "),
282		})
283		return false
284	})
285
286	res.Write(table.String())
287
288	if p.HasPages() {
289		res.Write("\n" + pager.Picker(p))
290	}
291}
292
293func renderBannedUsers(res *mux.ResponseWriter, req *mux.Request) {
294	name := req.GetVar("board")
295	board, found := gBoards.GetByName(name)
296	if !found {
297		res.Write(md.H3("Board not found"))
298		return
299	}
300
301	res.Write(md.H1(board.Name + " Banned Users"))
302
303	banned, found := getBannedUsers(board.ID)
304	if !found || banned.Size() == 0 {
305		res.Write(md.H3("Board has no banned users"))
306		return
307	}
308
309	p, err := pager.New(req.RawPath, banned.Size(), pager.WithPageSize(pageSizeDefault))
310	if err != nil {
311		res.Write(err.Error())
312		return
313	}
314
315	table := mdtable.Table{
316		Headers: []string{"User", "Banned Until", "Actions"},
317	}
318
319	res.Write(md.H3("These users have been banned from the board"))
320	banned.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
321		table.Append([]string{
322			userLink(address(addr)),
323			v.(time.Time).Format(dateFormat),
324			md.Link("unban", gRealmLink.Call(
325				"Unban",
326				"boardID", board.ID.String(),
327				"user", addr,
328				"reason", "",
329			)),
330		})
331		return false
332	})
333
334	res.Write(table.String())
335
336	if p.HasPages() {
337		res.Write("\n" + pager.Picker(p))
338	}
339}
340
341func infoAlert(title, msg string) string {
342	header := strings.TrimSpace("[!INFO] " + title)
343	return md.Blockquote(header + "\n" + msg)
344}
345
346func rolesToString(roles []boards.Role) string {
347	if len(roles) == 0 {
348		return ""
349	}
350
351	names := make([]string, len(roles))
352	for i, r := range roles {
353		names[i] = string(r)
354	}
355	return strings.Join(names, ", ")
356}
357
358func menuURL(name string) string {
359	// TODO: Menu URL works because no other GET arguments are being used
360	return "?menu=" + name
361}
362
363func getCurrentMenu(rawURL string) string {
364	_, rawQuery, found := strings.Cut(rawURL, "?")
365	if !found {
366		return ""
367	}
368
369	query, _ := url.ParseQuery(rawQuery)
370	return query.Get("menu")
371}