reactions.gno
5.58 Kb · 138 lines
1// Package reactions is a reaction block any realm can embed, keyed on the page
2// it appears on rather than on the realm that stores it.
3//
4// It is the shape the web settled on for this in 2010: one hosted widget, the
5// same snippet pasted everywhere, deciding what to show from the page
6// identifier it is handed. Here the snippet is two lines of gno:
7//
8// import "gno.land/r/moul/reactions/v0"
9//
10// func Render(path string) string {
11// return body + reactions.RenderBlock()
12// }
13//
14// [RenderBlock] takes no argument because it does not need one: a plain read
15// exported by a realm and called by another opens no realm frame, so
16// unsafe.CurrentRealm() inside it reports the CALLER. The host realm's own path
17// is the page key, and nothing has to be configured, registered or passed.
18// [RenderBlockFor] is the explicit form, for a realm that renders more than one
19// page or already holds its path as a constant.
20//
21// The host realm stores nothing. Every tally lives here, so a realm can add
22// reactions without a redeploy the next time it wants to change them, and a
23// reader's reaction survives the host realm being replaced.
24//
25// # What it is not
26//
27// Not a comment system. There is no text field anywhere, which is what makes
28// it possible to run with no moderation at all: six emoji, one reaction per
29// address per page, changeable and removable. The page shows the tally and who
30// reacted last. That is the whole feature.
31package reactions
32
33import (
34 "chain"
35 "chain/runtime"
36 "chain/runtime/unsafe"
37
38 rx "gno.land/p/moul/reactions/v0"
39)
40
41// realmPath is this realm's own path, the one its gnomod.toml module line
42// declares. It is written out rather than read from unsafe.CurrentRealm(),
43// which reports the CALLER in every read this realm exports and would therefore
44// build every link against whichever realm embedded the block.
45const realmPath = "gno.land/r/moul/reactions/v0"
46
47// board holds every page's tally.
48//
49// A redeploy would wipe it, which is why this realm is not private (see
50// gnomod.toml): the state here belongs to everyone who reacted, and the only
51// honest way to change this realm is a v1 beside it.
52var board = rx.NewBoard()
53
54// React records the caller's reaction to page.
55//
56// page is a package path, the full one: "gno.land/r/moul/home". It does not
57// have to be a realm that embeds the block, and nothing checks that it exists.
58// A page is just a name, which is what lets a realm key the block on something
59// finer than itself.
60//
61// key is one of the palette names, not a glyph: "up", "heart", "fire", "party",
62// "rocket", "eyes". Render("") lists them with their emoji.
63//
64// One reaction per address per page. Reacting again with a different key moves
65// it; reacting with the same key changes nothing but the "last reaction" line.
66func React(cur realm, page, key string) {
67 if !cur.IsCurrent() {
68 panic("spoofed realm: cur is not the live crossing frame")
69 }
70 if !rx.Valid(key) {
71 panic("reactions: no such reaction: " + key)
72 }
73 if !rx.ValidPage(page) {
74 panic("reactions: not a page key: " + page)
75 }
76
77 who := unsafe.PreviousRealm().Address()
78 board.React(page, who, key, runtime.ChainHeight())
79 chain.Emit("React", "page", page, "key", key, "who", who.String())
80}
81
82// Unreact removes the caller's reaction from page. It aborts when there is
83// none, rather than succeeding silently, so a misspelled page key is visible
84// instead of looking like a no-op.
85func Unreact(cur realm, page string) {
86 if !cur.IsCurrent() {
87 panic("spoofed realm: cur is not the live crossing frame")
88 }
89 who := unsafe.PreviousRealm().Address()
90 if !board.Unreact(page, who) {
91 panic("reactions: nothing to remove on " + page)
92 }
93 chain.Emit("Unreact", "page", page, "who", who.String())
94}
95
96// RenderBlock returns the block for the realm CALLING it, which is the form a
97// host realm embeds.
98//
99// It has no cur realm parameter on purpose. A plain read is borrowed: gno opens
100// no realm frame for it, so unsafe.CurrentRealm() reports the caller's path and
101// the block keys itself with no argument. Adding a cur realm here would
102// silently reverse that and key every embed to this realm instead.
103//
104// Calling it from inside this realm therefore keys on this realm, which is
105// never what a caller wants; the views below use the unexported blockFor.
106func RenderBlock() string {
107 return blockFor(unsafe.CurrentRealm().PkgPath())
108}
109
110// RenderBlockFor is [RenderBlock] for a named page. Use it from a crossing
111// function, where there is no caller to read off the stack, or when one realm
112// renders several pages.
113func RenderBlockFor(page string) string { return blockFor(page) }
114
115// blockFor is the shared body, so no exported read of this realm calls another
116// and picks up the wrong path.
117func blockFor(page string) string {
118 return rx.Block(realmPath, page, board.Page(page))
119}
120
121// Count reports how many addresses have a reaction on page.
122func Count(page string) int { return board.Page(page).Total() }
123
124// CountOf reports how many addresses hold one particular reaction on page.
125func CountOf(page, key string) int { return board.Page(page).Count(key) }
126
127// ReactionOf returns who's current reaction key on page, or "" when they have
128// none. A host realm can use it to say "you reacted" without reading the tree.
129func ReactionOf(page string, who address) string { return board.Page(page).Of(who) }
130
131// Last returns the most recent reaction on page: the palette key, the address,
132// and the height. The key is "" when the page has never been reacted to.
133func Last(page string) (key string, who address, height int64) {
134 return board.Page(page).Last()
135}
136
137// Pages reports how many pages have ever been reacted to.
138func Pages() int { return board.Pages() }