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

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}