github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/examples/gno.land/r/demo/boards/post.gno (about)

     1  package boards
     2  
     3  import (
     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.
    15  type PostID uint64
    16  
    17  func (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.
    23  type 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  
    39  func 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  
    56  func (post *Post) IsThread() bool {
    57  	return post.parentID == 0
    58  }
    59  
    60  func (post *Post) GetPostID() PostID {
    61  	return post.id
    62  }
    63  
    64  func (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  
    79  func (post *Post) Update(title string, body string) {
    80  	post.title = title
    81  	post.body = body
    82  	post.updatedAt = time.Now()
    83  }
    84  
    85  func (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  
    95  func (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  
   110  func (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  
   128  func (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  
   143  func (post *Post) GetSummary() string {
   144  	return summaryOf(post.body, 80)
   145  }
   146  
   147  func (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  
   157  func (post *Post) GetReplyFormURL() string {
   158  	return "/r/demo/boards?help&__func=CreateReply" +
   159  		"&bid=" + post.board.id.String() +
   160  		"&threadid=" + post.threadID.String() +
   161  		"&postid=" + post.id.String() +
   162  		"&body.type=textarea"
   163  }
   164  
   165  func (post *Post) GetRepostFormURL() string {
   166  	return "/r/demo/boards?help&__func=CreateRepost" +
   167  		"&bid=" + post.board.id.String() +
   168  		"&postid=" + post.id.String() +
   169  		"&title.type=textarea" +
   170  		"&body.type=textarea" +
   171  		"&dstBoardID.type=textarea"
   172  }
   173  
   174  func (post *Post) GetDeleteFormURL() string {
   175  	return "/r/demo/boards?help&__func=DeletePost" +
   176  		"&bid=" + post.board.id.String() +
   177  		"&threadid=" + post.threadID.String() +
   178  		"&postid=" + post.id.String()
   179  }
   180  
   181  func (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  
   207  func (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
   242  func (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  }