Skip to content

Commit

Permalink
Merge pull request #93 from jefft0/chore/add-reactions-support
Browse files Browse the repository at this point in the history
chore: Add reactions support
  • Loading branch information
jefft0 authored Jun 13, 2024
2 parents e46f2cf + 15a8904 commit 16160b2
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 2 deletions.
80 changes: 78 additions & 2 deletions realm/post.gno
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ func (pid PostID) String() string {
return strconv.Itoa(int(pid))
}

// Reaction is for the "enum" of ways to react to a post
type Reaction int

const (
Gnod Reaction = iota
MaxReaction
)

// A Post is a "thread" or a "reply" depending on context.
// A thread is a Post of a UserPosts that holds other replies.
// This is similar to boards.Post except that this doesn't have a title.
Expand All @@ -36,6 +44,7 @@ type Post struct {
threadID PostID // original PostID
parentID PostID // parent PostID (if reply or repost)
repostUser std.Address // UserPosts user std.Address of original post (if repost)
reactions *avl.Tree // Reaction -> *avl.Tree of std.Address -> "" (Use the avl.Tree keys as the "set" of addresses)
createdAt time.Time
}

Expand All @@ -51,6 +60,7 @@ func newPost(userPosts *UserPosts, id PostID, creator std.Address, body string,
threadID: threadID,
parentID: parentID,
repostUser: repostUser,
reactions: avl.NewTree(),
createdAt: debugNow(),
}
}
Expand Down Expand Up @@ -103,6 +113,60 @@ func (post *Post) GetReply(pid PostID) *Post {
}
}

// Add the userAddr to the posts.reactions for reaction.
// Create the reaction key in post.reactions if needed.
// If userAddr is already added, do nothing.
// If the userAddr is the post's creator, do nothing. (Don't react to one's own posts.)
// Return a boolean indicating whether the userAddr was added (false if it was already added).
func (post *Post) AddReaction(userAddr std.Address, reaction Reaction) bool {
validateReaction(reaction)

if userAddr == post.creator {
// Don't react to one's own posts.
return false
}
value := getOrCreateReactionValue(post.reactions, reaction)
if value.Has(userAddr.String()) {
// Already added.
return false
}

value.Set(userAddr.String(), "")
return true
}

// Remove the userAddr from the posts.reactions for reaction.
// If userAddr is already removed, do nothing.
// Return a boolean indicating whether the userAddr was found and removed.
func (post *Post) RemoveReaction(userAddr std.Address, reaction Reaction) bool {
validateReaction(reaction)

if !post.reactions.Has(reactionKey(reaction)) {
// There is no entry for reaction, so don't create one.
return false
}

_, removed := getOrCreateReactionValue(post.reactions, reaction).Remove(userAddr.String())
return removed
}

// Return the count of reactions for the reaction.
func (post *Post) GetReactionCount(reaction Reaction) int {
key := reactionKey(reaction)
valueI, exists := post.reactions.Get(key)
if exists {
return valueI.(*avl.Tree).Size()
} else {
return 0
}
}

func validateReaction(reaction Reaction) {
if reaction < 0 || reaction >= MaxReaction {
panic("invalid Reaction value: " + strconv.Itoa(int(reaction)))
}
}

func (post *Post) GetSummary() string {
return summaryOf(post.body, 80)
}
Expand All @@ -117,6 +181,14 @@ func (post *Post) GetURL() string {
}
}

func (post *Post) GetGnodFormURL() string {
return "/r/berty/social?help&__func=AddReaction" +
"&userPostsAddr=" + post.userPosts.userAddr.String() +
"&threadid=" + post.threadID.String() +
"&postid=" + post.id.String() +
"&reaction=" + strconv.Itoa(int(Gnod))
}

