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

v0 source realm

Package reactions is a reaction block any realm can embed, keyed on the page it appears on rather than on the realm t...

Readme View source

gno.land/r/moul/reactions/v0

A reaction block any realm can embed, keyed on the page it appears on rather than on the realm that stores it.

It is the shape the web settled on for this in 2010: one hosted widget, the same snippet pasted everywhere, deciding what to show from the page identifier it is handed. Here the snippet is two lines of gno.

1import "gno.land/r/moul/reactions/v0"
2
3func Render(path string) string {
4	return body + reactions.RenderBlock()
5}

That is the whole integration. Your realm stores nothing, registers nothing and configures nothing, and redeploying it never loses anyone's reaction.

Why RenderBlock needs no argument

A plain read exported by one realm and called by another is borrowed: gno opens no realm frame for it, so unsafe.CurrentRealm() inside it reports the caller. RenderBlock uses that as the page key, which is why the same line does the right thing in every realm that pastes it. (Measured on gnolang/gno@master 2026-09-22, recorded in this repo's AGENTS.md; it is the same mechanism behind r/moul/config's TopBlock.)

Adding a cur realm parameter would silently reverse it and key every embed to this realm, which is exactly why there is none.

RenderBlockFor(page) is the explicit form: use it from a crossing function, where there is no caller to read off the stack, or to key one block per article on the path you already serve.

1return body + reactions.RenderBlockFor("gno.land/r/you/blog:" + path)

What it is, and what it refuses to be

Not a comment system. There is no text field anywhere, which is what lets it run with no moderation at all: a closed palette of six, one reaction per address per page, changeable and removable. The block shows the tally and who reacted last. That is the feature.

key
up 👍
heart 💜
fire 🔥
party 🎉
rocket 🚀
eyes 👀

React aborts on anything else. That is the entire content policy, and it is a compile-time constant rather than a list someone has to curate.

The surface

React(cur, page, key) record the caller's reaction. Reacting again with a different key moves it; with the same key it only updates the "last reaction" line
Unreact(cur, page) remove it. Aborts when there is none, rather than succeeding silently
RenderBlock() the embed, keyed on the calling realm
RenderBlockFor(page) the embed for a named page
Count · CountOf · ReactionOf · Last · Pages reads, for a host realm that wants the numbers without the markdown

Render("") is the getting-started page plus the board; Render("<page>") is one page's tally, which is where the link under every block goes.

Two things that are permanent, and were decided on purpose

This realm is public, so it can never be redeployed. A private realm cannot be imported at all, and being imported is the entire feature. The redeploy that private = true would have bought is given up deliberately: the tallies under this path belong to everyone who reacted, not to the deployer, and a redeploy wipes every package-level variable. A fix ships as a v1 beside this one that keeps reading it, never as a replacement.

The page key is not checked against anything. A page is just a name: nothing verifies that the realm exists, that it embeds the block, or that the caller has any relationship to it. That is what lets a realm key the block on something finer than itself, and it means the board index is an open list. The index is capped at 25 rows for that reason; every page keeps its own view regardless.

Not to be confused with

r/moul/x/daily/reactions is an older, unrelated experiment: a single self-contained board with its own "topics", no embedding, and no way for a reader to change or remove a reaction. Nothing is shared between the two.

Where the code is

The engine (pages, tallies, palette, the block markdown) is p/moul/reactions/v0. This realm is the authority half: who may write, what the chain says the height and the caller are, and the two views.

First host: r/moul/home, which renders it as a :reactions: layout placeholder.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

gno.land/r/moul/reactions/v0 dependency graph

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.

Overview

Package reactions is a reaction block any realm can embed, keyed on the page it appears on rather than on the realm that stores it.

It is the shape the web settled on for this in 2010: one hosted widget, the same snippet pasted everywhere, deciding what to show from the page identifier it is handed. Here the snippet is two lines of gno:

Example
1import "gno.land/r/moul/reactions/v0"
2
3func Render(path string) string {
4	return body + reactions.RenderBlock()
5}

RenderBlock takes no argument because it does not need one: a plain read exported by a realm and called by another opens no realm frame, so unsafe.CurrentRealm() inside it reports the CALLER. The host realm's own path is the page key, and nothing has to be configured, registered or passed. RenderBlockFor is the explicit form, for a realm that renders more than one page or already holds its path as a constant.

The host realm stores nothing. Every tally lives here, so a realm can add reactions without a redeploy the next time it wants to change them, and a reader's reaction survives the host realm being replaced.

What it is not

Not a comment system. There is no text field anywhere, which is what makes it possible to run with no moderation at all: six emoji, one reaction per address per page, changeable and removable. The page shows the tally and who reacted last. That is the whole feature.

Functions 9

func Count

Action
1func Count(page string) int
source

Count reports how many addresses have a reaction on page.

func CountOf

Action
1func CountOf(page, key string) int
source

CountOf reports how many addresses hold one particular reaction on page.

func Pages

Action
1func Pages() int
source

Pages reports how many pages have ever been reacted to.

func React

crossing Action
1func React(cur realm, page, key string)
source

React records the caller's reaction to page.

page is a package path, the full one: "gno.land/r/moul/home". It does not have to be a realm that embeds the block, and nothing checks that it exists. A page is just a name, which is what lets a realm key the block on something finer than itself.

key is one of the palette names, not a glyph: "up", "heart", "fire", "party", "rocket", "eyes". Render("") lists them with their emoji.

One reaction per address per page. Reacting again with a different key moves it; reacting with the same key changes nothing but the "last reaction" line.

func ReactionOf

Action
1func ReactionOf(page string, who address) string
source

ReactionOf returns who's current reaction key on page, or "" when they have none. A host realm can use it to say "you reacted" without reading the tree.

func Render

1func Render(path string) string
source

Render serves two views:

Example
1""       the getting-started page: how to embed the block, plus the board
2"<page>" one page's tally, where <page> is a package path

The page view's path is the key itself, so the link under any embedded block is just this realm's path, a colon, and the page. That is why a page key may contain a colon of its own: "gno.land/r/you/blog:hello" reads back whole.

func RenderBlock

Action
1func RenderBlock() string
source

RenderBlock returns the block for the realm CALLING it, which is the form a host realm embeds.

It has no cur realm parameter on purpose. A plain read is borrowed: gno opens no realm frame for it, so unsafe.CurrentRealm() reports the caller's path and the block keys itself with no argument. Adding a cur realm here would silently reverse that and key every embed to this realm instead.

Calling it from inside this realm therefore keys on this realm, which is never what a caller wants; the views below use the unexported blockFor.

func RenderBlockFor

Action
1func RenderBlockFor(page string) string
source

RenderBlockFor is RenderBlock for a named page. Use it from a crossing function, where there is no caller to read off the stack, or when one realm renders several pages.

func Unreact

crossing Action
1func Unreact(cur realm, page string)
source

Unreact removes the caller's reaction from page. It aborts when there is none, rather than succeeding silently, so a misspelled page key is visible instead of looking like a no-op.

Imports 7

Source Files 5