Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

microblog.gno

4.35 Kb · 173 lines
  1// Package microblog is a public microblog wall for gno.land.
  2//
  3// Anyone can Post a short message (<= maxMsgLen chars). Every post records
  4// the caller address, the message, and the block height at which it was made.
  5// Render displays the wall newest-first, 20 posts per page, with pagination
  6// driven by the Render path (e.g. "?page=2" or "/2").
  7package microblog
  8
  9import (
 10	"chain"
 11	"chain/runtime"
 12	"chain/runtime/unsafe"
 13	"strconv"
 14	"strings"
 15
 16	"gno.land/p/nym-moultest123/kit/ui/v0"
 17)
 18
 19// maxMsgLen is the maximum length (in bytes) of a single post.
 20const maxMsgLen = 280
 21
 22// pageSize is how many posts a single Render page shows.
 23const pageSize = 20
 24
 25// post is a single microblog entry.
 26type post struct {
 27	Author string // bech32 address of the poster
 28	Msg    string // the message body
 29	Height int64  // block height at which it was posted
 30}
 31
 32// posts is the append-only wall, in chronological (oldest-first) order.
 33// Render reverses this view to show newest-first.
 34var posts []post
 35
 36// Post publishes msg to the wall. It is a crossing function: callers invoke it
 37// with Post(cross(cur), "hello"). It panics if msg is empty or longer than
 38// maxMsgLen bytes.
 39func Post(cur realm, msg string) {
 40	if !cur.IsCurrent() {
 41		panic("spoofed realm: cur is not the live crossing frame")
 42	}
 43	if len(msg) == 0 {
 44		panic("microblog: empty message")
 45	}
 46	if len(msg) > maxMsgLen {
 47		panic("microblog: message too long (max " + strconv.Itoa(maxMsgLen) + " bytes)")
 48	}
 49
 50	author := unsafe.PreviousRealm().Address()
 51
 52	posts = append(posts, post{
 53		Author: author.String(),
 54		Msg:    msg,
 55		Height: runtime.ChainHeight(),
 56	})
 57
 58	chain.Emit("Post", "author", author.String(), "len", strconv.Itoa(len(msg)))
 59}
 60
 61// Count returns the total number of posts on the wall.
 62func Count() int {
 63	return len(posts)
 64}
 65
 66// Render renders the wall as Markdown, newest-first, pageSize posts per page.
 67// The page is parsed from path: "?page=N", "?p=N", "/N", or a bare "N" all
 68// select page N (1-indexed). Anything else defaults to page 1.
 69func Render(path string) string {
 70	total := len(posts)
 71	page := parsePage(path)
 72
 73	var b strings.Builder
 74	b.WriteString("# Microblog Wall\n\n")
 75
 76	if total == 0 {
 77		b.WriteString("_No posts yet. Be the first to post._\n")
 78		return b.String()
 79	}
 80
 81	pages := (total + pageSize - 1) / pageSize
 82	if page < 1 {
 83		page = 1
 84	}
 85	if page > pages {
 86		page = pages
 87	}
 88
 89	b.WriteString(strconv.Itoa(total))
 90	b.WriteString(" posts total · page ")
 91	b.WriteString(strconv.Itoa(page))
 92	b.WriteString(" of ")
 93	b.WriteString(strconv.Itoa(pages))
 94	b.WriteString("\n\n")
 95
 96	// Newest-first: post index total-1 is newest. For page p (1-indexed),
 97	// skip (p-1)*pageSize newest posts, then take up to pageSize.
 98	start := total - 1 - (page-1)*pageSize
 99	end := start - pageSize + 1
100	if end < 0 {
101		end = 0
102	}
103
104	for i := start; i >= end; i-- {
105		p := posts[i]
106		b.WriteString("- **")
107		b.WriteString(ui.AddrOf(p.Author))
108		b.WriteString("** · height ")
109		b.WriteString(strconv.FormatInt(p.Height, 10))
110		b.WriteString("\n\n  ")
111		b.WriteString(ui.Inline(p.Msg))
112		b.WriteString("\n")
113	}
114
115	b.WriteString("\n")
116	b.WriteString(renderNav(page, pages))
117	return b.String()
118}
119
120// renderNav builds a simple prev/next navigation footer as Markdown links.
121func renderNav(page, pages int) string {
122	var b strings.Builder
123	if page > 1 {
124		b.WriteString("[← newer](?page=")
125		b.WriteString(strconv.Itoa(page - 1))
126		b.WriteString(")")
127	}
128	if page > 1 && page < pages {
129		b.WriteString(" · ")
130	}
131	if page < pages {
132		b.WriteString("[older →](?page=")
133		b.WriteString(strconv.Itoa(page + 1))
134		b.WriteString(")")
135	}
136	if b.Len() == 0 {
137		return ""
138	}
139	return b.String() + "\n"
140}
141
142// parsePage extracts the 1-indexed page number from a Render path. It accepts
143// "?page=N", "?p=N", "/N", and a bare "N". Unrecognized input yields page 1.
144func parsePage(path string) int {
145	path = strings.TrimSpace(path)
146	if path == "" {
147		return 1
148	}
149
150	// Query form: "?page=2&foo=bar" or "?p=2".
151	if idx := strings.IndexByte(path, '?'); idx >= 0 {
152		query := path[idx+1:]
153		for _, part := range strings.Split(query, "&") {
154			kv := strings.SplitN(part, "=", 2)
155			if len(kv) != 2 {
156				continue
157			}
158			if kv[0] == "page" || kv[0] == "p" {
159				if n, err := strconv.Atoi(strings.TrimSpace(kv[1])); err == nil {
160					return n
161				}
162			}
163		}
164		return 1
165	}
166
167	// Path form: "/2" or "2".
168	seg := strings.TrimPrefix(path, "/")
169	if n, err := strconv.Atoi(seg); err == nil {
170		return n
171	}
172	return 1
173}