Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

folders.gno

14.68 Kb · 626 lines
  1package kourtv3
  2import (
  3	"strconv"
  4	"strings"
  5	bptree "gno.land/p/nt/bptree/v0"
  6)
  7const maxFolderTextLen = 200
  8const (
  9	maxFolderDepth = 4
 10	maxFolderItems = 200
 11	maxFolders = 100
 12)
 13type folder struct {
 14	id     uint64
 15	name   string
 16	desc   string
 17	items  []uint64
 18	purged bool
 19	code   string
 20	retired bool
 21	bornOf uint64
 22	focusByDefault bool
 23	parent uint64
 24	ord int
 25	image *mediaItem
 26}
 27func (cm *courtMod) folderDepth(id uint64) int {
 28	d := 0
 29	for id != 0 && d <= maxFolders {
 30		d++
 31		v := cm.folders.Get(beClaimKey(id))
 32		if v == nil {
 33			break
 34		}
 35		id = v.(*folder).parent
 36	}
 37	return d
 38}
 39func (cm *courtMod) folderHeight(id uint64) int {
 40	h := 1
 41	cm.folders.Iterate("", "", func(_ string, v any) bool {
 42		f, ok := v.(*folder)
 43		if !ok || f.parent != id {
 44			return false
 45		}
 46		if ch := 1 + cm.folderHeight(f.id); ch > h {
 47			h = ch
 48		}
 49		return false
 50	})
 51	return h
 52}
 53func (cm *courtMod) mustNestable(id, parent uint64, height int) {
 54	if parent == 0 {
 55		return
 56	}
 57	if parent == id {
 58		panic("kourtv3: a folder cannot contain itself")
 59	}
 60	cm.mustFolder(parent)
 61	for up, n := parent, 0; up != 0 && n <= maxFolders; n++ {
 62		if up == id {
 63			panic("kourtv3: that would put a folder inside itself")
 64		}
 65		v := cm.folders.Get(beClaimKey(up))
 66		if v == nil {
 67			break
 68		}
 69		up = v.(*folder).parent
 70	}
 71	if cm.folderDepth(parent)+height > maxFolderDepth {
 72		panic("kourtv3: folders nest at most " + strconv.Itoa(maxFolderDepth) + " deep")
 73	}
 74}
 75func folderAfter(a, b *folder) bool {
 76	ap, bp := a.ord > 0, b.ord > 0
 77	if ap != bp {
 78		return bp
 79	}
 80	if ap && a.ord != b.ord {
 81		return a.ord > b.ord
 82	}
 83	return a.id > b.id
 84}
 85func (cm *courtMod) folderRows() []*folder {
 86	out := make([]*folder, 0, maxFolders)
 87	cm.folders.Iterate("", "", func(_ string, v any) bool {
 88		if f, ok := v.(*folder); ok {
 89			out = append(out, f)
 90		}
 91		return false
 92	})
 93	for i := range out {
 94		first := true
 95		for j := 0; j < i; j++ {
 96			if out[j].parent == out[i].parent {
 97				first = false
 98				break
 99			}
100		}
101		if !first {
102			continue
103		}
104		p := out[i].parent
105		slots := make([]int, 0, len(out))
106		grp := make([]*folder, 0, len(out))
107		for j, g := range out {
108			if g.parent == p {
109				slots = append(slots, j)
110				grp = append(grp, g)
111			}
112		}
113		for a := 1; a < len(grp); a++ {
114			f := grp[a]
115			b := a - 1
116			for b >= 0 && folderAfter(grp[b], f) {
117				grp[b+1] = grp[b]
118				b--
119			}
120			grp[b+1] = f
121		}
122		for k, g := range grp {
123			out[slots[k]] = g
124		}
125	}
126	return out
127}
128func (cm *courtMod) mustFolder(id uint64) *folder {
129	v := cm.folders.Get(beClaimKey(id))
130	if v == nil {
131		panic("kourtv3: no such folder")
132	}
133	return v.(*folder)
134}
135func requireFolderMod(c *Court, who address) *courtMod {
136	cm := ensureMod(c)
137	requireActiveMod(cm, who)
138	return cm
139}
140func folderTextOK(name, desc string) bool {
141	return runeLen(name) > 0 && runeLen(name) <= maxFolderTextLen && runeLen(desc) <= maxFolderTextLen
142}
143func folderNameTaken(cm *courtMod, name string) bool {
144	taken := false
145	if cm.folders == nil {
146		return false
147	}
148	cm.folders.Iterate("", "", func(_ string, v any) bool {
149		f := v.(*folder)
150		if !f.retired && f.name == name {
151			taken = true
152			return true
153		}
154		return false
155	})
156	return taken
157}
158func newFolderCore(cm *courtMod, name, desc string, parent uint64) uint64 {
159	if cm.folders.Size() >= maxFolders {
160		panic("kourtv3: this court already has " + strconv.Itoa(maxFolders) + " folders")
161	}
162	if !folderTextOK(name, desc) {
163		panic("kourtv3: a folder name is 1..200 and a description at most 200 characters")
164	}
165	if parent != 0 {
166		cm.mustNestable(0, parent, 1)
167	}
168	cm.folderSeq++
169	id := cm.folderSeq
170	cm.folders.Set(beClaimKey(id), &folder{id: id, name: name, desc: desc, parent: parent})
171	return id
172}
173type folderProposal struct {
174	id     uint64
175	name   string
176	desc   string
177	parent uint64
178	by     address
179	at     int64
180}
181func RegisterFolderProposal(cur realm, courtSlug, name, desc string, parentID uint64) uint64 {
182	if !cur.IsCurrent() {
183		panic(errStaleRealm)
184	}
185	who := cur.Previous().Address()
186	c := mustCourt(courtSlug)
187	cm := ensureMod(c)
188	if !folderTextOK(name, desc) {
189		panic("kourtv3: a folder name is 1..200 and a description at most 200 characters")
190	}
191	if parentID != 0 {
192		cm.mustNestable(0, parentID, 1)
193	}
194	if folderNameTaken(cm, name) {
195		panic("kourtv3: this court already has a folder by that name")
196	}
197	if cm.folderProps == nil {
198		cm.folderProps = bptree.NewBPTree32()
199	}
200	cm.folderPropSeq++
201	id := cm.folderPropSeq
202	cm.folderProps.Set(beClaimKey(id), &folderProposal{
203		id: id, name: name, desc: desc, parent: parentID, by: who, at: heightNow(),
204	})
205	return id
206}
207func (cm *courtMod) mustFolderProposal(id uint64) *folderProposal {
208	if cm.folderProps == nil {
209		panic("kourtv3: no such folder proposal")
210	}
211	v := cm.folderProps.Get(beClaimKey(id))
212	if v == nil {
213		panic("kourtv3: no such folder proposal")
214	}
215	return v.(*folderProposal)
216}
217func CreateFolder(cur realm, courtSlug, name, desc string) uint64 {
218	if !cur.IsCurrent() {
219		panic(errStaleRealm)
220	}
221	who := cur.Previous().Address()
222	c := mustCourt(courtSlug)
223	cm := requireFolderMod(c, who)
224	id := newFolderCore(cm, name, desc, 0)
225	emitModAct(c.id, 0, "folder-create", who)
226	return id
227}
228func CreateFolderIn(cur realm, courtSlug string, parentID uint64, name, desc string) uint64 {
229	if !cur.IsCurrent() {
230		panic(errStaleRealm)
231	}
232	who := cur.Previous().Address()
233	c := mustCourt(courtSlug)
234	cm := requireFolderMod(c, who)
235	id := newFolderCore(cm, name, desc, parentID)
236	emitModAct(c.id, 0, "folder-create", who)
237	return id
238}
239func MoveFolder(cur realm, courtSlug string, folderID, newParent uint64) {
240	if !cur.IsCurrent() {
241		panic(errStaleRealm)
242	}
243	who := cur.Previous().Address()
244	c := mustCourt(courtSlug)
245	cm := requireFolderMod(c, who)
246	f := cm.mustFolder(folderID)
247	cm.mustNestable(folderID, newParent, cm.folderHeight(folderID))
248	f.parent = newParent
249	emitModAct(c.id, 0, "folder-move", who)
250}
251func AddToFolder(cur realm, courtSlug string, folderID, claimID uint64) {
252	if !cur.IsCurrent() {
253		panic(errStaleRealm)
254	}
255	who := cur.Previous().Address()
256	c := mustCourt(courtSlug)
257	cm := requireFolderMod(c, who)
258	_ = mustClaim(c, claimID)
259	f := cm.mustFolder(folderID)
260	fileInto(f, claimID)
261	emitModAct(c.id, claimID, "folder-add", who)
262}
263func fileInto(f *folder, claimID uint64) {
264	for _, x := range f.items {
265		if x == claimID {
266			return
267		}
268	}
269	if len(f.items) >= maxFolderItems {
270		panic("kourtv3: a folder holds at most " + strconv.Itoa(maxFolderItems) + " claims")
271	}
272	f.items = append(f.items, claimID)
273}
274func RemoveFromFolder(cur realm, courtSlug string, folderID, claimID uint64) {
275	if !cur.IsCurrent() {
276		panic(errStaleRealm)
277	}
278	who := cur.Previous().Address()
279	c := mustCourt(courtSlug)
280	cm := requireFolderMod(c, who)
281	f := cm.mustFolder(folderID)
282	out := f.items[:0]
283	found := false
284	for _, x := range f.items {
285		if x == claimID {
286			found = true
287			continue
288		}
289		out = append(out, x)
290	}
291	if !found {
292		panic("kourtv3: that claim is not in the folder")
293	}
294	f.items = out
295	emitModAct(c.id, claimID, "folder-remove", who)
296}
297func RenameFolder(cur realm, courtSlug string, folderID uint64, name, desc string) {
298	if !cur.IsCurrent() {
299		panic(errStaleRealm)
300	}
301	who := cur.Previous().Address()
302	c := mustCourt(courtSlug)
303	cm := requireFolderMod(c, who)
304	if runeLen(name) == 0 || runeLen(name) > maxFolderTextLen || runeLen(desc) > maxFolderTextLen {
305		panic("kourtv3: a folder name is 1..200 and a description at most 200 characters")
306	}
307	f := cm.mustFolder(folderID)
308	if f.purged {
309		panic("kourtv3: this folder's text was purged and cannot be re-set")
310	}
311	f.name = name
312	f.desc = desc
313	emitModAct(c.id, 0, "folder-rename", who)
314}
315func SetFolderImage(cur realm, courtSlug string, folderID uint64, media string) {
316	if !cur.IsCurrent() {
317		panic(errStaleRealm)
318	}
319	who := cur.Previous().Address()
320	c := mustCourt(courtSlug)
321	cm := requireFolderMod(c, who)
322	f := cm.mustFolder(folderID)
323	if f.purged {
324		panic("kourtv3: this folder's text was purged and cannot be re-set")
325	}
326	items := parseMediaArg(media)
327	if len(items) != 1 {
328		panic("kourtv3: a folder carries exactly one image")
329	}
330	if items[0].kind != mediaKindImage {
331		panic("kourtv3: a folder's image is an image, not a video link")
332	}
333	f.image = &items[0]
334	emitModAct(c.id, 0, "folder-image", who)
335}
336func ClearFolderImage(cur realm, courtSlug string, folderID uint64) {
337	if !cur.IsCurrent() {
338		panic(errStaleRealm)
339	}
340	who := cur.Previous().Address()
341	c := mustCourt(courtSlug)
342	cm := requireFolderMod(c, who)
343	cm.mustFolder(folderID).image = nil
344	emitModAct(c.id, 0, "folder-image-clear", who)
345}
346func FolderImage(courtSlug string, folderID uint64) string {
347	c := mustCourt(courtSlug)
348	cm := c.mod
349	if cm == nil {
350		return "[]"
351	}
352	f := cm.mustFolder(folderID)
353	if f.image == nil {
354		return "[]"
355	}
356	return encodeMedia([]mediaItem{*f.image})
357}
358func PurgeFolder(cur realm, courtSlug string, folderID uint64, categoryCode string) {
359	if !cur.IsCurrent() {
360		panic(errStaleRealm)
361	}
362	who := cur.Previous().Address()
363	d := ensureGlobalDAO()
364	if !d.members.Has(who.String()) {
365		panic("kourtv3: only a global DAO member may purge")
366	}
367	mustCategoryCode(categoryCode)
368	c := mustCourt(courtSlug)
369	cm := ensureMod(c)
370	f := cm.mustFolder(folderID)
371	fire, code := approveAction(d.pending, "purgefolder:"+c.id+":"+strconv.FormatUint(folderID, 10), who, categoryCode, d.purgeM)
372	if !fire {
373		return
374	}
375	f.purged = true
376	f.code = code
377	f.name = ""
378	f.desc = ""
379	f.image = nil
380	emitPurge(c.id, 0, code, who)
381}
382func FolderCount(courtSlug string) int {
383	c := mustCourt(courtSlug)
384	if c.mod == nil {
385		return 0
386	}
387	return c.mod.folders.Size()
388}
389func FolderName(courtSlug string, folderID uint64) string {
390	c := mustCourt(courtSlug)
391	if c.mod == nil {
392		panic("kourtv3: no such folder")
393	}
394	f := c.mod.mustFolder(folderID)
395	if f.purged {
396		return "[purged:" + f.code + "]"
397	}
398	return f.name
399}
400func FolderDesc(courtSlug string, folderID uint64) string {
401	c := mustCourt(courtSlug)
402	if c.mod == nil {
403		panic("kourtv3: no such folder")
404	}
405	return c.mod.mustFolder(folderID).desc
406}
407func FolderItems(courtSlug string, folderID uint64) []uint64 {
408	c := mustCourt(courtSlug)
409	if c.mod == nil {
410		panic("kourtv3: no such folder")
411	}
412	f := c.mod.mustFolder(folderID)
413	out := make([]uint64, len(f.items))
414	copy(out, f.items)
415	return out
416}
417func ClaimFolders(courtSlug string, claimID uint64) []uint64 {
418	c := mustCourt(courtSlug)
419	if c.mod == nil {
420		return nil
421	}
422	var out []uint64
423	for _, f := range c.mod.folderRows() {
424		if f.purged {
425			continue
426		}
427		for _, it := range f.items {
428			if it == claimID {
429				out = append(out, f.id)
430				break
431			}
432		}
433	}
434	return out
435}
436func RetireFolder(cur realm, courtSlug string, folderID uint64) {
437	if !cur.IsCurrent() {
438		panic(errStaleRealm)
439	}
440	who := cur.Previous().Address()
441	c := mustCourt(courtSlug)
442	cm := requireFolderMod(c, who)
443	f := cm.mustFolder(folderID)
444	if cm.folderHeight(folderID) > 1 {
445		panic("kourtv3: retire the folders inside it first")
446	}
447	f.retired = true
448	f.items = nil
449	emitModAct(c.id, 0, "folder-retire", who)
450}
451func RestoreFolder(cur realm, courtSlug string, folderID uint64) {
452	if !cur.IsCurrent() {
453		panic(errStaleRealm)
454	}
455	who := cur.Previous().Address()
456	c := mustCourt(courtSlug)
457	cm := requireFolderMod(c, who)
458	f := cm.mustFolder(folderID)
459	if f.purged {
460		panic("kourtv3: a purged folder cannot be restored")
461	}
462	f.retired = false
463	emitModAct(c.id, 0, "folder-restore", who)
464}
465func MoveItemInFolder(cur realm, courtSlug string, folderID, claimID uint64, to int) {
466	if !cur.IsCurrent() {
467		panic(errStaleRealm)
468	}
469	who := cur.Previous().Address()
470	c := mustCourt(courtSlug)
471	cm := requireFolderMod(c, who)
472	f := cm.mustFolder(folderID)
473	at := -1
474	for i, x := range f.items {
475		if x == claimID {
476			at = i
477			break
478		}
479	}
480	if at < 0 {
481		panic("kourtv3: that claim is not in the folder")
482	}
483	if to < 0 || to >= len(f.items) {
484		panic("kourtv3: that position is outside the folder")
485	}
486	if to == at {
487		return
488	}
489	out := make([]uint64, 0, len(f.items))
490	for i, x := range f.items {
491		if i == at {
492			continue
493		}
494		if len(out) == to {
495			out = append(out, claimID)
496		}
497		out = append(out, x)
498	}
499	if len(out) == to {
500		out = append(out, claimID)
501	}
502	f.items = out
503	emitModAct(c.id, claimID, "folder-order", who)
504}
505func OrderFolders(cur realm, courtSlug string, parentID uint64, ids string) {
506	if !cur.IsCurrent() {
507		panic(errStaleRealm)
508	}
509	who := cur.Previous().Address()
510	c := mustCourt(courtSlug)
511	cm := requireFolderMod(c, who)
512	if parentID != 0 {
513		cm.mustFolder(parentID)
514	}
515	if len(ids) > maxFolders*21 {
516		panic("kourtv3: that is too long to be a list of folder ids")
517	}
518	sibs := make([]*folder, 0, maxFolders)
519	cm.folders.Iterate("", "", func(_ string, v any) bool {
520		if f, ok := v.(*folder); ok && f.parent == parentID {
521			sibs = append(sibs, f)
522		}
523		return false
524	})
525	if len(sibs) == 0 {
526		panic("kourtv3: there are no folders under that heading to order")
527	}
528	parts := strings.Split(ids, ",")
529	if len(parts) != len(sibs) {
530		panic("kourtv3: that heading holds " + strconv.Itoa(len(sibs)) +
531			" folders and the order lists " + strconv.Itoa(len(parts)) +
532			"; it must list every one of them, exactly once")
533	}
534	order := make([]*folder, 0, len(parts))
535	for _, p := range parts {
536		id, err := strconv.ParseUint(strings.TrimSpace(p), 10, 64)
537		if err != nil {
538			panic("kourtv3: an order is a comma-separated list of folder ids")
539		}
540		var f *folder
541		for _, s := range sibs {
542			if s.id == id {
543				f = s
544				break
545			}
546		}
547		if f == nil {
548			panic("kourtv3: folder " + strconv.FormatUint(id, 10) +
549				" is not under that heading")
550		}
551		for _, x := range order {
552			if x.id == id {
553				panic("kourtv3: folder " + strconv.FormatUint(id, 10) +
554					" is listed twice")
555			}
556		}
557		order = append(order, f)
558	}
559	for i, f := range order {
560		f.ord = i + 1
561	}
562	emitModAct(c.id, 0, "folder-sort", who)
563}
564func FolderTree(courtSlug string) string {
565	c := mustCourt(courtSlug)
566	if c.mod == nil {
567		return ""
568	}
569	var b strings.Builder
570	n := 0
571	for _, f := range c.mod.folderRows() {
572		if n > 0 {
573			b.WriteString(",")
574		}
575		flags := ""
576		if f.retired {
577			flags += "r"
578		}
579		if f.purged {
580			flags += "p"
581		}
582		if f.focusByDefault {
583			flags += "f"
584		}
585		if f.image != nil {
586			flags += "i"
587		}
588		if flags == "" {
589			flags = "-"
590		}
591		b.WriteString(strconv.FormatUint(f.id, 10) + ":" +
592			strconv.FormatUint(f.parent, 10) + ":" + flags + ":" +
593			strconv.FormatUint(f.bornOf, 10))
594		n++
595	}
596	return b.String()
597}
598func FolderRetired(courtSlug string, folderID uint64) bool {
599	c := mustCourt(courtSlug)
600	if c.mod == nil {
601		return false
602	}
603	v := c.mod.folders.Get(beClaimKey(folderID))
604	if v == nil {
605		return false
606	}
607	return v.(*folder).retired
608}
609func FolderParent(courtSlug string, folderID uint64) uint64 {
610	c := mustCourt(courtSlug)
611	if c.mod == nil {
612		return 0
613	}
614	v := c.mod.folders.Get(beClaimKey(folderID))
615	if v == nil {
616		return 0
617	}
618	return v.(*folder).parent
619}
620func FolderPurged(courtSlug string, folderID uint64) bool {
621	c := mustCourt(courtSlug)
622	if c.mod == nil {
623		panic("kourtv3: no such folder")
624	}
625	return c.mod.mustFolder(folderID).purged
626}