Search Apps Documentation Source Content File Folder Download Copy

post.gno

6.56 Kb ยท 263 lines
  1package boards
  2
  3import (
  4	"std"
  5	"strconv"
  6	"time"
  7
  8	"gno.land/p/demo/avl"
  9	"gno.land/p/moul/txlink"
 10)
 11
 12//----------------------------------------
 13// Post
 14
 15// NOTE: a PostID is relative to the board.
 16type PostID uint64
 17
 18func (pid PostID) String() string {
 19	return strconv.Itoa(int(pid))
 20}
 21
 22// A Post is a "thread" or a "reply" depending on context.
 23// A thread is a Post of a Board that holds other replies.
 24type Post struct {
 25	board       *Board
 26	id          PostID
 27	creator     std.Address
 28	title       string // optional
 29	body        string
 30	replies     avl.Tree // Post.id -> *Post
 31	repliesAll  avl.Tree // Post.id -> *Post (all replies, for top-level posts)
 32	reposts     avl.Tree // Board.id -> Post.id
 33	threadID    PostID   // original Post.id
 34	parentID    PostID   // parent Post.id (if reply or repost)
 35	repostBoard BoardID  // original Board.id (if repost)
 36	createdAt   time.Time
 37	updatedAt   time.Time
 38}
 39
 40func newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post {
 41	return &Post{
 42		board:       board,
 43		id:          id,
 44		creator:     creator,
 45		title:       title,
 46		body:        body,
 47		replies:     avl.Tree{},
 48		repliesAll:  avl.Tree{},
 49		reposts:     avl.Tree{},
 50		threadID:    threadID,
 51		parentID:    parentID,
 52		repostBoard: repostBoard,
 53		createdAt:   time.Now(),
 54	}
 55}
 56
 57func (post *Post) IsThread() bool {
 58	return post.parentID == 0
 59}
 60
 61func (post *Post) GetPostID() PostID {
 62	return post.id
 63}
 64
 65func (post *Post) AddReply(creator std.Address, body string) *Post {
 66	board := post.board
 67	pid := board.incGetPostID()
 68	pidkey := postIDKey(pid)
 69	reply := newPost(board, pid, creator, "", body, post.threadID, post.id, 0)
 70	post.replies.Set(pidkey, reply)
 71	if post.threadID == post.id {
 72		post.repliesAll.Set(pidkey, reply)
 73	} else {
 74		thread := board.GetThread(post.threadID)
 75		thread.repliesAll.Set(pidkey, reply)
 76	}
 77	return reply
 78}
 79
 80func (post *Post) Update(title string, body string) {
 81	post.title = title
 82	post.body = body
 83	post.updatedAt = time.Now()
 84}
 85
 86func (thread *Post) GetReply(pid PostID) *Post {
 87	pidkey := postIDKey(pid)
 88	replyI, ok := thread.repliesAll.Get(pidkey)
 89	if !ok {
 90		return nil
 91	} else {
 92		return replyI.(*Post)
 93	}
 94}
 95
 96func (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post {
 97	if !post.IsThread() {
 98		panic("cannot repost non-thread post")
 99	}
100	pid := dst.incGetPostID()
101	pidkey := postIDKey(pid)
102	repost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id)
103	dst.threads.Set(pidkey, repost)
104	if !dst.IsPrivate() {
105		bidkey := boardIDKey(dst.id)
106		post.reposts.Set(bidkey, pid)
107	}
108	return repost
109}
110
111func (thread *Post) DeletePost(pid PostID) {
112	if thread.id == pid {
113		panic("should not happen")
114	}
115	pidkey := postIDKey(pid)
116	postI, removed := thread.repliesAll.Remove(pidkey)
117	if !removed {
118		panic("post not found in thread")
119	}
120	post := postI.(*Post)
121	if post.parentID != thread.id {
122		parent := thread.GetReply(post.parentID)
123		parent.replies.Remove(pidkey)
124	} else {
125		thread.replies.Remove(pidkey)
126	}
127}
128
129func (post *Post) HasPermission(addr std.Address, perm Permission) bool {
130	if post.creator == addr {
131		switch perm {
132		case EditPermission:
133			return true
134		case DeletePermission:
135			return true
136		default:
137			return false
138		}
139	}
140	// post notes inherit permissions of the board.
141	return post.board.HasPermission(addr, perm)
142}
143
144func (post *Post) GetSummary() string {
145	return summaryOf(post.body, 80)
146}
147
148func (post *Post) GetURL() string {
149	if post.IsThread() {
150		return post.board.GetURLFromThreadAndReplyID(
151			post.id, 0)
152	} else {
153		return post.board.GetURLFromThreadAndReplyID(
154			post.threadID, post.id)
155	}
156}
157
158func (post *Post) GetReplyFormURL() string {
159	return txlink.Call("CreateReply",
160		"bid", post.board.id.String(),
161		"threadid", post.threadID.String(),
162		"postid", post.id.String(),
163	)
164}
165
166func (post *Post) GetRepostFormURL() string {
167	return txlink.Call("CreateRepost",
168		"bid", post.board.id.String(),
169		"postid", post.id.String(),
170	)
171}
172
173func (post *Post) GetDeleteFormURL() string {
174	return txlink.Call("DeletePost",
175		"bid", post.board.id.String(),
176		"threadid", post.threadID.String(),
177		"postid", post.id.String(),
178	)
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}