comment.gno
3.03 Kb · 165 lines
1package blog
2
3import (
4 "strconv"
5 "time"
6
7 "gno.land/p/moul/md"
8 "gno.land/p/nt/avl"
9 "gno.land/p/nt/seqid"
10)
11
12type Comment struct {
13 id seqid.ID
14 author string
15 content string
16 createdAt time.Time
17 editedAt *time.Time // nil if unedited
18 pinned bool
19 likes int
20 RepliesId seqid.ID
21 Replies *avl.Tree // id --> *Comment
22
23 footer string // additional text, e.g. "via @lou" or txlink calls
24 DisableLikes bool
25}
26
27func (c Comment) ID() string {
28 return c.id.String()
29}
30
31func (c Comment) Author() string {
32 return c.author
33}
34
35func (c Comment) Content() string {
36 return c.content
37}
38
39func (c Comment) CreatedAt() time.Time {
40 return c.createdAt
41}
42
43func (c Comment) EditedAt() *time.Time {
44 return c.editedAt
45}
46
47func (c Comment) Pinned() bool {
48 return c.pinned
49}
50
51func (c Comment) Likes() int {
52 return c.likes
53}
54
55func (c Comment) Footer() string {
56 return c.footer
57}
58
59func NewComment(author, content string) (*Comment, error) {
60 if content == "" {
61 return nil, ErrEmptyComment
62 }
63 return &Comment{
64 author: author,
65 content: content,
66 createdAt: time.Now(),
67 editedAt: nil,
68 pinned: false,
69 likes: 0,
70 RepliesId: seqid.ID(0),
71 Replies: avl.NewTree(),
72 footer: "",
73 DisableLikes: false,
74 }, nil
75}
76
77func (c *Comment) Edit(content string) error {
78 if content == "" {
79 return ErrEmptyComment
80 }
81 c.content = content
82 now := time.Now()
83 c.editedAt = &now
84 return nil
85}
86
87func (c *Comment) Pin() {
88 c.pinned = true
89}
90
91func (c *Comment) Unpin() {
92 c.pinned = false
93}
94
95func (c *Comment) AddLike() {
96 c.likes++
97}
98
99func (c *Comment) RemoveLike() {
100 if c.likes > 0 {
101 c.likes--
102 }
103}
104
105func (c *Comment) SetID(id seqid.ID) {
106 c.id = id
107}
108
109func (c *Comment) SetFooter(footer string) {
110 c.footer = footer
111}
112
113func (c *Comment) Render(blogPrefix string, depth int, resolver UserResolver) string {
114 prefix := ""
115 for i := 0; i < depth; i++ {
116 prefix += "> "
117 }
118 user, _ := CheckUser(c.Author(), resolver)
119 out := prefix + md.Bold(md.Link("@"+user, blogPrefix+":commenters/"+user)) + " "
120 if c.Pinned() {
121 out += "📌 "
122 }
123 if c.EditedAt() != nil {
124 out += md.Italic(formatTime(*c.EditedAt(), "relative")+" (edited)") + "\n"
125 } else {
126 out += md.Italic(formatTime(c.CreatedAt(), "relative")) + "\n"
127 }
128 out += "\n" + prefix + c.Content() + "\n"
129 if !c.DisableLikes {
130 out += prefix + "❤️ " + strconv.Itoa(c.Likes()) + " \n\n"
131 }
132 if c.Footer() != "" {
133 out += prefix + c.Footer() + "\n\n"
134 }
135
136 if c.Replies != nil && c.Replies.Size() > 0 {
137 c.Replies.ReverseIterate("", "", func(key string, value any) bool {
138 reply := value.(*Comment)
139 out += reply.Render(blogPrefix, depth+1, resolver)
140 return false
141 })
142 }
143 return out
144}
145
146func searchInReplies(tree *avl.Tree, id string, result **Comment) bool {
147 if tree == nil {
148 return false
149 }
150 found := false
151 tree.ReverseIterate("", "", func(_ string, v interface{}) bool {
152 c := v.(*Comment)
153 if c.ID() == id {
154 *result = c
155 found = true
156 return true
157 }
158 if searchInReplies(c.Replies, id, result) {
159 found = true
160 return true
161 }
162 return false
163 })
164 return found
165}