post.gno
6.51 Kb · 261 lines
1package boards
2
3import (
4 "strconv"
5 "time"
6
7 "gno.land/p/nt/avl/v0"
8)
9
10//----------------------------------------
11// Post
12
13// NOTE: a PostID is relative to the board.
14type PostID uint64
15
16func (pid PostID) String() string {
17 return strconv.Itoa(int(pid))
18}
19
20// A Post is a "thread" or a "reply" depending on context.
21// A thread is a Post of a Board that holds other replies.
22type Post struct {
23 board *Board
24 id PostID
25 creator address
26 title string // optional
27 body string
28 replies avl.Tree // Post.id -> *Post
29 repliesAll avl.Tree // Post.id -> *Post (all replies, for top-level posts)
30 reposts avl.Tree // Board.id -> Post.id
31 threadID PostID // original Post.id
32 parentID PostID // parent Post.id (if reply or repost)
33 repostBoard BoardID // original Board.id (if repost)
34 createdAt time.Time
35 updatedAt time.Time
36}
37
38func newPost(board *Board, id PostID, creator address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post {
39 return &Post{
40 board: board,
41 id: id,
42 creator: creator,
43 title: title,
44 body: body,
45 replies: avl.Tree{},
46 repliesAll: avl.Tree{},
47 reposts: avl.Tree{},
48 threadID: threadID,
49 parentID: parentID,
50 repostBoard: repostBoard,
51 createdAt: time.Now(),
52 }
53}
54
55func (post *Post) IsThread() bool {
56 return post.parentID == 0
57}
58
59func (post *Post) GetPostID() PostID {
60 return post.id
61}
62
63func (post *Post) AddReply(creator address, body string) *Post {
64 board := post.board
65 pid := board.incGetPostID()
66 pidkey := postIDKey(pid)
67 reply := newPost(board, pid, creator, "", body, post.threadID, post.id, 0)
68 post.replies.Set(pidkey, reply)
69 if post.threadID == post.id {
70 post.repliesAll.Set(pidkey, reply)
71 } else {
72 thread := board.GetThread(post.threadID)
73 thread.repliesAll.Set(pidkey, reply)
74 }
75 return reply
76}
77
78func (post *Post) Update(title string, body string) {
79 post.title = title
80 post.body = body
81 post.updatedAt = time.Now()
82}
83
84func (thread *Post) GetReply(pid PostID) *Post {
85 pidkey := postIDKey(pid)
86 replyI, ok := thread.repliesAll.Get(pidkey)
87 if !ok {
88 return nil
89 } else {
90 return replyI.(*Post)
91 }
92}
93
94func (post *Post) AddRepostTo(creator address, title, body string, dst *Board) *Post {
95 if !post.IsThread() {
96 panic("cannot repost non-thread post")
97 }
98 pid := dst.incGetPostID()
99 pidkey := postIDKey(pid)
100 repost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id)
101 dst.threads.Set(pidkey, repost)
102 if !dst.IsPrivate() {
103 bidkey := boardIDKey(dst.id)
104 post.reposts.Set(bidkey, pid)
105 }
106 return repost
107}
108
109func (thread *Post) DeletePost(pid PostID) {
110 if thread.id == pid {
111 panic("should not happen")
112 }
113 pidkey := postIDKey(pid)
114 postI, removed := thread.repliesAll.Remove(pidkey)
115 if !removed {
116 panic("post not found in thread")
117 }
118 post := postI.(*Post)
119 if post.parentID != thread.id {
120 parent := thread.GetReply(post.parentID)
121 parent.replies.Remove(pidkey)
122 } else {
123 thread.replies.Remove(pidkey)
124 }
125}
126
127func (post *Post) HasPermission(addr address, perm Permission) bool {
128 if post.creator == addr {
129 switch perm {
130 case EditPermission:
131 return true
132 case DeletePermission:
133 return true
134 default:
135 return false
136 }
137 }
138 // post notes inherit permissions of the board.
139 return post.board.HasPermission(addr, perm)
140}
141
142func (post *Post) GetSummary() string {
143 return summaryOf(post.body, 80)
144}
145
146func (post *Post) GetURL() string {
147 if post.IsThread() {
148 return post.board.GetURLFromThreadAndReplyID(
149 post.id, 0)
150 } else {
151 return post.board.GetURLFromThreadAndReplyID(
152 post.threadID, post.id)
153 }
154}
155
156func (post *Post) GetReplyFormURL() string {
157 return gRealmLink.Call("CreateReply",
158 "bid", post.board.id.String(),
159 "threadid", post.threadID.String(),
160 "postid", post.id.String(),
161 )
162}
163
164func (post *Post) GetRepostFormURL() string {
165 return gRealmLink.Call("CreateRepost",
166 "bid", post.board.id.String(),
167 "postid", post.id.String(),
168 )
169}
170
171func (post *Post) GetDeleteFormURL() string {
172 return gRealmLink.Call("DeletePost",
173 "bid", post.board.id.String(),
174 "threadid", post.threadID.String(),
175 "postid", post.id.String(),
176 )
177}
178
179func (post *Post) RenderSummary() string {
180 if post.repostBoard != 0 {
181 dstBoard := getBoard(post.repostBoard)
182 if dstBoard == nil {
183 panic("repostBoard does not exist")
184 }
185 thread := dstBoard.GetThread(PostID(post.parentID))
186 if thread == nil {
187 return "reposted post does not exist"
188 }
189 return "Repost: " + post.GetSummary() + "\n" + thread.RenderSummary()
190 }
191 str := ""
192 if post.title != "" {
193 str += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n"
194 str += "\n"
195 }
196 str += post.GetSummary() + "\n"
197 str += "\\- " + displayAddressMD(post.creator) + ","
198 str += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")"
199 str += " \\[[x](" + post.GetDeleteFormURL() + ")]"
200 str += " (" + strconv.Itoa(post.replies.Size()) + " replies)"
201 str += " (" + strconv.Itoa(post.reposts.Size()) + " reposts)" + "\n"
202 return str
203}
204
205func (post *Post) RenderPost(indent string, levels int) string {
206 if post == nil {
207 return "nil post"
208 }
209 str := ""
210 if post.title != "" {
211 str += indent + "# " + post.title + "\n"
212 str += indent + "\n"
213 }
214 str += indentBody(indent, post.body) + "\n" // TODO: indent body lines.
215 str += indent + "\\- " + displayAddressMD(post.creator) + ", "
216 str += "[" + post.createdAt.Format("2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")"
217 str += " \\[[reply](" + post.GetReplyFormURL() + ")]"
218 if post.IsThread() {
219 str += " \\[[repost](" + post.GetRepostFormURL() + ")]"
220 }
221 str += " \\[[x](" + post.GetDeleteFormURL() + ")]\n"
222 if levels > 0 {
223 if post.replies.Size() > 0 {
224 post.replies.Iterate("", "", func(key string, value any) bool {
225 str += indent + "\n"
226 str += value.(*Post).RenderPost(indent+"> ", levels-1)
227 return false
228 })
229 }
230 } else {
231 if post.replies.Size() > 0 {
232 str += indent + "\n"
233 str += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n"
234 }
235 }
236 return str
237}
238
239// render reply and link to context thread
240func (post *Post) RenderInner() string {
241 if post.IsThread() {
242 panic("unexpected thread")
243 }
244 threadID := post.threadID
245 // replyID := post.id
246 parentID := post.parentID
247 str := ""
248 str += "_[see thread](" + post.board.GetURLFromThreadAndReplyID(
249 threadID, 0) + ")_\n\n"
250 thread := post.board.GetThread(post.threadID)
251 var parent *Post
252 if thread.id == parentID {
253 parent = thread
254 } else {
255 parent = thread.GetReply(parentID)
256 }
257 str += parent.RenderPost("", 0)
258 str += "\n"
259 str += post.RenderPost("> ", 5)
260 return str
261}