diff --git a/examples/gno.land/p/demo/boardsv2/boardsv2.gno b/examples/gno.land/p/demo/boardsv2/boardsv2.gno deleted file mode 100644 index 4ad3d466272..00000000000 --- a/examples/gno.land/p/demo/boardsv2/boardsv2.gno +++ /dev/null @@ -1 +0,0 @@ -package boardsv2 \ No newline at end of file diff --git a/examples/gno.land/p/demo/boardsv2/draft0/app.gno b/examples/gno.land/p/demo/boardsv2/draft0/app.gno new file mode 100644 index 00000000000..28017f895c8 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/app.gno @@ -0,0 +1,64 @@ +package boardsv2 + +import ( + "gno.land/demo/p/boardsv2/post" + contentplugin "gno.land/demo/p/boardsv2/post/plugins/content" +) + +// type Rating struct{} +// +// var ratingIndex = avl.Tree{} +// app.AddBoardHook(func (changeType int, change ChangeSet) { +// if changeType == 0 { +// ratingIndex.Set("...", ) +// } +// }) + +type App struct { + st Storage + boards []Board +} + +func New(s Storage, o ...Option) App { + a := App{ + st: Storage, + } + // Define the rule for a spesific view. + boardsView := view.New(view.Filter{ + Level: 0, // this will give me the list of the boards. + }) + + return a +} + +func (a *App) AddBoard(name, title, description string) (*Board, error) { + p := post.New(contentplugin.TitleBasedContent{ + Title: title, + Description: description, + }) + + // I want to create a query for listing threads under this new board. + threadView := view.New(view.Filter{ + Level: 1, + SlugPrefix: name, + }) + userActivityView := view.New(view.Filter{ + LevelGte: 2, + By: func(content Content) []View { + c.Author // by account address + } + }) + + if err := post.Add(a.st, name, p); err != nil { + nil, err + } + return a.GetBoard(name), nil +} + +func (a *App) GetBoard(name string) (board *Board, found bool) { + +} + +func (a *App) ListBoards() ([]*Board, error) { + +} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/board.gno b/examples/gno.land/p/demo/boardsv2/draft0/board.gno new file mode 100644 index 00000000000..e56895a9b1d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/board.gno @@ -0,0 +1,24 @@ +package boardsv2 + +type Board struct { +} + +func (b *Board) AddPost() error { + +} + +func (b *Board) GetThread(id string) (post *Post, found bool) { + +} + +func (b *Board) ListThreads(id string) (post *Post, found bool) { + threadView.List() // there should be an iterator, pagination +} + +func (b *Board) Fork() error { + +} + +func (b *Board) Lock() error { + +} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno b/examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno new file mode 100644 index 00000000000..91c308f9576 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/boardsv2.gno @@ -0,0 +1,7 @@ +// boardsv2 is a reddit like abstraction around post/*. +// You might implement other abstractions around post/* to create +// different type of dApps. +// refer to the app.gno file to get started. +package boardsv2 + + diff --git a/examples/gno.land/p/demo/boardsv2/draft0/option.gno b/examples/gno.land/p/demo/boardsv2/draft0/option.gno new file mode 100644 index 00000000000..b8d7b65c643 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/option.gno @@ -0,0 +1,13 @@ +package boardsv2 + +type Option struct{} + +// LinearReputationPolicy allows upvoting or downvoting a post by one +// for each account. +func LinearReputationPolicy() Option {} + +// TokenBasedReputationPolicy allows upvoting or downvoting a post propotional +// to the specified tokens that an account holds. +func TokenBasedReputationPolicy() Option {} + +// TODO: make it configurable how many levels allowed diff --git a/examples/gno.land/p/demo/boardsv2/post/avlstorage/avlstorage.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/avlstorage/avlstorage.gno similarity index 100% rename from examples/gno.land/p/demo/boardsv2/post/avlstorage/avlstorage.gno rename to examples/gno.land/p/demo/boardsv2/draft0/post/avlstorage/avlstorage.gno diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/content.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/content.gno new file mode 100644 index 00000000000..cbfd45c36fe --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/post/content.gno @@ -0,0 +1,5 @@ +package post + +type Content interface { + Render() string +} diff --git a/examples/gno.land/p/demo/boardsv2/post/plugin.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/plugin.gno similarity index 100% rename from examples/gno.land/p/demo/boardsv2/post/plugin.gno rename to examples/gno.land/p/demo/boardsv2/draft0/post/plugin.gno diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno new file mode 100644 index 00000000000..05043e7e172 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/post/plugins/content/content.gno @@ -0,0 +1,30 @@ +package commentplugin + +type CommentContent struct { + Body string +} + +func (c CommentContent) Render() string { + +} + +type TitleBasedContent struct{} + +type TextContent struct { + Title string + Body string + Tags []string +} + +func (c TextContent) Render() string { + +} + +type PollContent struct { + Question string + Options []string + Votes []struct { + Address std.Adress + Option string + } +} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/post.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/post.gno new file mode 100644 index 00000000000..d103579d437 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/post/post.gno @@ -0,0 +1,90 @@ +package post + +import "time" + +/* +alicePost = &Post { content: "foo" } (0x001) +bobFork := &Post { Origial: alicePost (0x001) } + +//1. Check gc behavior in realm for forks + +--- +alicePost := &(*alicePost) (0x002) +alicePost.content = "new content" + +bobFork := &Post { Origial: uintptr(0x001) } +--- +type Post struct { + ID int + Level int +} + +package reddit + +// explore with plugins +// - boardsv2 +// - pkg/general +// - pkg/reddit +var ( + rating avl.Tree +) + +genericPost := Post{} +reddit.UpvotePost(genericPost.ID) +*/ + +// Blog example +// Home +// - post 1 (content: title, body, author, label, timestamp) +// - post 1.1 (body, author) (thread) +// - post 1.1.1 (comment to a thread but also a new thread) +// - post 1.1.1.1 +// - post 1.2 (thread) +// +// - post 2 +// - post 3 +// +// Reddit example +// Home +// - post 1 (title, body) (board) +// - post 1.1 (title, body) (sub-board) +// - post 1.1.1 (title, body, label) +// - post 1.1.1.1 (comment, thread) +type Post struct { + ID string + Content Content // title, body, label, author, other metadata... + Level int + Base *Post + Children []*Post + Forks []*Post + UpdatedAt time.Time + CreatedAt time.Time // the time when created by user or forked. + Creator std.Address +} + +// create plugins for Post type < +// upvoting < implement first plugin +// define public API for plugin, post packages and boardsv2 +// moderation +// +// plugin ideas: +// - visibility +// - upcoting +// - acess control > you shouldn't be able to answer to the boards yo're not invited +// - moedaration (ban certain posts -this could be through a dao in the future) + +func New(s Storage) Post { + +} + +func Create(c Content) *Post { + +} + +func (p *Post) NextIncrementalKey(base string) string { + +} + +// func (p *Post) Append() error { +// +// } diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno new file mode 100644 index 00000000000..49a5f7eef32 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/post/storage.gno @@ -0,0 +1,4 @@ +package post + +type Storage interface { +} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/post/view.gno b/examples/gno.land/p/demo/boardsv2/draft0/post/view.gno new file mode 100644 index 00000000000..3921e441039 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/post/view.gno @@ -0,0 +1,10 @@ +package post + +// Two cases to solve +// - Give me a list of boards (board list page) +// - Give me a list of comments, created by a user accross all boards (user activity page, of a user) +type View interface { + Name() string + Size() int + Iterate(start, end string, fn func(key string, v interface{}) bool) bool +} diff --git a/examples/gno.land/p/demo/boardsv2/draft0/thread.gno b/examples/gno.land/p/demo/boardsv2/draft0/thread.gno new file mode 100644 index 00000000000..0ecad4ae41d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft0/thread.gno @@ -0,0 +1,30 @@ +package boardsv2 + +import ( + "gno.land/demo/p/boardsv2/post" + replyplugin "gno.land/demo/p/boardsv2/post/plugins/content/reply" +) + +type Thread struct { + post post.Post + st Store +} + +func (p *Thread) Comment(creator std.Address, message string) (id string, err error) { + pp := p.New(replyplugin.MessageContent{ + Message: message, + }) + id := p.post.NextIncrementalKey(creator.String()) // Post.ID/address/1 = "comment ID" + if err := post.Add(p.st, id); err != nil { + return "", err + } + return id, nil +} + +func (p *Thread) Upvote() error { + +} + +func (p *Thread) Downvote() error { + +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/boards.gno b/examples/gno.land/p/demo/boardsv2/draft1/boards.gno new file mode 100644 index 00000000000..f05972c0879 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/boards.gno @@ -0,0 +1,69 @@ +package boardsv2 + +import ( + "strconv" +) + +// TODO: Locking +// - Locking a board means, you can not create new threads, and you can not comment in existing ones +// - Locking a thread means, you can not comment in this thread anymore + +// TODO: Move boards (or App) to `boardsv2` +type Boards struct { + // NOTE: We might want different AVL trees to avoid using level prefixes + posts avl.Tree // string(Post.Level + Post.CreatedAt + slug) -> *Post (post, comment, poll) + locking lockingplugin.Plugin +} + +// TODO: Support pagination Start/End (see pager implementation) +func (b Boards) Iterate(level int, path string, fn func(*Post) bool) bool {} +func (b Boards) ReverseIterate(level int, path string, fn func(*Post) bool) bool {} + +func (b *Boards) Lock(path string) { + post := b.Get(LevelBoard, path) // Otherwise we try LevelPost + if err := b.locking.Lock(post); err != nil { + panic(err) + } +} + +// How to map render paths to actual post instances? +// +// AVL KEYS BY LEVEL PREFIX (start/end) +// Boards => 0_ ... 1_ +// Posts => 1_BOARD/ ... 2_ +// Comments => 2_BOARD/POST/ ... 3_ +// +// HOW TO GUESS PREFIX FROM SLUG +// User enters a SLUG => (one part => 1_BOARD)(more than one part => 1_BOARD/POST) +// How to recognize comments? Should be URL accesible? We could use ":" as separator (not optimal) +// +// LEVEL_BOARD/POST/POST-2/COMMENT/COMMENT-2 (deprecated) +// LEVEL_TIMESTAMP_BOARD/POST/COMMENT +// +// :board/post/comment + +func (b *Boards) Set(p *Post) (updated bool) { + key := newKey(p.Level, p.Slug()) + return b.posts.Set(key, p) +} + +func (b *Boards) Remove(level int, path string) (_ *Post, removed bool) { + key := newKey(level, path) + if v, removed := b.posts.Remove(key); removed { + return v.(*Post), true + } + return nil, false +} + +func (b Boards) Get(level int, path string) (_ *Post, found bool) { + key := newKey(level, path) + if v, found := b.posts.Get(key); found { + return v.(*Post), true + } + return "", false +} + +func newKey(level int, path string) string { + // TODO: Add timestamp to key + return strconv.Itoa(level) + "_" + path +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_board.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_board.gno new file mode 100644 index 00000000000..e59af9bc4cf --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/content_board.gno @@ -0,0 +1,30 @@ +package boardsv2 + +// TODO: Move content types to `boardsv2` API + +const ContentTypeBoard = "boards:board" + +var _ Content = (*BoardContent)(nil) + +type BoardContent struct { + Name string + Tags []string +} + +func NewBoard() *Post { + return &Post{ // TODO: Use a contructor to be able to use private fields (use options), NewPost + // ... + Level: LevelBoard, + Content: &BoardContent{ + // ... + }, + } +} + +func (c BoardContent) Type() string { + return ContentTypeBoard +} + +func (c BoardContent) Render() string { + return "" +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno new file mode 100644 index 00000000000..fcaac13c3e8 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/content_comment.gno @@ -0,0 +1,27 @@ +package boardsv2 + +const ContentTypeComment = "boards:comment" + +var _ Content = (*CommentContent)(nil) + +type CommentContent struct { + Body string +} + +func NewComment() *Post { + return &Post{ + // ... + Level: LevelComment, + Content: &CommentContent{ + // ... + }, + } +} + +func (c CommentContent) Type() string { + return ContentTypeComment +} + +func (c CommentContent) Render() string { + return "" +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno new file mode 100644 index 00000000000..1f3a0bc9846 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/content_poll.gno @@ -0,0 +1,33 @@ +package boardsv2 + +const ContentTypePoll = "boards:poll" + +var _ Content = (*PollContent)(nil) + +type PollContent struct { + Question string + Options []string + Votes []struct { + Address std.Adress + Option string + } + Tags []string +} + +func NewPoll( /* ... */ ) *Post { + return &Post{ + // ... + Level: LevelPost, + Content: &PollContent{ + // ... + }, + } +} + +func (c PollContent) Type() string { + return ContentTypePoll +} + +func (c PollContent) Render() string { + return "" +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/content_post.gno b/examples/gno.land/p/demo/boardsv2/draft1/content_post.gno new file mode 100644 index 00000000000..289b5ba0099 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/content_post.gno @@ -0,0 +1,29 @@ +package boardsv2 + +const ContentTypePost = "boards:post" + +var _ Content = (*TextContent)(nil) + +type TextContent struct { + Title string + Body string + Tags []string +} + +func NewPost() *Post { + return &Post{ + // ... + Level: LevelPost, + Content: &TextContent{ + // ... + }, + } +} + +func (c TextContent) Type() string { + return ContentTypePost +} + +func (c TextContent) Render() string { + return "" +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/features.gno b/examples/gno.land/p/demo/boardsv2/draft1/features.gno new file mode 100644 index 00000000000..c5dff81eee4 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/features.gno @@ -0,0 +1,69 @@ +package boardsv2 + +import "errors" + +func AddBoard(s PostStore, slug string /* ... */) (path string, _ error) { + // TODO: Finish implementation + + return slug, nil +} + +// NOTE: Define a pattern to add functionality to posts by type (AddComment, AddThread, AddPoll, Repost, Upvote, ...) +// NOTE: Maybe though functions that assert the right arguments +func AddComment(s PostStore, parentPath string, creator std.Address, message string) (path string, _ error) { + // Try to get parent as a post or a comment, otherwise parent doesn't support comments + p, found := s.Get(LevelPost, parentPath) + if !found { + p, found = s.Get(LevelComment, parentPath) + if !found { + return "", errors.New("parent post or comment not found: " + parentPath) + } + } + + // TODO: + // Call the IsLocked function from the plugin for both the board post and thread post + // of this new comment. And confirm that both of them are false + // if so, then proceed, otherwise can not add new comments because locked. + // level 0 - boards + // level 1 - thread + // level 2 - comment + // level 3 - comment under comment + // level 4 - comment under comment under comment + // ... + + // TODO: + // Consider using reverse iteration while checking IsLocked in parent levels. + // If the keys in the AVL tree has levels as the prefix it should be optimized. If + // timestamp is used it may not be. + + comment := NewComment(p /* ... */) + + // TODO: Finish implementation + s.Set( /* ... */ ) + + path = parentPath + "/" + comment.ID + return path, nil +} + +// NOTE: Arguments could potentially be many, consider variadic + sane defaults (?) +func AddThread(s PostStore, parentPath, slug string, creator std.Address /* ... */) (path string, _ error) { + p, found := b.Get(LevelPost, parentPath) + if !found { + return "", errors.New("parent post not found: " + parentPath) + } + + post := NewPost(p, slug /* ... */) + + // TODO: Finish implementation + + path = parentPath + "/" + post.ID + return path, nil +} + +// ----- Other features ----- +// type VotesStore interface { +// /*...*/ +// } +// +// func Upvote(s VotesStore /* ... */) {} +// func DownVote(s VotesStore /* ... */) {} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno b/examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno new file mode 100644 index 00000000000..ba23251a4ef --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/plugin/locking/locking.gno @@ -0,0 +1,50 @@ +package lockingplugin + +import "errors" + +const Name = "boards:locking" + +var ErrInvalidPostType = errors.New("post type is not a board or thread") + +type ( + Plugin struct{} + Storage struct { + IsLocked bool + } +) + +func New() Plugin { + return Plugin{} +} + +func (p Plugin) Name() string { + return Name +} + +func (p *Plugin) Lock(p *Post) error { + if !isBoardOrThread(p) { + return ErrInvalidPostType + } + + p.MustGetPluginStorage(p.Name()).(*Storage).IsLocked = true +} + +func (p *Plugin) Unlock(p *Post) error { + if !isBoardOrThread(p) { + return ErrInvalidPostType + } + + p.MustGetPluginStorage(p.Name()).(*Storage).IsLocked = false +} + +func (p Plugin) IsLocked(p *Post) bool { + if !isBoardOrThread(p) { + return ErrInvalidPostType + } + + return p.MustGetPluginStorage(p.Name()).(*Storage).IsLocked +} + +func isBoardOrThread(p *Post) bool { + return p.Level == LevelBoard || p.Level == LevelPost +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno b/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno new file mode 100644 index 00000000000..83287a53580 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/options.gno @@ -0,0 +1,15 @@ +package reputationplugin + +type Option func(*Plugin) + +func UseTokenBasePolicy() Option { + return func(p *Plugin) { + p.Policy = PolicyTokenBase + } +} + +func AllowedPostLevels(levels []int) Option { + return func(p *Plugin) { + p.AllowedPostLevels = levels + } +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno b/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno new file mode 100644 index 00000000000..13fcfa3facd --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/plugin/reputation/reputation.gno @@ -0,0 +1,77 @@ +package reputationplugin + +import "errors" + +const ( + PolicyLinear = iota + PolicyTokenBase +) + +const Name = "boards:reputation" + +var ErrNotSupported = errors.New("reputation not supported") + +type ( + Plugin struct { + Policy int + AllowedPostLevels []int + } + + Storage struct { + Upvotes uint + Downbotes uint + ListOfWhoVotedWhat avl.Tree // string(std.Address) -> ?? (TODO: define) + } +) + +func NewReputationPlugin(o ...Option) Plugin { + var p Plugin + for _, apply := range o { + apply(&p) + } + return p +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) HasReputationSupport(p *Post) bool { + if len(p.AllowedPostLevels) == 0 { + return true + } + + for _, lvl := range p.AllowedPostLevels { + if p.Level == lvl { + return true + } + } + return false +} + +func (p *Plugin) Votes(p *Post) uint32 { + if !p.HasReputationSupport(p) { + return ErrNotSupported + } + + // TODO: Implement +} + +func (p *Plugin) Upvote(p *Post) error { + if !p.HasReputationSupport(p) { + return ErrNotSupported + } + + // TODO: Modify global state + // TODO: Modify local state + // TODO: Implement + st := p.MustGetPluginStorage(p.Name()).(*Storage) +} + +func (p *Plugin) Downvote(p *Post) error { + if !p.HasReputationSupport(p) { + return ErrNotSupported + } + + // TODO: Implement +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/post.gno b/examples/gno.land/p/demo/boardsv2/draft1/post.gno new file mode 100644 index 00000000000..1498cda066f --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/post.gno @@ -0,0 +1,45 @@ +package boardsv2 + +import ( + "strconv" + "time" +) + +const ( + LevelBoard = iota + LevelPost + LevelComment +) + +type ( + Content interface { + Type() string + Render() string + } + + Post struct { + ID string + Content Content + PluginStorage avl.Tree // string(plugin name) -> interface{}(plugin storage) + Parent *Post + Level int + Base *Post + Children []*Post + Forks []*Post + UpdatedAt time.Time + CreatedAt time.Time + Creator std.Address + } +) + +func (p Post) MustGetPluginStorage(name string) interface{} { + if v, found := p.pluginStorage.Get(name); found { + return v + } + + panic("plugin storage not found: " + name) +} + +func (p Post) NextIncrementalKey(baseKey string) string { + return baseKey + "/" + strconv.Itoa(len(p.Children)) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft1/store.gno b/examples/gno.land/p/demo/boardsv2/draft1/store.gno new file mode 100644 index 00000000000..f8c98f3e724 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft1/store.gno @@ -0,0 +1,9 @@ +package boardsv2 + +// NOTE: Maybe we could abstract the location where posts are stored +type PostStore interface { + Set(*Post) (updated bool) + Get(level int, path string) (_ *Post, found bool) // NOTE: Level could be a type alias for better semantics + + // TODO: Add iterator (or define PostIterator interface) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/app.gno b/examples/gno.land/p/demo/boardsv2/draft2/app.gno new file mode 100644 index 00000000000..30125302302 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/app.gno @@ -0,0 +1,91 @@ +package boards + +import ( + "plugin" + + "golang.org/x/mod/sumdb/storage" +) + +const ( + LevelBoard = iota + LevelThread + LevelComment +) + +type App struct { + cntx Context +} + +func New(st storage.PostStorage, o ...Option) App { + // TODO: avl.Tree feels wrong here but maybe get rid of the map anyway + p := map[plugin.Name]plugin.Plugin{ + plugintitle.Name: plugintitle.New(st), // content for boards + plugintext.Name: plugintext.New(st),// content for text based threads + pluginpoll.Name: pluginpoll.New(st),// content for poll based threads + plugincomment.Name: plugincomment.New(st),// content for comments to the threads + } + + c := Context{ + storage: st, + plugins: p, + } + + a := App{ + cntx: c, + } + + return a +} + +func (a App) Board(path string) (Board, error) { + a.c.Get(level, path func(){}) +} + +func (a App) Boards(c post.Cursor) ([]Board, error) { + +} + +func (a App) CreateBoard(c BoardContent) (Board, error) {} + +func (b App) Thread(path string) (Thread, error) { + return ThreadWithComments(path, nil) +} + +// Fork forks either a board or a thread by their path. +func (a App) Fork(path, newPath string) error {} + +// Lock locks either a board or a thread by their path. +// Once a board is locked new threads to the board and comments to the existing +// threads won't be allowed. +// Once a thread is locked new comments to the thread won't be allowed. +func (a App) Lock(path string) error {} + + +// ThreadWithComments returns a thread with its comments with the comment depth +// configured with commentDepth for direct and child comments. +// For ex. +// To get a thread with only 10 direct (parent level) comments use: +// - []int{10} +// To get a thread with 10 direct comments and 3 of their child comments use: +// - []int{10, 3} +// You can define configure this for more levels until you reach to value defined +// by MaxCommentDepth. +// By default the configuration is as follows: +// - []int{20, 3} +func (b App) ThreadWithComments(path string, commentDepth []int) (Thread, error) {} +func (b App) Threads(c post.Cursor) ([]Thread, error) {} +func (b App) CreateTextThread(c ThreadTextContent) (Thread, error) {} +func (b App) CreatePollThread(c ThreadPollContent) (Thread, error) {} + +// parentPath could be a path to thread (root), or path to any of the +// nested comments. +func (b App) Comments(parentPath string, c Cursor) ([]Comment, error) {} +func (b App) CreateComment(path string, c plugincomment.Content) (Comment, error) { + post, err := a.c.Plugin(plugincomment.Name).NewPost(c, LevelComment) + if err != nil { + return Comment{}, err + } + return Comment{Post: post, c: a.c} +} + +func (a App) Render(path string) string {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/board.gno b/examples/gno.land/p/demo/boardsv2/draft2/board.gno new file mode 100644 index 00000000000..ac9258fa629 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/board.gno @@ -0,0 +1,14 @@ +package boards + +type Board struct { + post.Post + c Context +} + +func (b Board) Content() BoardContent { + return b.c.Plugin(pluginbasiccontent.Name).Content(b.Post) +} + +func (b Board) Render() string { + +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/comment.gno b/examples/gno.land/p/demo/boardsv2/draft2/comment.gno new file mode 100644 index 00000000000..d66a77d1a67 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/comment.gno @@ -0,0 +1,8 @@ +package boards + +type Comment struct { + post.Post + c Context +} + +func (c Comment) Content() CommentContent {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/context.gno b/examples/gno.land/p/demo/boardsv2/draft2/context.gno new file mode 100644 index 00000000000..15c31e1db1d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/context.gno @@ -0,0 +1,38 @@ +package boards + +import "plugin" + +type Context struct { + opts []Option + st post.Storage + plugs map[post.PluginName]plugin.Plugin +} + +func newContext() Context { + +} + +func (c Context) Plugin(n post.PluginName) post.Plugin { + +} + +func (c Context) Set(p *Post) (updated bool) { + key := newKey(p.Level, p.Slug()) + return b.posts.Set(key, p) +} + +func (c Context) Remove(level int, path string) (_ *Post, removed bool) { + key := newKey(level, path) + if v, removed := b.posts.Remove(key); removed { + return v.(*Post), true + } + return nil, false +} + +func (c Context) Get(level int, path string, iterator func()) (_ *Post, found bool) { + key := newKey(level, path) + if v, found := b.posts.Get(key); found { + return v.(*Post), true + } + return "", false +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/option.gno b/examples/gno.land/p/demo/boardsv2/draft2/option.gno new file mode 100644 index 00000000000..48ab0cc1bff --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/option.gno @@ -0,0 +1,19 @@ +package boards + +type Option struct{} + +// LinearReputationPolicy allows upvoting or downvoting a post by one +// for each account. +func LinearReputationPolicy() Option {} + +// TokenBasedReputationPolicy allows upvoting or downvoting a post propotional +// to the specified tokens that an account holds. +func TokenBasedReputationPolicy() Option {} + +// MaxPostDepth configures the max depth for nested comments. +// 0 -> boards +// 1 -> threads +// 2 -> comments-1 (direct comments to the threads) +// The above are already reserved. +// Setting it to zero will disable comments. +func MaxCommentDepth(d int) Option {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno new file mode 100644 index 00000000000..44f8ebe85f6 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/cursor.gno @@ -0,0 +1,8 @@ +package post + +type Cursor struct { + FromID string + Count int +} + +func NewCursor(fromID string, count int) Cursor {} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno new file mode 100644 index 00000000000..2c4d7a53994 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/comment/comment.gno @@ -0,0 +1,34 @@ +package plugincomment + +const Name = "post-comment" + +func New(st Storage) Plugin { +} + +type Plugin struct { + postStorage Storage +} + +type Content struct { // Content of the comment. + Title string + Description string + Tags []string +} + +func (p Plugin) CreateComment(id string, c Content, level int) *post.Post { + pp := &post.Post{ + ID: id, + Level: level, + } + p.EditCommentContent(pp, c) + return pp +} + +func (p Plugin) Content(pst *post.Post) Content { + return pst.PluginStorage[Name].(Content) +} + +func (p Plugin) EditCommentContent(pp *Post, c Content) (updated bool) { + pp.PluginStorage[Name] = c + return p.postStorage.Set(post.ID, pp) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno new file mode 100644 index 00000000000..5cd488aaabe --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/lock/lock.gno @@ -0,0 +1 @@ +package pluginlock diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno new file mode 100644 index 00000000000..2f1cff7e627 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/plugin.gno @@ -0,0 +1,7 @@ +package plugin + +type Plugin interface { + Type() string +} + +type Name string diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno new file mode 100644 index 00000000000..db9c0bb40ce --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/poll/poll.gno @@ -0,0 +1 @@ +package pluginpoll diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno new file mode 100644 index 00000000000..155fc53850f --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/reputation/reputation.gno @@ -0,0 +1 @@ +package pluginreputation diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno new file mode 100644 index 00000000000..a9f41c39947 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/text/text.gno @@ -0,0 +1,3 @@ +package plugintext + +const Name = "post-text" diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno new file mode 100644 index 00000000000..da2f8e905b5 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/plugin/title/title.gno @@ -0,0 +1,24 @@ +package plugintitle + +const Name = "post-title-only" + +func New(st Storage) Plugin { +} + +type Plugin struct { + st Storage +} + +type Content struct { + Title string + Description string + Tags []string +} + +func (p Plugin) Content(post *Post) Content { + return post.Body[Name].(Content) +} + +func (p Plugin) SetContent(post *Post, c Content) { + post.Body[Name] = c +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/post.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/post.gno new file mode 100644 index 00000000000..04b6b88f48c --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/post.gno @@ -0,0 +1,26 @@ +package boardsv2 + +import ( + "plugin" + "strconv" + "time" +) + +type ( + Post struct { + ID string + PluginStore plugin.Plugin + Parent *Post + Level int + Base *Post + Children []*Post + Forks []*Post + UpdatedAt time.Time + CreatedAt time.Time + Creator std.Address + } +) + +func (p Post) NextIncrementalKey(baseKey string) string { + return baseKey + "/" + strconv.Itoa(len(p.Children)) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno b/examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno new file mode 100644 index 00000000000..72440ea2a61 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/post/store/store.gno @@ -0,0 +1 @@ +package store diff --git a/examples/gno.land/p/demo/boardsv2/draft2/thread.gno b/examples/gno.land/p/demo/boardsv2/draft2/thread.gno new file mode 100644 index 00000000000..42e731d4fd2 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft2/thread.gno @@ -0,0 +1,18 @@ +package boards + +type Thread struct { + post.Post + c Context +} + +func (t Thread) TextContent() ThreadTextContent { + +} + +func (t Thread) PollContent() ThreadPollContent {} +func (t Thread) Type() ContentType {} + +// Comments returns a list of comments sent to the thread. +// The comment slice will be non-nil only when Thread is initiated +// through ThreadWithComments. +func (t Thread) Comments() []Comment {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/app.gno b/examples/gno.land/p/demo/boardsv2/draft3/app.gno new file mode 100644 index 00000000000..3893e0866f7 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/app.gno @@ -0,0 +1,133 @@ +package boards + +import ( + "gno.land/p/demo/boards/post" + "gno.land/p/demo/boards/post/plugin" + pluginfork "gno.land/p/demo/boards/post/plugin/fork" + pluginpoll "gno.land/p/demo/boards/post/plugin/poll" + pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" + plugintitle "gno.land/p/demo/boards/post/plugin/title" +) + +const ( + LevelBoard = iota + LevelThread + LevelComment +) + +// App is the boards application. +type App struct { + posts post.Store + plugins *plugin.Registry + maxCommentsDepth int + reputationPolicy pluginreputation.Policy +} + +// New creates a new boards application. +func New(st post.Store, options ...Option) App { + app := App{ + posts: st, + maxCommentsDepth: -1, // Infinite number of comments + } + for _, apply := range options { + apply(&app) + } + + app.plugins := plugin.NewRegistry( + plugintitle.New(st), // Plugin for boards + plugintext.New(st), // Plugin for text based threads + pluginpoll.New(st), // Plugin for poll based threads + plugincomment.New(st), // Plugin for comments to the threads + pluginreputation.New( + pluginreputation.UsePolicy(app.reputationPolicy), + pluginreputation.AllowedPostLevels(post.LevelPost, post.LevelComment), + ), + pluginfork.New( + pluginfork.AllowedPostLevels(post.LevelPost), + ), + ) + return app +} + +func (a App) GetBoard(path string) (_ Board, found bool) { + p, found := a.posts.GetByLevel(path, LevelBoard) + if !found { + return Board{}, false + } + return Board{p}, true +} + +func (a App) GetThread(path string) (_ Thread, found bool) { + p, found := a.posts.GetByLevel(path, LevelThread) + if !found { + return Thread{}, false + } + return Thread{p}, true +} + +func (a App) GetComment(path string) (_ Comment, found bool) { + p, found := a.posts.GetByLevel(path, LevelComment) + if !found { + return Comment{}, false + } + return Comment{p}, true +} + +func (a App) CreateBoard(slug, title, description string, tags []string) (Board, error) {} + +// Fork forks either a board or a thread by their path. +func (a App) ForkBoard(b Board, newPath string) error { + // NOTE: Instead of `app.ForkBoard()` we could use `b.Fork(newPath)` instead but that requires Board to have plugin access + // NOTE: This case gets the plugin from the plugin list to fork + p, _ := a.plugins.Get(pluginfork.Name) + return p.Fork(b.Post, newPath) +} + +func (a App) ForkThread(t Thread, newPath string) error { + // TODO: Implement thread fork app support +} + +// Lock locks either a board or a thread by their path. +// Once a board is locked new threads to the board and comments to the existing +// threads won't be allowed. +// Once a thread is locked new comments to the thread won't be allowed. +func (a App) Lock(path string) error {} + +// ---- TODO: Review the following list of app methods ----- // + +func (a App) Boards(c post.Cursor) ([]Board, error) { +} + +func (b App) Thread(path string) (Thread, error) { + return ThreadWithComments(path, nil) +} + +// ThreadWithComments returns a thread with its comments with the comment depth +// configured with commentDepth for direct and child comments. +// For ex. +// To get a thread with only 10 direct (parent level) comments use: +// - []int{10} +// To get a thread with 10 direct comments and 3 of their child comments use: +// - []int{10, 3} +// You can define configure this for more levels until you reach to value defined +// by MaxCommentDepth. +// By default the configuration is as follows: +// - []int{20, 3} +func (b App) ThreadWithComments(path string, commentDepth []int) (Thread, error) {} +func (b App) Threads(c post.Cursor) ([]Thread, error) {} +func (b App) CreateTextThread(c ThreadTextContent) (Thread, error) {} +func (b App) CreatePollThread(c ThreadPollContent) (Thread, error) {} + +// parentPath could be a path to thread (root), or path to any of the +// nested comments. +func (b App) Comments(parentPath string, c Cursor) ([]Comment, error) {} + +func (b App) CreateComment(path string, c plugincomment.Content) (Comment, error) { + post, err := a.c.Plugin(plugincomment.Name).NewPost(c, LevelComment) + if err != nil { + return Comment{}, err + } + return Comment{Post: post, c: a.c} +} + +func (a App) Render(path string) string {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/board.gno b/examples/gno.land/p/demo/boardsv2/draft3/board.gno new file mode 100644 index 00000000000..b75db758a79 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/board.gno @@ -0,0 +1,52 @@ +package boards + +import ( + "gno.land/p/demo/boards/post" + pluginfork "gno.land/p/demo/boards/post/plugin/fork" + pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" + plugintitle "gno.land/p/demo/boards/post/plugin/title" +) + +type ( + BoardContent plugintitle.Content + + Board struct { + *post.Post + } +) + +func NewBoard(pst *post.Post) Board { + // TODO: Local plugins must be initialized here (same for other plugins) + return Board{pst} +} + +func (b Board) Info() BoardContent { + return BoardContent(b.getContent()) +} + +func (b Board) Update(c BoardContent) { + b.PluginStore[plugintitle.Name] = plugintitle.Content(c) +} + +func (b Board) Upvote() error { + r := b.getReputation() + return r.Upvote(b.Post) +} + +func (b Board) Downvote() error { + r := b.getReputation() + return r.Downvote(b.Post) +} + +func (b Board) Render() string { + c := b.getContent() + return c.Render() +} + +func (b Board) getContent() *plugintitle.Content { + return b.PluginStore[plugintitle.Name].(*plugintitle.Content) +} + +func (b Board) getReputation() *pluginreputation.Reputation { + return b.PluginStore[pluginreputation.Name].(*pluginreputation.Reputation) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/comment.gno b/examples/gno.land/p/demo/boardsv2/draft3/comment.gno new file mode 100644 index 00000000000..d66a77d1a67 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/comment.gno @@ -0,0 +1,8 @@ +package boards + +type Comment struct { + post.Post + c Context +} + +func (c Comment) Content() CommentContent {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/context.gno b/examples/gno.land/p/demo/boardsv2/draft3/context.gno new file mode 100644 index 00000000000..f3139abccb4 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/context.gno @@ -0,0 +1,36 @@ +package boards + +import "plugin" + +type Context struct { + opts []Option + st post.Store + plugs map[post.PluginName]plugin.Plugin +} + +func newContext() Context { +} + +func (c Context) Plugin(n post.PluginName) post.Plugin { +} + +func (c Context) Set(p *Post) (updated bool) { + key := newKey(p.Level, p.Slug()) + return b.posts.Set(key, p) +} + +func (c Context) Remove(level int, path string) (_ *Post, removed bool) { + key := newKey(level, path) + if v, removed := b.posts.Remove(key); removed { + return v.(*Post), true + } + return nil, false +} + +func (c Context) Get(level int, path string, iterator func()) (_ *Post, found bool) { + key := newKey(level, path) + if v, found := b.posts.Get(key); found { + return v.(*Post), true + } + return "", false +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/options.gno b/examples/gno.land/p/demo/boardsv2/draft3/options.gno new file mode 100644 index 00000000000..906b1130663 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/options.gno @@ -0,0 +1,31 @@ +package boards + +import ( + pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" +) + +// Option configures board applications. +type Options func(*App) + +// LinearReputationPolicy allows upvoting or downvoting a post by one for each account. +func LinearReputationPolicy() Option { + return func(a *App) { + a.reputationPolicy = pluginreputation.PolicyLinear + } +} + +// TokenBasedReputationPolicy allows upvoting or downvoting +// a post propotional to the specified tokens that an account holds. +func TokenBasedReputationPolicy() Option { + return func(a *App) { + a.reputationPolicy = pluginreputation.PolicyTokenBased + } +} + +// MaxCommentsDepth configures the max depth for nested comments. +// Setting it to -1 allows an infinite number of nested comments (default). +func MaxCommentsDepth(d int) Option { + return func(a *App) { + a.maxCommentsDepth = d + } +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno new file mode 100644 index 00000000000..f7b1eed2e5e --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/comment/comment.gno @@ -0,0 +1,50 @@ +package plugincomment + +const Name = "post-comment" + +type ( + Plugin struct { + posts post.Store + } + + // Content is the comment's content. + Content struct { + Title string + Description string + Tags []string + } +) + +func New(st post.Store) Plugin { + return Plugin{ + posts: st, + } +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + // TODO: Implement render support for comments + return "" +} + +func (p Plugin) CreateComment(id string, c Content, level int) *post.Post { + pst := &post.Post{ + ID: id, + Level: level, + } + p.SetContent(pst, c) + return pst +} + +func (p Plugin) Content(pst *post.Post) (_ *Content, ok bool) { + c, ok := pst.PluginStore[Name].(*Content) + return c, ok +} + +func (p Plugin) SetContent(pst *post.Post, c Content) (updated bool) { + ps.PluginStore[Name] = c + return p.posts.Set(pst) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno new file mode 100644 index 00000000000..543f18b6e8d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/fork.gno @@ -0,0 +1,47 @@ +package pluginfork + +import ( + "gno.land/p/demo/boards/post" +) + +const Name = "fork" + +// TODO: Implement fork plugin to support thread forking +type Plugin struct { + AllowedPostLevels []int +} + +func New(o ...Option) Plugin { + var p Plugin + for _, apply := range o { + apply(&p) + } + return p +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + // TODO: Implement render support for text + return "" +} + +func (p Plugin) HasForkSupport(pst *post.Post) bool { + if len(p.AllowedPostLevels) == 0 { + return true + } + + for _, lvl := range p.AllowedPostLevels { + if pst.Level == lvl { + return true + } + } + return false +} + +func (p Plugin) Fork(pst *post.Post, newPath string) error { + // TODO: Implement fork support + return nil +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno new file mode 100644 index 00000000000..85fe888bcb1 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/fork/options.gno @@ -0,0 +1,9 @@ +package pluginfork + +type Option func(*Plugin) + +func AllowedPostLevels(levels []int) Option { + return func(p *Plugin) { + p.AllowedPostLevels = levels + } +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno new file mode 100644 index 00000000000..fe75dd364c5 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/lock/lock.gno @@ -0,0 +1,55 @@ +package pluginlock + +import "errors" + +const Name = "lock" + +var ErrInvalidPostType = errors.New("post type is not a board or thread") + +type ( + Plugin struct{} + Lock struct { + IsLocked bool + } +) + +func New() Plugin { + return Plugin{} +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + return "" +} + +func (p *Plugin) Lock(pst *post.Post) error { + if !isBoardOrThread(pst) { + return ErrInvalidPostType + } + + pst.PluginStore[Name].(*Lock).IsLocked = true +} + +func (p *Plugin) Unlock(pst *post.Post) error { + if !isBoardOrThread(pst) { + return ErrInvalidPostType + } + + pst.PluginStore[Name].(*Lock).IsLocked = false +} + +func (p Plugin) IsLocked(pst *post.Post) bool { + if !isBoardOrThread(pst) { + return ErrInvalidPostType + } + + // TODO: Check parents if current post is not locked + return pst.PluginStore[Name].(*Lock).IsLocked +} + +func isBoardOrThread(pst *post.Post) bool { + return pst.Level == post.LevelBoard || pst.Level == post.LevelPost +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno new file mode 100644 index 00000000000..5a42dc8bd10 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/plugin.gno @@ -0,0 +1,48 @@ +// TODO: Document how plugins work and best practices +package plugin + +import ( + "gno.land/p/demo/avl" +) + +type ( + // NOTE: Consider adding lifecycle methods like `Post` creation, deletion, ... + Plugin interface { + Name() string + Render() string + } + + Registry struct { + plugins avl.Tree // string(name) -> Plugin + } +) + +func NewRegistry(plugins ...Plugin) *Registry { + r := &Registry{} + for _, p := range plugins { + r.plugins.Set(p.Name(), p) + } + return r +} + +func (r Registry) Has(name string) bool { + return r.posts.Has(name) +} + +func (r Registry) Get(name string) (_ Plugin, found bool) { + if v, found := r.plugins.Get(name); found { + return v.(Plugin), true + } + return nil, false +} + +func (r *Registry) Add(p Plugin) { + r.plugins.Set(p.Name(), p) +} + +func (r *Registry) Remove(name string) (_ Plugin, removed bool) { + if v, removed := r.plugins.Remove(name, p); removed { + return v.(Plugin), false + } + return nil, false +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno new file mode 100644 index 00000000000..a20a4a22dd7 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/poll/poll.gno @@ -0,0 +1,47 @@ +package pluginpoll + +const Name = "post-poll" + +type ( + Plugin struct { + posts post.Store + } + + Poll struct { + Question string + Options []string + Votes []struct { + Address std.Adress + Option string + } + Tags []string + } +) + +func New(st post.Store) Plugin { + return Plugin{ + posts: st, + } +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + return "" +} + +func (p Plugin) CreatePoll(id string, v Poll) *post.Post { + pst := &post.Post{ + ID: id, + Level: LevelPost, + } + p.SetPoll(pst, v) + return pst +} + +func (p Plugin) SetPoll(pst *post.Post, v Poll) (updated bool) { + pst.PluginStore[Name] = v + return p.posts.Set(pst.ID, pst) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno new file mode 100644 index 00000000000..a87b45bf1e0 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/options.gno @@ -0,0 +1,15 @@ +package pluginreputation + +type Option func(*Plugin) + +func UsePolicy(v Policy) Option { + return func(p *Plugin) { + p.Policy = v + } +} + +func AllowedPostLevels(levels []int) Option { + return func(p *Plugin) { + p.AllowedPostLevels = levels + } +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno new file mode 100644 index 00000000000..0ec8e1406af --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/reputation.gno @@ -0,0 +1,99 @@ +package pluginreputation + +import ( + "errors" + "std" +) + +// NOTE: Think about implementing a reputation based policy +const ( + PolicyLinear Policy = iota + PolicyTokenBased +) + +const Name = "reputation" + +var ErrNotSupported = errors.New("reputation not supported") + +type ( + Policy int + + Plugin struct { + Store Store + Policy Policy + AllowedPostLevels []int + } + + Reputation struct { + Upvotes uint + Downvotes uint + } +) + +func New(o ...Option) Plugin { + var p Plugin + for _, apply := range o { + apply(&p) + } + return p +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + return "" +} + +func (p Plugin) HasReputationSupport(pst *post.Post) bool { + if len(p.AllowedPostLevels) == 0 { + return true + } + + for _, lvl := range p.AllowedPostLevels { + if pst.Level == lvl { + return true + } + } + return false +} + +func (p Plugin) Votes(pst *post.Post) (upvotes uint64, downvotes uint64) { + if !p.HasReputationSupport(pst) { + return ErrNotSupported + } + + r := pst.PluginStore[Name].(*Reputation) + return r.Upvotes, r.Downvotes +} + +func (p Plugin) Voters(pst *post.Post) []std.Address { + if !p.HasReputationSupport(pst) { + return ErrNotSupported + } + + // TODO: Implement support for tracking voters +} + +func (p *Plugin) Upvote(pst *post.Post) error { + if !p.HasReputationSupport(pst) { + return ErrNotSupported + } + + // TODO: Handle accounts and change downvotes for existing accounts that downvoted + r := pst.PluginStore[Name].(*Reputation) + r.Upvotes++ + p.store.inc(pst.ID) +} + +func (p *Plugin) Downvote(pst *post.Post) error { + if !p.HasReputationSupport(pst) { + return ErrNotSupported + } + + // TODO: Handle accounts and change upvotes for existing accounts that upvoted + r := pst.PluginStore[Name].(*Reputation) + r.Downvotes++ + p.store.dec(pst.ID) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno new file mode 100644 index 00000000000..9661210f830 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/reputation/store.gno @@ -0,0 +1,58 @@ +package pluginreputation + +import ( + "gno.land/p/demo/seqid" +) + +type ( + VotesIterFn = func(votes uint64, path string) bool + + Store struct { + votes avl.Tree // string(count) -> string(path) + } +) + +func (s Store) Iterate(fn VotesIterFn) bool { + // TODO: Support pagination of votes? + return s.votes.Iterate("", "", func(key string, v interface{}) bool { + count, _ := seqid.FromBinary(key) + return fn(uint64(count), v.(string)) + }) +} + +func (s *Store) inc(path string) uint64 { + var ( + current seqid.ID + v, found = s.votes.Get(path) + ) + if found { + current = v.(seqid.ID) + // TODO: Implement the right solution because this is not right, just showcase + s.votes.Remove(current.Binary()) + } + + current.Next() + s.votes.Set(current.Binary(), path) + return uint64(current) +} + +func (s *Store) dec(path string) uint64 { + var ( + current seqid.ID + v, found = s.votes.Get(path) + ) + if found { + current = v.(seqid.ID) + } + + if current == 0 { + return current + } + + s.votes.Remove(current.Binary()) + current-- + if current != 0 { + s.votes.Set(current.Binary(), current) + } + return uint64(current) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno new file mode 100644 index 00000000000..014956e367b --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/text/text.gno @@ -0,0 +1,38 @@ +// plugintext is a content type for representing a tweet, blog post or a thread like Reddit. +package plugintext + +import ( + "gno.land/p/demo/boards/post" // NOTE: Plugins should be at the same level of post package +) + +const Name = "post-text" + +type ( + Plugin struct{} + Content struct { + Title string + Body string + Tags []string + } +) + +func New() Plugin { + return Plugin{} +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + // TODO: Implement render support for text + return "" +} + +func (p Plugin) Content(pst *post.Post) Content { + return pst.Body[Name].(*Content) +} + +func (p Plugin) SetContent(pst *post.Post, c Content) { + pst.Body[Name] = c +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno new file mode 100644 index 00000000000..506f32001a8 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/plugin/title/title.gno @@ -0,0 +1,35 @@ +// plugintext is a content type for representing organizations, categories or sections. +package plugintitle + +const Name = "post-title" + +type ( + Plugin struct{} + + Content struct { + Title string + Description string + Tags []string + } +) + +func New() Plugin { + return Plugin{} +} + +func (p Plugin) Name() string { + return Name +} + +func (p Plugin) Render() string { + // TODO: Implement render support for title + return "" +} + +func (p Plugin) Content(pst *post.Post) Content { + return pst.Body[Name].(*Content) +} + +func (p Plugin) SetContent(pst *post.Post, c Content) { + pst.Body[Name] = c +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/post.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/post.gno new file mode 100644 index 00000000000..229a904fd66 --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/post.gno @@ -0,0 +1,25 @@ +package post + +import ( + "strconv" + "time" + + "gno.land/p/demo/boards/post/plugin" +) + +type Post struct { + ID string + PluginStore plugin.Plugin + Parent *Post + Level int + Base *Post + Children []*Post + Forks []*Post + UpdatedAt time.Time + CreatedAt time.Time + Creator std.Address +} + +func (p Post) NextIncrementalKey(baseKey string) string { + return baseKey + "/" + strconv.Itoa(len(p.Children)) +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/post/store.gno b/examples/gno.land/p/demo/boardsv2/draft3/post/store.gno new file mode 100644 index 00000000000..49513f250db --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/post/store.gno @@ -0,0 +1,31 @@ +package post + +func NewStore() Store { + return Store{} +} + +// TODO: Implement posts store +type Store struct { + posts avl.Tree // string(level + creation timestamp + slug) -> *Post + slugs avl.Tree // string(slug) -> *Post +} + +func (s Store) Get(path string) (_ *Post, found bool) { + if v, found := s.slugs.Get(path); found { + return v.(*Post), true + } + return nil, false +} + +func (s Store) GetByLevel(path string, level int) (_ *Post, found bool) { + v, found := s.slugs.Get(path) + if !found { + return nil, false + } + + p := v.(*Post) + if p.Level != level { + return nil, false + } + return p, true +} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno b/examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno new file mode 100644 index 00000000000..c4fc379c28d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/store/cursor.gno @@ -0,0 +1,9 @@ +package store + +// TODO: Define how cursors should be used alongside stores +type Cursor struct { + FromID string + Count int +} + +func NewCursor(fromID string, count int) Cursor {} diff --git a/examples/gno.land/p/demo/boardsv2/draft3/store/store.gno b/examples/gno.land/p/demo/boardsv2/draft3/store/store.gno new file mode 100644 index 00000000000..5c7fba3db4d --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/store/store.gno @@ -0,0 +1,6 @@ +package store + +// TODO: Define a storage interface and create and avl.Tree wrapper +type Store interface{} + +type AVLTreeStore struct{} // TODO: Use IAVL instead if there is a package implemented diff --git a/examples/gno.land/p/demo/boardsv2/draft3/thread.gno b/examples/gno.land/p/demo/boardsv2/draft3/thread.gno new file mode 100644 index 00000000000..cce35799b6f --- /dev/null +++ b/examples/gno.land/p/demo/boardsv2/draft3/thread.gno @@ -0,0 +1,64 @@ +package boards + +import ( + "gno.land/p/demo/boards/post" + pluginfork "gno.land/p/demo/boards/post/plugin/fork" + pluginpoll "gno.land/p/demo/boards/post/plugin/poll" + pluginreputation "gno.land/p/demo/boards/post/plugin/reputation" + plugintext "gno.land/p/demo/boards/post/plugin/text" +) + +type ( + ThreadContent plugintext.Content + + // TODO: Should polls be handler within this type? + Thread struct { + *post.Post + } +) + +func (t Thread) Info() ThreadContent { + return ThreadContent(t.getContent()) +} + +func (t Thread) Update(c ThreadContent) { + t.PluginStore[plugintext.Name] = plugintext.Content(c) +} + +func (t Thread) Upvote() error { + r := t.getReputation() + return r.Upvote(t.Post) +} + +func (t Thread) Downvote() error { + r := t.getReputation() + return r.Downvote(t.Post) +} + +func (t Thread) Fork(newPath string) error { + f := t.getFork() + return f.Fork(t.Post) +} + +func (t Thread) Render() string { + c := t.getContent() + return c.Render() +} + +// Comments returns a list of comments sent to the thread. +// The comment slice will be non-nil only when Thread is initiated +// through ThreadWithComments. +// TODO: Add support to get sub-threads (any type) and comments +// func (t Thread) Comments() []Comment {} + +func (t Thread) getContent() *plugintext.Content { + return t.PluginStore[plugintext.Name].(*plugintext.Content) +} + +func (t Thread) getReputation() *pluginreputation.Reputation { + return t.PluginStore[pluginreputation.Name].(*pluginreputation.Reputation) +} + +func (t Thread) getFork() *pluginfork.Fork { + return t.PluginStore[pluginfork.Name].(*pluginfork.Fork) +} diff --git a/examples/gno.land/p/demo/boardsv2/post/post.gno b/examples/gno.land/p/demo/boardsv2/post/post.gno deleted file mode 100644 index 54be1f50a86..00000000000 --- a/examples/gno.land/p/demo/boardsv2/post/post.gno +++ /dev/null @@ -1 +0,0 @@ -package post \ No newline at end of file diff --git a/examples/gno.land/p/demo/boardsv2/post/view.gno b/examples/gno.land/p/demo/boardsv2/post/view.gno deleted file mode 100644 index 54be1f50a86..00000000000 --- a/examples/gno.land/p/demo/boardsv2/post/view.gno +++ /dev/null @@ -1 +0,0 @@ -package post \ No newline at end of file diff --git a/examples/gno.land/r/demo/boardsv2/boardsv2.gno b/examples/gno.land/r/demo/boardsv2/boardsv2.gno index 4ad3d466272..4205c210da5 100644 --- a/examples/gno.land/r/demo/boardsv2/boardsv2.gno +++ b/examples/gno.land/r/demo/boardsv2/boardsv2.gno @@ -1 +1,44 @@ -package boardsv2 \ No newline at end of file +package boardsv2 + +import "gno.land/p/demo/avl" + +// TODO: This goes in the realm +// type Boards struct { +// // TODO: Define how do we want to display and sort boards and posts (upvotes, pinned, ...) +// boards avl.Tree +// Title string +// Description string +// } + +func Render(path string) string { + // TODO: Implement render + return "" +} + +// TODO: Define public API + +func CreateBoard() {} // Maybe +func EditBoard() {} // Maybe +func ForkBoard() {} // Maybe + +func CreatePost() {} +func EditPost() {} +func ForkPost() {} +func DeletePost() {} +func Repost() {} +func Pin() {} +func Invite() {} // Maybe: Could also rely on an allow list +func UpVote() {} +func DownVote() {} + +func Comment() {} // Maybe +func EditComment() {} // Maybe +func DeleteComment() {} // Maybe + +func ToggleCommentsSupport() {} // Maybe +func ToggleThreadsSupport() {} // Maybe +func GetTags() {} // Maybe: List of allowed tags (moderated) + +func AddModerator() {} // Maybe +func RemoveModerator() {} // Maybe +func GetModerators() {} // Maybe diff --git a/examples/gno.land/r/demo/boardsv2/draft2/main.gno b/examples/gno.land/r/demo/boardsv2/draft2/main.gno new file mode 100644 index 00000000000..6f8d203d31a --- /dev/null +++ b/examples/gno.land/r/demo/boardsv2/draft2/main.gno @@ -0,0 +1,15 @@ +package boards + +var postStore = avl.Tree{} // string(level + timestamp + slug) -> *Post + +func newApp() boards.App { // stateless approach for App struct + return boards.New( + postStore, + boards.MaxCommentDepth(10), + boards.LinearReputationPolicy(), + ) +} + +func Boards(c post.Cursor) ([]boards.Board, error) { + return newApp().Boards(c) +} diff --git a/examples/gno.land/r/demo/boardsv2/draft3/boards.gno b/examples/gno.land/r/demo/boardsv2/draft3/boards.gno new file mode 100644 index 00000000000..efc316fa458 --- /dev/null +++ b/examples/gno.land/r/demo/boardsv2/draft3/boards.gno @@ -0,0 +1,68 @@ +package boards + +import ( + "std" + + "gno.land/p/demo/boards" + "gno.land/p/demo/boards/post" +) + +var app = boards.New( + post.NewStore(), + boards.MaxCommentDepth(10), + boards.LinearReputationPolicy(), +) + +func Render(path string) string { + // TODO: Define how to render the tree of boards, posts and comments + return "" +} + +func CreateBoard(slug, title, description string, tags []string) (path string) { + creator := std.GetOrigCaller() + board := app.CreateBoard(slug, title, description, tags, creator) + return board.ID +} + +func Lock(path string) { + post := getBoardOrThread(path) + if post == nil { + panic("path doesn't exist or locking this path not supported") + } + + assertOrigCallerIsCreator(post) + + // NOTE: Explore if it's better to use Post or Board/Thread types + if err := app.Lock(post); err != nil { + panic(err) + } +} + +func Fork(path, newPath string) { + post := getBoardOrThread(path) + if post == nil { + panic("path doesn't exist or forking this path not supported") + } + + // TODO: Use this way + app.ForkBoard(board) + app.ForkThread(thread) + + if err := app.Fork(post, newPath); err != nil { + panic(err) + } +} + +func getBoardOrThread(path string) *post.Post { + p, found := app.GetPost(path) + if found && (p.Level == boards.LevelBoard || p.Level == boards.LevelThread) { + return p + } + return nil +} + +func assertOrigCallerIsCreator(p *post.Post) { + if post.Creator != std.GetOrigCaller() { + panic("original caller is not allowed to perform this action") + } +}