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