post.gno

6.54 Kb ยท 262 lines
  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 gRealmLink.Call("CreateReply",
159		"bid", post.board.id.String(),
160		"threadid", post.threadID.String(),
161		"postid", post.id.String(),
162	)
163}
164
165func (post *Post) GetRepostFormURL() string {
166	return gRealmLink.Call("CreateRepost",
167		"bid", post.board.id.String(),
168		"postid", post.id.String(),
169	)
170}
171
172func (post *Post) GetDeleteFormURL() string {
173	return gRealmLink.Call("DeletePost",
174		"bid", post.board.id.String(),
175		"threadid", post.threadID.String(),
176		"postid", post.id.String(),
177	)
178}
179
180func (post *Post) RenderSummary() string {
181	if post.repostBoard != 0 {
182		dstBoard := getBoard(post.repostBoard)
183		if dstBoard == nil {
184			panic("repostBoard does not exist")
185		}
186		thread := dstBoard.GetThread(PostID(post.parentID))
187		if thread == nil {
188			return "reposted post does not exist"
189		}
190		return "Repost: " + post.GetSummary() + "\n" + thread.RenderSummary()
191	}
192	str := ""
193	if post.title != "" {
194		str += "## [" + summaryOf(post.title, 80) + "](" + post.GetURL() + ")\n"
195		str += "\n"
196	}
197	str += post.GetSummary() + "\n"
198	str += "\\- " + displayAddressMD(post.creator) + ","
199	str += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")"
200	str += " \\[[x](" + post.GetDeleteFormURL() + ")]"
201	str += " (" + strconv.Itoa(post.replies.Size()) + " replies)"
202	str += " (" + strconv.Itoa(post.reposts.Size()) + " reposts)" + "\n"
203	return str
204}
205
206func (post *Post) RenderPost(indent string, levels int) string {
207	if post == nil {
208		return "nil post"
209	}
210	str := ""
211	if post.title != "" {
212		str += indent + "# " + post.title + "\n"
213		str += indent + "\n"
214	}
215	str += indentBody(indent, post.body) + "\n" // TODO: indent body lines.
216	str += indent + "\\- " + displayAddressMD(post.creator) + ", "
217	str += "[" + post.createdAt.Format("2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")"
218	str += " \\[[reply](" + post.GetReplyFormURL() + ")]"
219	if post.IsThread() {
220		str += " \\[[repost](" + post.GetRepostFormURL() + ")]"
221	}
222	str += " \\[[x](" + post.GetDeleteFormURL() + ")]\n"
223	if levels > 0 {
224		if post.replies.Size() > 0 {
225			post.replies.Iterate("", "", func(key string, value any) bool {
226				str += indent + "\n"
227				str += value.(*Post).RenderPost(indent+"> ", levels-1)
228				return false
229			})
230		}
231	} else {
232		if post.replies.Size() > 0 {
233			str += indent + "\n"
234			str += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n"
235		}
236	}
237	return str
238}
239
240// render reply and link to context thread
241func (post *Post) RenderInner() string {
242	if post.IsThread() {
243		panic("unexpected thread")
244	}
245	threadID := post.threadID
246	// replyID := post.id
247	parentID := post.parentID
248	str := ""
249	str += "_[see thread](" + post.board.GetURLFromThreadAndReplyID(
250		threadID, 0) + ")_\n\n"
251	thread := post.board.GetThread(post.threadID)
252	var parent *Post
253	if thread.id == parentID {
254		parent = thread
255	} else {
256		parent = thread.GetReply(parentID)
257	}
258	str += parent.RenderPost("", 0)
259	str += "\n"
260	str += post.RenderPost("> ", 5)
261	return str
262}