Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}