From 6f4efc60dc7ca8d3d4a81b2883ace8d886240b05 Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sat, 24 Feb 2024 18:24:21 +0100 Subject: [PATCH] feat(blog): add sorting, better rendering (#1541) ## Description This PR adds two new AVL trees that enable sorting of the blog posts by publication date and title. This approach utilizes the fact that the AVL tree iteration order is lexicographic by key. We introduce two new AVL trees into the `Blog` struct - which store a key to sort by (title or publication date), and store the pointers to posts, meaning the memory overhead is small. This PR also modifies the rendering of the blog: Old blog home page: ![Screenshot 2024-01-23 at 11 34 10](https://github.com/gnolang/gno/assets/33522493/78e197d3-a477-49a9-88d8-06b4f2c3d51d) New blog home page (sorted newest-top-left): ![Screenshot 2024-01-23 at 11 33 38](https://github.com/gnolang/gno/assets/33522493/16712e37-c971-4cf5-9962-9dbf639a6088) Old post rendering - header & footer: ![Screenshot 2024-01-23 at 11 34 46](https://github.com/gnolang/gno/assets/33522493/d6a713bb-379c-44b7-9ea7-38997e6b5c5a) New post rendering - header & footer: ![Screenshot 2024-01-23 at 11 35 03](https://github.com/gnolang/gno/assets/33522493/352ea40d-c9ac-41a0-a362-fb3c5ca62968) ---- ![Screenshot 2024-01-23 at 11 35 11](https://github.com/gnolang/gno/assets/33522493/6f21416c-8945-4eca-9bee-b67a9e56a568) The API changes are the following: - Each post now stores the publisher (ie caller of ModAddPost), and the authors. - It is envisioned that the author list will be a list of GitHub usernames, while the publisher is simply the address of the caller. A PR in the Blog repo will be added to accommodate this change. - If the author list in the frontmatter of the blog post is empty, just the publisher will be displayed - The timestamp for when the blog was written will primarily be taken in from the outside world, ie from the frontmatter, in the `RFC3339` format. This timestamp allows us to still keep the ordering of the blogs even when the block height/timestamp is unreliable. In case the timestamps are not provided, time.Now() in the realm will be used instead. Publishing flow - two options: - Pass in data directly to the blog realm via `ModAddPost`, - Use the `gnoblog-cli` or similar parsing & publishing tool. Either way, it is expected that any timestamps passed will be used in the `time.RFC3339` format. This PR is being published in pair with a [refactor](https://github.com/gnolang/blog/pull/63) of the gnoblog-cli tool. Related: https://github.com/gnolang/blog/issues/20
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/blog/blog.gno | 162 +++++++++++++----- examples/gno.land/p/demo/blog/errors.gno | 10 +- examples/gno.land/r/gnoland/blog/admin.gno | 22 ++- examples/gno.land/r/gnoland/blog/gnoblog.gno | 7 + .../gno.land/r/gnoland/blog/gnoblog_test.gno | 107 ++++++++---- examples/gno.land/r/gnoland/pages/admin.gno | 12 +- .../gno.land/r/gnoland/pages/page_about.gno | 2 +- .../r/gnoland/pages/page_ecosystem.gno | 2 +- .../gno.land/r/gnoland/pages/page_events.gno | 2 +- .../gno.land/r/gnoland/pages/page_gnolang.gno | 2 +- .../gno.land/r/gnoland/pages/page_gor.gno | 2 +- .../r/gnoland/pages/page_partners.gno | 2 +- .../gno.land/r/gnoland/pages/page_start.gno | 2 +- .../r/gnoland/pages/page_testnets.gno | 2 +- .../r/gnoland/pages/page_tokenomics.gno | 2 +- examples/gno.land/r/manfred/present/admin.gno | 12 +- .../r/manfred/present/present_miami23.gno | 4 +- .../present/present_miami23_filetest.gno | 52 +----- gno.land/genesis/genesis_txs.txt | 4 +- 19 files changed, 253 insertions(+), 157 deletions(-) diff --git a/examples/gno.land/p/demo/blog/blog.gno b/examples/gno.land/p/demo/blog/blog.gno index 1cf37b7ad3a..6be11336b0c 100644 --- a/examples/gno.land/p/demo/blog/blog.gno +++ b/examples/gno.land/p/demo/blog/blog.gno @@ -1,7 +1,6 @@ package blog import ( - "errors" "std" "strconv" "strings" @@ -13,10 +12,12 @@ import ( ) type Blog struct { - Title string - Prefix string // i.e. r/gnoland/blog: - Posts avl.Tree // slug -> Post - NoBreadcrumb bool + Title string + Prefix string // i.e. r/gnoland/blog: + Posts avl.Tree // slug -> *Post + PostsPublished avl.Tree // published-date -> *Post + PostsAlphabetical avl.Tree // title -> *Post + NoBreadcrumb bool } func (b Blog) RenderLastPostsWidget(limit int) string { @@ -42,7 +43,7 @@ func (b Blog) RenderHome(res *mux.ResponseWriter, req *mux.Request) { } res.Write("
") - b.Posts.Iterate("", "", func(key string, value interface{}) bool { + b.PostsPublished.ReverseIterate("", "", func(key string, value interface{}) bool { post := value.(*Post) res.Write(post.RenderListItem()) return false @@ -62,19 +63,16 @@ func (b Blog) RenderPost(res *mux.ResponseWriter, req *mux.Request) { } p := post.(*Post) - if !b.NoBreadcrumb { - breadStr := breadcrumb([]string{ - ufmt.Sprintf("[%s](%s)", b.Title, b.Prefix), - "p", - p.Title, - }) - res.Write(breadStr) - } - - // output += ufmt.Sprintf("## [%s](%s)\n", p.Title, p.URL()) + res.Write("# " + p.Title + "\n\n") res.Write(p.Body + "\n\n") + res.Write("---\n\n") + res.Write(p.RenderTagList() + "\n\n") - res.Write(formatAuthorAndDate(p.Author, p.CreatedAt) + "\n\n") + res.Write(p.RenderAuthorList() + "\n\n") + res.Write(p.RenderPublishData() + "\n\n") + + res.Write("---\n") + res.Write("
Comment section\n\n") // comments p.Comments.ReverseIterate("", "", func(key string, value interface{}) bool { @@ -82,6 +80,8 @@ func (b Blog) RenderPost(res *mux.ResponseWriter, req *mux.Request) { res.Write(comment.RenderListItem()) return false }) + + res.Write("
\n") } func (b Blog) RenderTag(res *mux.ResponseWriter, req *mux.Request) { @@ -124,21 +124,34 @@ func (b Blog) Render(path string) string { return router.Render(path) } -func (b *Blog) NewPost(author std.Address, slug, title, body string, tags []string) error { - _, found := b.Posts.Get(slug) - if found { - return errors.New("slug already exists.") +func (b *Blog) NewPost(publisher std.Address, slug, title, body, pubDate string, authors, tags []string) error { + if _, found := b.Posts.Get(slug); found { + return ErrPostSlugExists } - post := Post{ - Author: author, + var parsedTime time.Time + var err error + if pubDate != "" { + parsedTime, err = time.Parse(time.RFC3339, pubDate) + if err != nil { + return err + } + } else { + // If no publication date was passed in by caller, take current block time + parsedTime = time.Now() + } + + post := &Post{ + Publisher: publisher, + Authors: authors, Slug: slug, Title: title, Body: body, Tags: tags, - CreatedAt: time.Now(), + CreatedAt: parsedTime, } - return b.prepareAndSetPost(&post) + + return b.prepareAndSetPost(post) } func (b *Blog) prepareAndSetPost(post *Post) error { @@ -146,20 +159,35 @@ func (b *Blog) prepareAndSetPost(post *Post) error { post.Body = strings.TrimSpace(post.Body) if post.Title == "" { - return errors.New("title is missing.") + return ErrPostTitleMissing } if post.Body == "" { - return errors.New("body is missing.") + return ErrPostBodyMissing } if post.Slug == "" { - return errors.New("slug is missing.") + return ErrPostSlugMissing } - // more input sanitization? post.Blog = b post.UpdatedAt = time.Now() + trimmedTitleKey := strings.Replace(post.Title, " ", "", -1) + pubDateKey := post.CreatedAt.Format(time.RFC3339) + + // Cannot have two posts with same title key + if _, found := b.PostsAlphabetical.Get(trimmedTitleKey); found { + return ErrPostTitleExists + } + // Cannot have two posts with *exact* same timestamp + if _, found := b.PostsPublished.Get(pubDateKey); found { + return ErrPostPubDateExists + } + + // Store post under keys + b.PostsAlphabetical.Set(trimmedTitleKey, post) + b.PostsPublished.Set(pubDateKey, post) b.Posts.Set(post.Slug, post) + return nil } @@ -179,15 +207,24 @@ type Post struct { CreatedAt time.Time UpdatedAt time.Time Comments avl.Tree - Author std.Address + Authors []string + Publisher std.Address Tags []string CommentIndex int } -func (p *Post) Update(title, body string, tags []string) error { +func (p *Post) Update(title, body, publicationDate string, authors, tags []string) error { p.Title = title p.Body = body p.Tags = tags + p.Authors = authors + + parsedTime, err := time.Parse(time.RFC3339, publicationDate) + if err != nil { + return err + } + + p.CreatedAt = parsedTime return p.Blog.prepareAndSetPost(p) } @@ -234,31 +271,66 @@ func (p *Post) RenderListItem() string { return "error: no such post\n" } output := "
\n\n" - output += ufmt.Sprintf("## [%s](%s)\n", p.Title, p.URL()) - output += ufmt.Sprintf("**[Learn More](%s)**\n", p.URL()) + output += ufmt.Sprintf("### [%s](%s)\n", p.Title, p.URL()) + // output += ufmt.Sprintf("**[Learn More](%s)**\n\n", p.URL()) + + output += " " + p.CreatedAt.Format("02 Jan 2006") // output += p.Summary() + "\n\n" // output += p.RenderTagList() + "\n\n" - // output += formatAuthorAndDate(p.Author, p.CreatedAt) + "\n" output += "\n" output += "
" return output } +// Render post tags func (p *Post) RenderTagList() string { if p == nil { return "error: no such post\n" } - output := "" + if len(p.Tags) == 0 { + return "" + } + + output := "Tags: " for idx, tag := range p.Tags { if idx > 0 { output += " " } tagURL := p.Blog.Prefix + "t/" + tag output += ufmt.Sprintf("[#%s](%s)", tag, tagURL) + } return output } +// Render authors if there are any +func (p *Post) RenderAuthorList() string { + out := "Written" + if len(p.Authors) != 0 { + out += " by " + + for idx, author := range p.Authors { + out += author + if idx < len(p.Authors)-1 { + out += ", " + } + } + } + out += " on " + p.CreatedAt.Format("02 Jan 2006") + + return out +} + +func (p *Post) RenderPublishData() string { + out := "Published " + if p.Publisher != "" { + out += "by " + p.Publisher.String() + " " + } + out += "to " + p.Blog.Title + + return out +} + func (p *Post) URL() string { if p == nil { return p.Blog.Prefix + "404" @@ -287,15 +359,15 @@ type Comment struct { } func (c Comment) RenderListItem() string { - output := "" - output += ufmt.Sprintf("#### %s\n", formatAuthorAndDate(c.Author, c.CreatedAt)) - output += c.Comment + "\n" - output += "\n" - return output -} + output := "
" + output += c.Comment + "\n\n" + output += "
" + + output += "
" + output += ufmt.Sprintf("by %s on %s", c.Author, c.CreatedAt.Format(time.RFC822)) + output += "
\n\n" -func formatAuthorAndDate(author std.Address, createdAt time.Time) string { - authorString := author.String() // FIXME: username. - createdAtString := createdAt.Format("2006-01-02 3:04pm MST") - return ufmt.Sprintf("by %s on %s", authorString, createdAtString) + output += "---\n\n" + + return output } diff --git a/examples/gno.land/p/demo/blog/errors.gno b/examples/gno.land/p/demo/blog/errors.gno index db9f8f39fa1..9d885d7222f 100644 --- a/examples/gno.land/p/demo/blog/errors.gno +++ b/examples/gno.land/p/demo/blog/errors.gno @@ -2,4 +2,12 @@ package blog import "errors" -var ErrNoSuchPost = errors.New("no such post") +var ( + ErrPostTitleMissing = errors.New("post title is missing") + ErrPostSlugMissing = errors.New("post slug is missing") + ErrPostBodyMissing = errors.New("post body is missing") + ErrPostSlugExists = errors.New("post with specified slug already exists") + ErrPostPubDateExists = errors.New("post with specified publication date exists") + ErrPostTitleExists = errors.New("post with specified title already exists") + ErrNoSuchPost = errors.New("no such post") +) diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 646fe5f155e..f615e26e491 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -39,20 +39,32 @@ func AdminRemoveModerator(addr std.Address) { moderatorList.Set(addr.String(), false) // FIXME: delete instead? } -func ModAddPost(slug, title, body, tags string) { +func ModAddPost(slug, title, body, publicationDate, authors, tags string) { assertIsModerator() caller := std.GetOrigCaller() - tagList := strings.Split(tags, ",") - err := b.NewPost(caller, slug, title, body, tagList) + + var tagList []string + if tags != "" { + tagList = strings.Split(tags, ",") + } + var authorList []string + if authors != "" { + authorList = strings.Split(authors, ",") + } + + err := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList) + checkErr(err) } -func ModEditPost(slug, title, body, tags string) { +func ModEditPost(slug, title, body, publicationDate, authors, tags string) { assertIsModerator() tagList := strings.Split(tags, ",") - err := b.GetPost(slug).Update(title, body, tagList) + authorList := strings.Split(authors, ",") + + err := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList) checkErr(err) } diff --git a/examples/gno.land/r/gnoland/blog/gnoblog.gno b/examples/gno.land/r/gnoland/blog/gnoblog.gno index cad84507614..1cdc95fe9a8 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog.gno @@ -27,3 +27,10 @@ func Render(path string) string { func RenderLastPostsWidget(limit int) string { return b.RenderLastPostsWidget(limit) } + +func PostExists(slug string) bool { + if b.GetPost(slug) == nil { + return false + } + return true +} diff --git a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno index e98b34fdc2b..1be61138b39 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno @@ -24,24 +24,22 @@ No posts. // create two posts, list post. { - ModAddPost("slug1", "title1", "body1", "tag1,tag2") - ModAddPost("slug2", "title2", "body2", "tag1,tag3") + ModAddPost("slug1", "title1", "body1", "2022-05-20T13:17:22Z", "moul", "tag1,tag2") + ModAddPost("slug2", "title2", "body2", "2022-05-20T13:17:23Z", "moul", "tag1,tag3") got := Render("") expected := ` -# Gnoland's Blog + # Gnoland's Blog
-## [title1](/r/gnoland/blog:p/slug1) -**[Learn More](/r/gnoland/blog:p/slug1)** - +### [title2](/r/gnoland/blog:p/slug2) + 20 May 2022
-## [title2](/r/gnoland/blog:p/slug2) -**[Learn More](/r/gnoland/blog:p/slug2)** - +### [title1](/r/gnoland/blog:p/slug1) + 20 May 2022
-` + ` assertMDEquals(t, got, expected) } @@ -49,14 +47,24 @@ No posts. { got := Render("p/slug2") expected := ` -# [Gnoland's Blog](/r/gnoland/blog:) / p / title2 +# title2 body2 -[#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) +--- -by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC -` +Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) + +Written by moul on 20 May 2022 + +Published by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog + +--- +
Comment section + +
+ + ` assertMDEquals(t, got, expected) } @@ -72,11 +80,10 @@ by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC
-## [title1](/r/gnoland/blog:p/slug1) -**[Learn More](/r/gnoland/blog:p/slug1)** - +### [title1](/r/gnoland/blog:p/slug1) + 20 May 2022
-` + ` assertMDEquals(t, got, expected) } @@ -89,42 +96,72 @@ by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC AddComment("slug1", "comment5") got := Render("p/slug2") expected := ` -# [Gnoland's Blog](/r/gnoland/blog:) / p / title2 +# title2 body2 -[#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) +--- -by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC +Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) -#### by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC -comment4 +Written by moul on 20 May 2022 -#### by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC -comment2 -` +Published by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog + +--- +
Comment section + +
comment4 + +
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+ +--- + +
comment2 + +
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+ +--- + +
+ ` assertMDEquals(t, got, expected) } // edit post. { - ModEditPost("slug2", "title2++", "body2++", "tag1,tag4") + ModEditPost("slug2", "title2++", "body2++", "2009-11-10T23:00:00Z", "manfred", "tag1,tag4") got := Render("p/slug2") expected := ` -# [Gnoland's Blog](/r/gnoland/blog:) / p / title2++ +# title2++ body2++ -[#tag1](/r/gnoland/blog:t/tag1) [#tag4](/r/gnoland/blog:t/tag4) +--- -by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC +Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag4](/r/gnoland/blog:t/tag4) -#### by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC -comment4 +Written by manfred on 10 Nov 2009 -#### by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC -comment2 -` +Published by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog + +--- +
Comment section + +
comment4 + +
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+ +--- + +
comment2 + +
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+ +--- + +
+ ` assertMDEquals(t, got, expected) } diff --git a/examples/gno.land/r/gnoland/pages/admin.gno b/examples/gno.land/r/gnoland/pages/admin.gno index 39fba6d3274..ab447e8f604 100644 --- a/examples/gno.land/r/gnoland/pages/admin.gno +++ b/examples/gno.land/r/gnoland/pages/admin.gno @@ -38,20 +38,24 @@ func AdminRemoveModerator(addr std.Address) { moderatorList.Set(addr.String(), false) // XXX: delete instead? } -func ModAddPost(slug, title, body, tags string) { +func ModAddPost(slug, title, body, publicationDate, authors, tags string) { assertIsModerator() caller := std.GetOrigCaller() tagList := strings.Split(tags, ",") - err := b.NewPost(caller, slug, title, body, tagList) + authorList := strings.Split(authors, ",") + + err := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList) checkErr(err) } -func ModEditPost(slug, title, body, tags string) { +func ModEditPost(slug, title, body, publicationDate, authors, tags string) { assertIsModerator() tagList := strings.Split(tags, ",") - err := b.GetPost(slug).Update(title, body, tagList) + authorList := strings.Split(authors, ",") + + err := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList) checkErr(err) } diff --git a/examples/gno.land/r/gnoland/pages/page_about.gno b/examples/gno.land/r/gnoland/pages/page_about.gno index 80c43c1741d..c296b9427b0 100644 --- a/examples/gno.land/r/gnoland/pages/page_about.gno +++ b/examples/gno.land/r/gnoland/pages/page_about.gno @@ -16,5 +16,5 @@ This consensus mechanism also achieves higher security with fewer validators, op Any blockchain using Gno achieves succinctness, composability, expressivity, and completeness not found in any other smart contract platform. By observing a minimal structure, the design can endure over time and challenge the regime of information censorship we’re living in today.` - _ = b.NewPost("", path, title, body, nil) + _ = b.NewPost("", path, title, body, "2022-05-20T13:17:22Z", nil, nil) } diff --git a/examples/gno.land/r/gnoland/pages/page_ecosystem.gno b/examples/gno.land/r/gnoland/pages/page_ecosystem.gno index 68969c44529..c5e0134565f 100644 --- a/examples/gno.land/r/gnoland/pages/page_ecosystem.gno +++ b/examples/gno.land/r/gnoland/pages/page_ecosystem.gno @@ -31,5 +31,5 @@ Gnoswap is currently under development and led by the Onbloc team. Gnoswap will Through the Gno.land Developer Portal, new developers can explore the exciting world of Gnolang (Gno), a novel programming language that powers the Gno.land blockchain. If you want to interact with Gno.land, start writing a realm, build a dApp, or even port a Solidity contract to a Gnolang realm, you’ll find the resources to [get started here](https://docs.onbloc.xyz/).` ) - _ = b.NewPost("", path, title, body, nil) + _ = b.NewPost("", path, title, body, "2022-05-20T13:17:23Z", nil, nil) } diff --git a/examples/gno.land/r/gnoland/pages/page_events.gno b/examples/gno.land/r/gnoland/pages/page_events.gno index 18e7faeb3d3..eca3cda88ca 100644 --- a/examples/gno.land/r/gnoland/pages/page_events.gno +++ b/examples/gno.land/r/gnoland/pages/page_events.gno @@ -147,5 +147,5 @@ If you’re interested in building web3 with us, catch up with Gno.land in perso
` ) - _ = b.NewPost("", path, title, body, nil) + _ = b.NewPost("", path, title, body, "2022-05-20T13:17:24Z", nil, nil) } diff --git a/examples/gno.land/r/gnoland/pages/page_gnolang.gno b/examples/gno.land/r/gnoland/pages/page_gnolang.gno index ecbadab9f01..9e76263322d 100644 --- a/examples/gno.land/r/gnoland/pages/page_gnolang.gno +++ b/examples/gno.land/r/gnoland/pages/page_gnolang.gno @@ -39,5 +39,5 @@ Using Gno, developers can rapidly accelerate application development and adopt a The Go language is so well designed that the Gno smart contract system will become the new gold standard for smart contract development and other blockchain applications. As a programming language that is universally adopted, secure, composable, and complete, Gno is essential for the broader adoption of web3 and its sustainable growth.` ) - _ = b.NewPost("", path, title, body, nil) + _ = b.NewPost("", path, title, body, "2022-05-20T13:17:25Z", nil, nil) } diff --git a/examples/gno.land/r/gnoland/pages/page_gor.gno b/examples/gno.land/r/gnoland/pages/page_gor.gno index 3a6bb022e09..98047b34848 100644 --- a/examples/gno.land/r/gnoland/pages/page_gor.gno +++ b/examples/gno.land/r/gnoland/pages/page_gor.gno @@ -217,5 +217,5 @@ Game of Realms participants and core contributors have made significant progress ` - _ = b.NewPost("", path, title, body, nil) + _ = b.NewPost("", path, title, body, "2022-05-20T13:17:26Z", nil, nil) } diff --git a/examples/gno.land/r/gnoland/pages/page_partners.gno b/examples/gno.land/r/gnoland/pages/page_partners.gno index 440302437fa..36674d84a78 100644 --- a/examples/gno.land/r/gnoland/pages/page_partners.gno +++ b/examples/gno.land/r/gnoland/pages/page_partners.gno @@ -17,5 +17,5 @@ Are you a builder, tinkerer, or researcher? If you’re looking to create awesom ` - _ = b.NewPost("", path, title, body, nil) + _ = b.NewPost("", path, title, body, "2022-05-20T13:17:27Z", nil, nil) } diff --git a/examples/gno.land/r/gnoland/pages/page_start.gno b/examples/gno.land/r/gnoland/pages/page_start.gno index a36ec6e52b1..e08244ae57b 100644 --- a/examples/gno.land/r/gnoland/pages/page_start.gno +++ b/examples/gno.land/r/gnoland/pages/page_start.gno @@ -17,5 +17,5 @@ func init() { - [Install Gno Key](/r/demo/boards:testboard/5) - TODO: add more links ` - _ = b.NewPost("", path, title, body, nil) + _ = b.NewPost("", path, title, body, "2022-05-20T13:17:28Z", nil, nil) } diff --git a/examples/gno.land/r/gnoland/pages/page_testnets.gno b/examples/gno.land/r/gnoland/pages/page_testnets.gno index b6c09ab71ee..031afa4b044 100644 --- a/examples/gno.land/r/gnoland/pages/page_testnets.gno +++ b/examples/gno.land/r/gnoland/pages/page_testnets.gno @@ -15,5 +15,5 @@ func init() { See CONTRIBUTING.md on GitHub. ` - _ = b.NewPost("", path, title, body, nil) + _ = b.NewPost("", path, title, body, "2022-05-20T13:17:29Z", nil, nil) } diff --git a/examples/gno.land/r/gnoland/pages/page_tokenomics.gno b/examples/gno.land/r/gnoland/pages/page_tokenomics.gno index de899ae0a70..f51364c36e6 100644 --- a/examples/gno.land/r/gnoland/pages/page_tokenomics.gno +++ b/examples/gno.land/r/gnoland/pages/page_tokenomics.gno @@ -7,5 +7,5 @@ func init() { // XXX: description = """ body = `Lorem Ipsum` ) - _ = b.NewPost("", path, title, body, nil) + _ = b.NewPost("", path, title, body, "2022-05-20T13:17:30Z", nil, nil) } diff --git a/examples/gno.land/r/manfred/present/admin.gno b/examples/gno.land/r/manfred/present/admin.gno index ff0cb075656..60af578b54f 100644 --- a/examples/gno.land/r/manfred/present/admin.gno +++ b/examples/gno.land/r/manfred/present/admin.gno @@ -38,20 +38,24 @@ func AdminRemoveModerator(addr std.Address) { moderatorList.Set(addr.String(), false) // XXX: delete instead? } -func ModAddPost(slug, title, body, tags string) { +func ModAddPost(slug, title, body, publicationDate, authors, tags string) { assertIsModerator() caller := std.GetOrigCaller() tagList := strings.Split(tags, ",") - err := b.NewPost(caller, slug, title, body, tagList) + authorList := strings.Split(authors, ",") + + err := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList) checkErr(err) } -func ModEditPost(slug, title, body, tags string) { +func ModEditPost(slug, title, body, publicationDate, authors, tags string) { assertIsModerator() tagList := strings.Split(tags, ",") - err := b.GetPost(slug).Update(title, body, tagList) + authorList := strings.Split(authors, ",") + + err := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList) checkErr(err) } diff --git a/examples/gno.land/r/manfred/present/present_miami23.gno b/examples/gno.land/r/manfred/present/present_miami23.gno index 36b1980bb0b..ca2160de3a9 100644 --- a/examples/gno.land/r/manfred/present/present_miami23.gno +++ b/examples/gno.land/r/manfred/present/present_miami23.gno @@ -4,8 +4,6 @@ func init() { path := "miami23" title := "Portal Loop Demo (Miami 2023)" body := ` -# Portal Loop Demo (Miami 2023) - Rendered by Gno. [Source (WIP)](https://github.com/gnolang/gno/pull/1176) @@ -40,5 +38,5 @@ Rendered by Gno. - Engage in workshops. - Connect and have fun with colleagues. ` - _ = b.NewPost(adminAddr, path, title, body, []string{"demo", "portal-loop", "miami"}) + _ = b.NewPost(adminAddr, path, title, body, "2023-10-15T13:17:24Z", []string{"moul"}, []string{"demo", "portal-loop", "miami"}) } diff --git a/examples/gno.land/r/manfred/present/present_miami23_filetest.gno b/examples/gno.land/r/manfred/present/present_miami23_filetest.gno index 05c41905060..ac19d83ade4 100644 --- a/examples/gno.land/r/manfred/present/present_miami23_filetest.gno +++ b/examples/gno.land/r/manfred/present/present_miami23_filetest.gno @@ -1,57 +1,11 @@ package main -import "gno.land/r/manfred/present" +import ( + "gno.land/r/manfred/present" +) func main() { println(present.Render("")) println("------------------------------------") println(present.Render("p/miami23")) } - -// Output: -//
-// -// ## [Portal Loop Demo (Miami 2023)](/r/manfred/present:p/miami23) -// **[Learn More](/r/manfred/present:p/miami23)** -// -//
-// ------------------------------------ -// # Portal Loop Demo (Miami 2023) -// -// Rendered by Gno. -// -// [Source (WIP)](https://github.com/gnolang/gno/pull/1176) -// -// ## Portal Loop -// -// - DONE: Dynamic homepage, key pages, aliases, and redirects. -// - TODO: Deploy with history, complete worxdao v0. -// - Will replace the static gno.land site. -// - Enhances local development. -// -// [GitHub Issue](https://github.com/gnolang/gno/issues/1108) -// -// ## Roadmap -// -// - Crafting the roadmap this week, open to collaboration. -// - Combining onchain (portal loop) and offchain (GitHub). -// - Next week: Unveiling the official v0 roadmap. -// -// ## Teams, DAOs, Projects -// -// - Developing worxDAO contracts for directories of projects and teams. -// - GitHub teams and projects align with this structure. -// - CODEOWNER file updates coming. -// - Initial teams announced next week. -// -// ## Tech Team Retreat Plan -// -// - Continue Portal Loop. -// - Consider dApp development. -// - Explore new topics [here](https://github.com/orgs/gnolang/projects/15/). -// - Engage in workshops. -// - Connect and have fun with colleagues. -// -// [#demo](/r/manfred/present:t/demo) [#portal-loop](/r/manfred/present:t/portal-loop) [#miami](/r/manfred/present:t/miami) -// -// by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 1970-01-01 12:00am UTC diff --git a/gno.land/genesis/genesis_txs.txt b/gno.land/genesis/genesis_txs.txt index 33257380260..3baf9d3231b 100644 --- a/gno.land/genesis/genesis_txs.txt +++ b/gno.land/genesis/genesis_txs.txt @@ -13,5 +13,5 @@ {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Simple echo example with coins","This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.gno](/r/banktest/banktest.gno) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n\t\"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e\nSelf explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime std.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tstd.FormatTimestamp(act.time, \"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract.\nNotice that the \"latest\" variable is defined \"globally\" within\nthe context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package\nare encapsulated within this \"data realm\", where the data is \nmutated based on transactions that can potentially cross many\nrealm and non-realm packge boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named\n\"Deposit\". `std.AssertOriginCall() asserts that this function was called by a\ngno transactional Message. The caller is the user who signed off on this\ntransactional message. Send is the amount of deposit sent along with this\nmessage.\n\n```go\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: std.GetTimestamp(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n\t// return if any.\n\tif returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n\t\tpkgaddr := std.GetOrigPkgAddr()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/banktest:](/r/banktest:).\n\n```go\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/banktest](/r/banktest) and the [quickstart guide](/r/boards:testboard/4).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"iZX/llZlNTdZMLv1goCTgK2bWqzT8enlTq56wMTCpVxJGA0BTvuEM5Nnt9vrnlG6Taqj2GuTrmEnJBkDFTmt9g=="}],"memo":""} {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","TASK: Describe in your words","Describe in an essay (250+ words), on your favorite medium, why you are interested in gno.land and gnolang.\n\nReply here with a URL link to your written piece as a comment, for rewards.\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"4HBNtrta8HdeHj4JTN56PBTRK8GOe31NMRRXDiyYtjozuyRdWfOGEsGjGgHWcoBUJq6DepBgD4FetdqfhZ6TNQ=="}],"memo":""} {"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards\nGetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards\nBOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards\ngnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum", "tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum", "tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum","2022-05-20T13:17:22Z","","tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum","2022-05-20T13:17:23Z","","tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} \ No newline at end of file