Search Apps Documentation Source Content File Folder Download Copy Actions Download

post.gno

8.99 Kb · 321 lines
  1package social
  2
  3import (
  4	"bytes"
  5	"strconv"
  6	"time"
  7
  8	"gno.land/p/moul/txlink"
  9	"gno.land/p/nt/avl/v0"
 10	"gno.land/p/nt/ufmt/v0"
 11)
 12
 13//----------------------------------------
 14// Post
 15
 16// NOTE: a PostID is relative to the userPosts.
 17type PostID uint64
 18
 19func (pid PostID) String() string {
 20	return strconv.Itoa(int(pid))
 21}
 22
 23// Reaction is for the "enum" of ways to react to a post
 24type Reaction int
 25
 26const (
 27	Gnod Reaction = iota
 28	MaxReaction
 29)
 30
 31// A Post is a "thread" or a "reply" depending on context.
 32// A thread is a Post of a UserPosts that holds other replies.
 33// This is similar to boards.Post except that this doesn't have a title.
 34type Post struct {
 35	userPosts  *UserPosts
 36	id         PostID
 37	creator    address
 38	body       string
 39	replies    avl.Tree  // PostID -> *Post
 40	repliesAll avl.Tree  // PostID -> *Post (all replies, for top-level posts)
 41	reposts    avl.Tree  // UserPosts user address -> PostID
 42	threadID   PostID    // original PostID
 43	parentID   PostID    // parent PostID (if reply or repost)
 44	repostUser address   // UserPosts user address of original post (if repost)
 45	reactions  *avl.Tree // Reaction -> *avl.Tree of address -> "" (Use the avl.Tree keys as the "set" of addresses)
 46	createdAt  time.Time
 47}
 48
 49func newPost(userPosts *UserPosts, id PostID, creator address, body string, threadID, parentID PostID, repostUser address) *Post {
 50	return &Post{
 51		userPosts:  userPosts,
 52		id:         id,
 53		creator:    creator,
 54		body:       body,
 55		replies:    avl.Tree{},
 56		repliesAll: avl.Tree{},
 57		reposts:    avl.Tree{},
 58		threadID:   threadID,
 59		parentID:   parentID,
 60		repostUser: repostUser,
 61		reactions:  avl.NewTree(),
 62		createdAt:  time.Now(),
 63	}
 64}
 65
 66func (post *Post) IsThread() bool {
 67	return post.parentID == 0
 68}
 69
 70func (post *Post) GetPostID() PostID {
 71	return post.id
 72}
 73
 74func (post *Post) AddReply(creator address, body string) *Post {
 75	userPosts := post.userPosts
 76	pid := userPosts.incGetPostID()
 77	pidkey := postIDKey(pid)
 78	reply := newPost(userPosts, pid, creator, body, post.threadID, post.id, "")
 79	post.replies.Set(pidkey, reply)
 80	if post.threadID == post.id {
 81		post.repliesAll.Set(pidkey, reply)
 82	} else {
 83		thread := userPosts.GetThread(post.threadID)
 84		thread.repliesAll.Set(pidkey, reply)
 85	}
 86	return reply
 87}
 88
 89func (post *Post) AddRepostTo(creator address, comment string, dst *UserPosts) *Post {
 90	if !post.IsThread() {
 91		panic("cannot repost non-thread post")
 92	}
 93
 94	pid := dst.incGetPostID()
 95	pidkey := postIDKey(pid)
 96	repost := newPost(dst, pid, creator, comment, pid, post.id, post.userPosts.userAddr)
 97	dst.threads.Set(pidkey, repost)
 98	// Also add to the home posts.
 99	dst.homePosts.Set(pidkey, repost)
100	post.reposts.Set(creator.String(), pid)
101	return repost
102}
103
104func (post *Post) GetReply(pid PostID) *Post {
105	pidkey := postIDKey(pid)
106	replyI, ok := post.repliesAll.Get(pidkey)
107	if !ok {
108		return nil
109	} else {
110		return replyI.(*Post)
111	}
112}
113
114// Add the userAddr to the posts.reactions for reaction.
115// Create the reaction key in post.reactions if needed.
116// If userAddr is already added, do nothing.
117// If the userAddr is the post's creator, do nothing. (Don't react to one's own posts.)
118// Return a boolean indicating whether the userAddr was added (false if it was already added).
119func (post *Post) AddReaction(userAddr address, reaction Reaction) bool {
120	validateReaction(reaction)
121
122	if userAddr == post.creator {
123		// Don't react to one's own posts.
124		return false
125	}
126	value := getOrCreateReactionValue(post.reactions, reaction)
127	if value.Has(userAddr.String()) {
128		// Already added.
129		return false
130	}
131
132	value.Set(userAddr.String(), "")
133	return true
134}
135
136// Remove the userAddr from the posts.reactions for reaction.
137// If userAddr is already removed, do nothing.
138// Return a boolean indicating whether the userAddr was found and removed.
139func (post *Post) RemoveReaction(userAddr address, reaction Reaction) bool {
140	validateReaction(reaction)
141
142	if !post.reactions.Has(reactionKey(reaction)) {
143		// There is no entry for reaction, so don't create one.
144		return false
145	}
146
147	_, removed := getOrCreateReactionValue(post.reactions, reaction).Remove(userAddr.String())
148	return removed
149}
150
151// Return the count of reactions for the reaction.
152func (post *Post) GetReactionCount(reaction Reaction) int {
153	key := reactionKey(reaction)
154	valueI, exists := post.reactions.Get(key)
155	if exists {
156		return valueI.(*avl.Tree).Size()
157	} else {
158		return 0
159	}
160}
161
162func validateReaction(reaction Reaction) {
163	if reaction < 0 || reaction >= MaxReaction {
164		panic("invalid Reaction value: " + strconv.Itoa(int(reaction)))
165	}
166}
167
168func (post *Post) GetSummary() string {
169	return summaryOf(post.body, 80)
170}
171
172func (post *Post) GetURL() string {
173	if post.IsThread() {
174		return post.userPosts.GetURLFromThreadAndReplyID(
175			post.id, 0)
176	} else {
177		return post.userPosts.GetURLFromThreadAndReplyID(
178			post.threadID, post.id)
179	}
180}
181
182func (post *Post) GetGnodFormURL() string {
183	return txlink.Call("AddReaction",
184		"userPostsAddr", post.userPosts.userAddr.String(),
185		"threadid", post.threadID.String(),
186		"postid", post.id.String(),
187		"reaction", strconv.Itoa(int(Gnod)))
188}
189
190func (post *Post) GetReplyFormURL() string {
191	return txlink.Call("PostReply",
192		"userPostsAddr", post.userPosts.userAddr.String(),
193		"threadid", post.threadID.String(),
194		"postid", post.id.String())
195}
196
197func (post *Post) GetRepostFormURL() string {
198	return txlink.Call("RepostThread",
199		"userPostsAddr", post.userPosts.userAddr.String(),
200		"threadid", post.threadID.String(),
201		"postid", post.id.String())
202}
203
204func (post *Post) RenderSummary() string {
205	if post.repostUser != "" {
206		dstUserPosts := getUserPosts(post.repostUser)
207		if dstUserPosts == nil {
208			panic("repost user does not exist")
209		}
210		thread := dstUserPosts.GetThread(PostID(post.parentID))
211		if thread == nil {
212			return "reposted post does not exist"
213		}
214		return "Repost: " + post.GetSummary() + "\n\n" + thread.RenderSummary()
215	}
216	str := ""
217	str += post.GetSummary() + "\n"
218	str += "\\- " + displayAddressMD(post.creator) + ","
219	str += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")"
220	str += " (" + strconv.Itoa(post.GetReactionCount(Gnod)) + " gnods)"
221	str += " (" + strconv.Itoa(post.replies.Size()) + " replies)"
222	str += " (" + strconv.Itoa(post.reposts.Size()) + " reposts)" + "\n"
223	return str
224}
225
226func (post *Post) RenderPost(indent string, levels int) string {
227	if post == nil {
228		return "nil post"
229	}
230	str := ""
231	str += indentBody(indent, post.body) + "\n" // TODO: indent body lines.
232	str += indent + "\\- " + displayAddressMD(post.creator) + ", "
233	str += "[" + post.createdAt.Format("2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")"
234	str += " - (" + strconv.Itoa(post.GetReactionCount(Gnod)) + " gnods) - "
235	str += " \\[[gnod](" + post.GetGnodFormURL() + ")]"
236	str += " \\[[reply](" + post.GetReplyFormURL() + ")]"
237	if post.IsThread() {
238		str += " \\[[repost](" + post.GetRepostFormURL() + ")]"
239	}
240	str += "\n"
241	if levels > 0 {
242		if post.replies.Size() > 0 {
243			post.replies.ReverseIterate("", "", func(key string, value interface{}) bool {
244				str += indent + "\n"
245				str += value.(*Post).RenderPost(indent+"> ", levels-1)
246				return false
247			})
248		}
249	} else {
250		if post.replies.Size() > 0 {
251			str += indent + "\n"
252			str += indent + "_[see all " + strconv.Itoa(post.replies.Size()) + " replies](" + post.GetURL() + ")_\n"
253		}
254	}
255	return str
256}
257
258// render reply and link to context thread
259func (post *Post) RenderInner() string {
260	if post.IsThread() {
261		panic("unexpected thread")
262	}
263	threadID := post.threadID
264	// replyID := post.id
265	parentID := post.parentID
266	str := ""
267	str += "_[see thread](" + post.userPosts.GetURLFromThreadAndReplyID(
268		threadID, 0) + ")_\n\n"
269	thread := post.userPosts.GetThread(post.threadID)
270	var parent *Post
271	if thread.id == parentID {
272		parent = thread
273	} else {
274		parent = thread.GetReply(parentID)
275	}
276	str += parent.RenderPost("", 0)
277	str += "\n"
278	str += post.RenderPost("> ", 5)
279	return str
280}
281
282// MarshalJSON implements the json.Marshaler interface.
283func (post *Post) MarshalJSON() ([]byte, error) {
284	createdAt, err := post.createdAt.MarshalJSON()
285	if err != nil {
286		return nil, err
287	}
288
289	json := new(bytes.Buffer)
290
291	json.WriteString(ufmt.Sprintf(`{"id": %d, "createdAt": %s, "creator": "%s", "n_gnods": %d, "n_replies": %d, "n_replies_all": %d, "parent_id": %d`,
292		uint64(post.id), string(createdAt), post.creator.String(), post.GetReactionCount(Gnod), post.replies.Size(), post.repliesAll.Size(),
293		uint64(post.parentID)))
294	if post.repostUser != "" {
295		json.WriteString(ufmt.Sprintf(`, "repost_user": %s`, strconv.Quote(post.repostUser.String())))
296	}
297	json.WriteString(ufmt.Sprintf(`, "body": %s}`, strconv.Quote(post.body)))
298
299	return json.Bytes(), nil
300}
301
302func getPosts(posts avl.Tree, startIndex int, endIndex int) string {
303	json := ufmt.Sprintf("{\"n_threads\": %d, \"posts\": [\n  ", posts.Size())
304
305	for i := startIndex; i < endIndex && i < posts.Size(); i++ {
306		if i > startIndex {
307			json += ",\n  "
308		}
309
310		_, postI := posts.GetByIndex(i)
311		post := postI.(*Post)
312		postJson, err := post.MarshalJSON()
313		if err != nil {
314			panic("can't get post JSON")
315		}
316		json += ufmt.Sprintf("{\"index\": %d, \"post\": %s}", i, string(postJson))
317	}
318
319	json += "]}"
320	return json
321}