func (post *Post) GetReplyFormURL() string {
return "/r/berty/social?help&__func=PostReply" +
"&userPostsAddr=" + post.userPosts.userAddr.String() +
Expand Down Expand Up @@ -149,6 +221,7 @@ func (post *Post) RenderSummary() string {
str += post.GetSummary() + "\n"
str += "\\- " + displayAddressMD(post.creator) + ","
str += " [" + post.createdAt.Format("2006-01-02 3:04pm MST") + "](" + post.GetURL() + ")"
str += " (" + strconv.Itoa(post.GetReactionCount(Gnod)) + " gnods)"
str += " (" + strconv.Itoa(post.replies.Size()) + " replies)"
str += " (" + strconv.Itoa(post.reposts.Size()) + " reposts)" + "\n"
return str
Expand All @@ -162,6 +235,8 @@ func (post *Post) RenderPost(indent string, levels int) string {
str += indentBody(indent, post.body) + "\n" // TODO: indent body lines.
str += indent + "\\- " + displayAddressMD(post.creator) + ", "
str += "[" + post.createdAt.Format("2006-01-02 3:04pm (MST)") + "](" + post.GetURL() + ")"
str += " - (" + strconv.Itoa(post.GetReactionCount(Gnod)) + " gnods) - "
str += " \\[[gnod](" + post.GetGnodFormURL() + ")]"
str += " \\[[reply](" + post.GetReplyFormURL() + ")]"
if post.IsThread() {
str += " \\[[repost](" + post.GetRepostFormURL() + ")]"
Expand Down Expand Up @@ -217,8 +292,9 @@ func (post *Post) MarshalJSON() ([]byte, error) {

json := new(bytes.Buffer)

json.WriteString(ufmt.Sprintf(`{"id": %d, "createdAt": %s, "creator": "%s", "n_replies": %d, "n_replies_all": %d, "parent_id": %d`,
uint64(post.id), string(createdAt), post.creator.String(), post.replies.Size(), post.repliesAll.Size(), uint64(post.parentID)))
json.WriteString(ufmt.Sprintf(`{"id": %d, "createdAt": %s, "creator": "%s", "n_gnods": %d, "n_replies": %d, "n_replies_all": %d, "parent_id": %d`,
uint64(post.id), string(createdAt), post.creator.String(), post.GetReactionCount(Gnod), post.replies.Size(), post.repliesAll.Size(),
uint64(post.parentID)))
if post.repostUser != "" {
json.WriteString(ufmt.Sprintf(`, "repost_user": %s`, strconv.Quote(post.repostUser.String())))
}
Expand Down
60 changes: 60 additions & 0 deletions realm/public.gno
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,66 @@ func Unfollow(followedAddr std.Address) {
userPosts.Unfollow(followedAddr)
}

// Add the reaction by the caller to the post of userPostsAddr, where threadid is the ID
// returned by the original call to PostMessage. If postid == threadid then add the reaction
// to a top-level post for the threadid, otherwise add the reaction to the postid "sub reply".
// (This function's arguments are similar to PostReply.)
// The caller must already be registered with /r/demo/users Register.
// Return a boolean indicating whether the userAddr was added. See Post.AddReaction.
func AddReaction(userPostsAddr std.Address, threadid, postid PostID, reaction Reaction) bool {
caller := std.GetOrigCaller()
if usernameOf(caller) == "" {
panic("please register")
}
userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
panic("posts for userPostsAddr do not exist")
}
thread := userPosts.GetThread(threadid)
if thread == nil {
panic("threadid in user posts does not exist")
}
if postid == threadid {
return thread.AddReaction(caller, reaction)
} else {
post := thread.GetReply(postid)
if post == nil {
panic("postid does not exist")
}
return post.AddReaction(caller, reaction)
}
}

// Remove the reaction by the caller to the post of userPostsAddr, where threadid is the ID
// returned by the original call to PostMessage. If postid == threadid then remove the reaction
// from a top-level post for the threadid, otherwise remove the reaction from the postid "sub reply".
// (This function's arguments are similar to PostReply.)
// The caller must already be registered with /r/demo/users Register.
// Return a boolean indicating whether the userAddr was removed. See Post.RemoveReaction.
func RemoveReaction(userPostsAddr std.Address, threadid, postid PostID, reaction Reaction) bool {
caller := std.GetOrigCaller()
if usernameOf(caller) == "" {
panic("please register")
}
userPosts := getUserPosts(userPostsAddr)
if userPosts == nil {
panic("posts for userPostsAddr do not exist")
}
thread := userPosts.GetThread(threadid)
if thread == nil {
panic("threadid in user posts does not exist")
}
if postid == threadid {
return thread.RemoveReaction(caller, reaction)
} else {
post := thread.GetReply(postid)
if post == nil {
panic("postid does not exist")
}
return post.RemoveReaction(caller, reaction)
}
}

// Call users.GetUserByAddress and return the result as JSON, or "" if not found.
// (This is a temporary utility until gno.land supports returning structured data directly.)
func GetJsonUserByAddress(addr std.Address) string {
Expand Down
19 changes: 19 additions & 0 deletions realm/util.gno
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strconv"
"strings"

"gno.land/p/demo/avl"
"gno.land/p/demo/ufmt"
p_users "gno.land/p/demo/users"
"gno.land/r/demo/users"
Expand Down Expand Up @@ -60,6 +61,24 @@ func postIDKey(pid PostID) string {
return padZero(uint64(pid), 10)
}

func reactionKey(reaction Reaction) string {
return strconv.Itoa(int(reaction))
}

// If reactions has an value for the given reaction, then return it.
// Otherwise, add the reaction key to reactions, set the value to an empty avl.Tree and return it.
func getOrCreateReactionValue(reactions *avl.Tree, reaction Reaction) *avl.Tree {
key := reactionKey(reaction)
valueI, exists := reactions.Get(key)
if exists {
return valueI.(*avl.Tree)
} else {
value := avl.NewTree()
reactions.Set(key, value)
return value
}
}

func indentBody(indent string, body string) string {
lines := strings.Split(body, "\n")
res := ""
Expand Down

0 comments on commit 16160b2

Please sign in to comment.