github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/examples/gno.land/r/demo/boards/post.gno (about) 1 package boards 2 3 import ( 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. 15 type PostID uint64 16 17 func (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. 23 type 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 39 func 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 56 func (post *Post) IsThread() bool { 57 return post.parentID == 0 58 } 59 60 func (post *Post) GetPostID() PostID { 61 return post.id 62 } 63 64 func (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 79 func (post *Post) Update(title string, body string) { 80 post.title = title 81 post.body = body 82 post.updatedAt = time.Now() 83 } 84 85 func (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 95 func (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 110 func (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 128 func (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 143 func (post *Post) GetSummary() string { 144 return summaryOf(post.body, 80) 145 } 146 147 func (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 157 func (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 165 func (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 174 func (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 181 func (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 207 func (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 242 func (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 }