Fresh data with Prop and load()¶
Some things should be read fresh on every click instead of saved in the message, such as a database row. Mark them
risa.Prop and fill them in load().
A leaderboard¶
game is a field like any other, saved in the message. rows is a
risa.Prop, and load() fills
it. Scores stands in for your database, and you can find it in the full file.
@risa.register(name="leaderboard")
class Leaderboard(risa.View):
game: str = "chess"
rows: risa.Prop[list[str]] = msgspec.field(default_factory=list[str])
async def load(self, scores: Scores) -> None:
self.rows = await scores.top(self.game)
load() names Scores as a parameter, as a handler would, and
main() registers it. Without that, client.build raises linkd's DependencyNotSatisfiableException.
msgspec.field(default_factory=list[str]) gives each view a new empty list.
render() reads a prop as it reads a field. Each game's button runs switch, and _TOP at the top of the file holds
each game's top players.
def render(self) -> ui.Layout:
lines = [f"{n}. {name}" for n, name in enumerate(self.rows, start=1)]
buttons = [
ui.Button(
risa.bind(self.switch, game),
label=game.title(),
disabled=game == self.game,
)
for game in _TOP
]
return ui.Container(
ui.TextDisplay(f"## Top players in {self.game}"),
ui.TextDisplay("\n".join(lines)),
ui.Row(*buttons, ui.Button(self.refresh, label="Refresh")),
)
In Discord
Top players in chess
1. Mika
2. Jo
3. Sam
What a prop costs¶
A prop takes no room in the message: nothing about rows goes into the buttons' ids, however many rows load()
returns. It needs a default, because each click builds the view with its props at their defaults before load() runs,
and without one @risa.register raises ViewDeclarationError.
A prop is never saved, so what a handler puts in one lasts for that click's rerender, and the next click starts from the default again.
When load() runs¶
- 1Fields returnFrom the message.
- 2
load()Fills the props. - 3The handlerChanges fields.
- 4
rerender()Draws fields and props.
client.build runs it too, before render(), so the first message has its rows. On a click, refresh only rerenders,
and the rows are new because load() has just read them.
@risa.handler
async def refresh(self, ctx: risa.ComponentContext) -> None:
await ctx.rerender()
load() runs inside the three seconds from Answering in time, and its time counts toward
auto-defer's 2 seconds.
A handler that changes what load() reads¶
load() runs before the handler, and not again.
A handler that sets self.game and calls ctx.rerender() draws the new heading over the old rows, because load()
read them while game still held the old value.
Read them again in the handler. switch takes the game as a wire argument, as in
Handlers and arguments, and after it scores, which risa injects
as it does for load(). A handler's injected parameter needs no default.
@risa.handler
async def switch(
self,
ctx: risa.ComponentContext,
game: str,
scores: Scores,
) -> None:
self.game = game
self.rows = await scores.top(game)
await ctx.rerender()
After a click on Go
Top players in go
1. Noor
2. Mika
3. Ren
Full file
"""A field for what to show, and a prop for what the database says right now."""
from __future__ import annotations
import asyncio
import os
import hikari
import msgspec
import risa
from risa import ui
_TOP = {
"chess": ["Mika", "Jo", "Sam"],
"go": ["Noor", "Mika", "Ren"],
}
class Scores:
async def top(self, game: str) -> list[str]:
await asyncio.sleep(0.1)
return _TOP[game]
@risa.register(name="leaderboard")
class Leaderboard(risa.View):
game: str = "chess"
rows: risa.Prop[list[str]] = msgspec.field(default_factory=list[str])
async def load(self, scores: Scores) -> None:
self.rows = await scores.top(self.game)
def render(self) -> ui.Layout:
lines = [f"{n}. {name}" for n, name in enumerate(self.rows, start=1)]
buttons = [
ui.Button(
risa.bind(self.switch, game),
label=game.title(),
disabled=game == self.game,
)
for game in _TOP
]
return ui.Container(
ui.TextDisplay(f"## Top players in {self.game}"),
ui.TextDisplay("\n".join(lines)),
ui.Row(*buttons, ui.Button(self.refresh, label="Refresh")),
)
@risa.handler
async def refresh(self, ctx: risa.ComponentContext) -> None:
await ctx.rerender()
@risa.handler
async def switch(
self,
ctx: risa.ComponentContext,
game: str,
scores: Scores,
) -> None:
self.game = game
self.rows = await scores.top(game)
await ctx.rerender()
def add_dependencies(client: risa.Client) -> None:
registry = client.di.registry_for(risa.Contexts.DEFAULT)
registry.register_value(Scores, Scores())
def main() -> None:
bot = hikari.GatewayBot(
os.environ["DISCORD_TOKEN"],
intents=hikari.Intents.ALL_UNPRIVILEGED | hikari.Intents.MESSAGE_CONTENT,
)
client = risa.client_from_app(bot)
add_dependencies(client)
client.add_view(Leaderboard)
@bot.listen()
async def on_message(event: hikari.GuildMessageCreateEvent) -> None:
if event.content == "!leaderboard":
rendered = await client.build(Leaderboard())
await rendered.send_to(bot.rest, event.channel_id)
bot.run()
if __name__ == "__main__":
main()