linktree.gno
3.06 Kb · 137 lines
1package linktree
2
3import (
4 "strconv"
5 "strings"
6
7 "chain"
8 "chain/runtime/unsafe"
9
10 "gno.land/p/nt/avl/v0"
11)
12
13// link is a single labeled URL on a profile.
14type link struct {
15 Label string
16 URL string
17}
18
19// profile is one address's personal links page.
20type profile struct {
21 Bio string
22 Links []link
23}
24
25// profiles maps an address (string key) to its *profile.
26// avl.Tree keeps iteration deterministic for Render.
27var profiles avl.Tree
28
29// SetBio sets (or replaces) the calling address's bio.
30func SetBio(cur realm, bio string) {
31 addr := unsafe.PreviousRealm().Address()
32 p := getOrCreate(addr.String())
33 p.Bio = bio
34 chain.Emit("BioSet", "addr", addr.String())
35}
36
37// AddLink appends a labeled URL to the calling address's profile.
38func AddLink(cur realm, label string, url string) {
39 if label == "" {
40 panic("label must not be empty")
41 }
42 if url == "" {
43 panic("url must not be empty")
44 }
45 addr := unsafe.PreviousRealm().Address()
46 p := getOrCreate(addr.String())
47 p.Links = append(p.Links, link{Label: label, URL: url})
48 chain.Emit("LinkAdded", "addr", addr.String(), "label", label)
49}
50
51// RemoveLink deletes the link at index from the calling address's profile.
52func RemoveLink(cur realm, index int) {
53 addr := unsafe.PreviousRealm().Address()
54 key := addr.String()
55 v, ok := profiles.Get(key)
56 if !ok {
57 panic("no profile for caller")
58 }
59 p := v.(*profile)
60 if index < 0 || index >= len(p.Links) {
61 panic("index out of range")
62 }
63 p.Links = append(p.Links[:index], p.Links[index+1:]...)
64 chain.Emit("LinkRemoved", "addr", key, "index", strconv.Itoa(index))
65}
66
67// getOrCreate returns the profile for key, creating an empty one if absent.
68func getOrCreate(key string) *profile {
69 if v, ok := profiles.Get(key); ok {
70 return v.(*profile)
71 }
72 p := &profile{}
73 profiles.Set(key, p)
74 return p
75}
76
77// Render shows all profiles at root, or a single profile at "/<address>".
78func Render(path string) string {
79 addr := strings.Trim(path, "/")
80 if addr == "" {
81 return renderIndex()
82 }
83 return renderProfile(addr)
84}
85
86func renderIndex() string {
87 var b strings.Builder
88 b.WriteString("# Linktree\n\n")
89 if profiles.Size() == 0 {
90 b.WriteString("_No profiles yet. Call `SetBio` or `AddLink` to create one._\n")
91 return b.String()
92 }
93 b.WriteString("Profiles:\n\n")
94 profiles.Iterate("", "", func(key string, _ interface{}) bool {
95 b.WriteString("- [")
96 b.WriteString(key)
97 b.WriteString("](/r/REPLACE_ADDR/linktree:")
98 b.WriteString(key)
99 b.WriteString(")\n")
100 return false
101 })
102 return b.String()
103}
104
105func renderProfile(addr string) string {
106 var b strings.Builder
107 b.WriteString("# ")
108 b.WriteString(addr)
109 b.WriteString("\n\n")
110
111 v, ok := profiles.Get(addr)
112 if !ok {
113 b.WriteString("_No profile for this address._\n")
114 return b.String()
115 }
116 p := v.(*profile)
117
118 if p.Bio != "" {
119 b.WriteString(p.Bio)
120 b.WriteString("\n\n")
121 }
122
123 if len(p.Links) == 0 {
124 b.WriteString("_No links yet._\n")
125 return b.String()
126 }
127
128 b.WriteString("## Links\n\n")
129 for _, l := range p.Links {
130 b.WriteString("- [")
131 b.WriteString(l.Label)
132 b.WriteString("](")
133 b.WriteString(l.URL)
134 b.WriteString(")\n")
135 }
136 return b.String()
137}