diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx index 9f82ce35..2348b0c1 100644 --- a/guide/docs/interactions/buttons.mdx +++ b/guide/docs/interactions/buttons.mdx @@ -1,5 +1,5 @@ --- -description: They refer to views, buttons and select menus that can be added to the messages your bot sends. +description: This section covers the usage of button components and how to implement them using Views and via low-level components. --- # Buttons @@ -25,41 +25,9 @@ Components allow users to interact with your bot through interactive UI elements
-The code for this command is given below. - -```python title="buttons.py" -# At the top of the file. -import disnake -from disnake.ext import commands - -# The slash command that responds with a message. -@bot.slash_command() -async def buttons(inter: disnake.ApplicationCommandInteraction): - await inter.response.send_message( - "Need help?", - components=[ - disnake.ui.Button(label="Yes", style=disnake.ButtonStyle.success, custom_id="yes"), - disnake.ui.Button(label="No", style=disnake.ButtonStyle.danger, custom_id="no"), - ], - ) - - -@bot.listen("on_button_click") -async def help_listener(inter: disnake.MessageInteraction): - if inter.component.custom_id not in ["yes", "no"]: - # We filter out any other button presses except - # the components we wish to process. - return - - if inter.component.custom_id == "yes": - await inter.response.send_message("Contact us at https://discord.gg/disnake!") - elif inter.component.custom_id == "no": - await inter.response.send_message("Got it. Signing off!") -``` - ## Building and sending buttons -## Button styles +### Button styles | Name | Syntax | Color | | --------- | ----------------------------------------------------------------- | ------- | @@ -88,13 +56,181 @@ async def help_listener(inter: disnake.MessageInteraction):
:::note - `Link` buttons _cannot_ have a `custom_id`, and _do not_ send an interaction event when clicked. +::: + +### Example of Buttons + +Button components, like [SelectMenu](select-menus.mdx) components, can be implemented using a View +or via a [low-level implementation](#Views-vs-low-level-components). +:::note +A message can include up to 25 button components, organized with a maximum of 5 buttons per row across 5 rows. ::: -### Disabled buttons +```python title="button_view.py" +import disnake +from disnake.ext import commands + + +class ButtonView(disnake.ui.View): + # Here, we use `@disnake.ui.button` (notice the lower `b`) + # to create the buttons and assign the decorated functions as the button callbacks. + # In this case, we simply respond to the interaction with a message + # then explicitly stop the View from continuing to listen for further interactions. + + @disnake.ui.button(label="Yes", style=disnake.ButtonStyle.success) + async def success(self, button: disnake.ui.Button, inter: disnake.MessageInteraction): + await inter.response.send_message("Contact us at https://discord.gg/disnake!") + self.stop() + + @disnake.ui.button(label="No", style=disnake.ButtonStyle.danger) + async def no(self, button: disnake.ui.Button, inter: disnake.MessageInteraction): + await inter.response.send_message("Got it. Signing off!") + self.stop() + + +# The slash command that will respond with a message and the View. +@bot.slash_command() +async def buttons_View(inter: disnake.ApplicationCommandInteraction): + await inter.response.send_message("Need help?", view=ButtonView()) +``` + +## Handling View timeout and stopping Views + +One of the key advantages of using a View is the streamlined management +of your components' state and the convenient setting up and handling of timeouts. + +By default, when a View has timed out, the buttons remain attached to the message and appear to be interactable, however, if a user attempts to +interact with one of thse buttons, an error is displayed. This is also applies `View`s that have been explicitly stopped. To avoid this poor +user experience, we can customize what happens when a `View` has timed out by altering the on_timeout +method and then updating our button callbacks to manage the state of components when the `View` is to be stopped. + +Below is an example of handling this type of implementation. + +```python title="timeout.py" +import disnake +from disnake.ext import commands + + +class TimeoutButtonView(disnake.ui.View): -## Receiving button callback + # This type hints that the future `self.message` attribute that will be accessed is + # a disnake.InteractionMessage type. + message: disnake.InteractionMessage + + def __init__(self, timeout: float = 180.0): + # Again, we want to set a timeout for this View, but this time we will assign it + # when constructing the View later. + super().__init__(timeout=timeout) + + def disable_children(self): + # Simply loop over `self.children` to access each component within the View + # and set `.disabled=True` for each of the children. + for child in self.children: + if isinstance(child, (disnake.ui.Button, disnake.ui.BaseSelect)): + child.disabled = True + + async def on_timeout(self): + # When the View has timed out we want to disable all components associated with this View. + # A disabled component will still appear on the message, but will be greyed and non-interactable. + self.disable_children() + + # Now that the View's children have been disabled, we edit the message by sending the updated View. + # Since the View has timed out, it does not need to be explicitly stopped. + await self.message.edit(view=self) + + # You may also remove all components by calling `self.clear_items()` which returns the View with + # no components. + # Sending this updated View in the `message.edit` will remove the components from the message. + # await self.message.edit(view=self.clear_items()) + + @disnake.ui.button(label="Yes", style=disnake.ButtonStyle.success) + async def yes(self, button: disnake.ui.Button, inter: disnake.MessageInteraction): + + await inter.response.send_message("Contact us at https://discord.gg/disnake!") + + # Update the View by disabling all buttons, then editing the original message + # containing this View, not the response message sent above. + # Note: This can also be done with `inter.response.edit_message()` instead + # if you did not wish to send a separate response. + self.disable_children() + await inter.message.edit(view=self) + + # Now, we explicitly stop the View which prevents it from listening for more interactions. + self.stop() + + @disnake.ui.button(label="No", style=disnake.ButtonStyle.danger) + async def no(self, button: disnake.ui.Button, inter: disnake.MessageInteraction): + + await inter.response.send_message("Got it. Signing off!") + + self.disable_children() + await inter.message.edit(view=self) + + self.stop() + + +... +... +# The slash command that will respond with a message and the View. +@bot.slash_command() +async def buttons(inter: disnake.ApplicationCommandInteraction): + + # Here we need to construct the TimeoutButtonView so that it can be sent with + # the slash command response message. Since we do not have the message object yet, + # we assign it after the response has been sent. + view = TimeoutButtonView(timeout=120) + await inter.response.send_message("Need help?", view=view) + + # Because interaction responses do not return a message, we will need + # to fetch the message and assign it to `View.message` to be used if `on_timeout` is called. + view.message = await inter.original_response() +``` ## Views vs. low-level components + +With disnake, you have the flexibility to implement low-level components instead of relying on a View. + +It's important to highlight that when sending components using this approach, it's essential to assign a unique `custom_id` to each component. +This is necessary because component interactions are broadcasted to all listeners, and you need to distinguish which listener is intended for +each component. You can handle multiple component interactions with a single listnener. + +The primary benefit of adopting this technique is that these low-level components and listeners are inherently persistent, retaining their +functionality even when the bot restarts. Listeners are stored in the bot only once and are shared across all components. As a result, this +approach generally has a smaller memory footprint compared to an equivalent View. + +The example below demonstrates a similar user experience to the above example, however, it will be implmented using low-level components +and will not timeout nor be stopped. As a result, the button components will not be disabled after use. + +```python title="buttons.py" +# At the top of the file. +import disnake +from disnake.ext import commands + +# The slash command that responds with a message. +@bot.slash_command() +async def buttons(inter: disnake.ApplicationCommandInteraction): + await inter.response.send_message( + "Need help?", + components=[ + disnake.ui.Button(label="Yes", style=disnake.ButtonStyle.success, custom_id="yes"), + disnake.ui.Button(label="No", style=disnake.ButtonStyle.danger, custom_id="no"), + ], + ) + + +# Create the listener that will handle the button interactions, similarly to the callbacks used above. +@bot.listen("on_button_click") +async def help_listener(inter: disnake.MessageInteraction): + if inter.component.custom_id == "yes": + await inter.response.send_message("Contact us at https://discord.gg/disnake!") + elif inter.component.custom_id == "no": + await inter.response.send_message("Got it. Signing off!") +``` + +## More Examples + +Use the links below to View more button examples using Views and low-level implementations in the repo: +[View examples](https://github.com/DisnakeDev/disnake/tree/master/examples/views/button) +[Low-level](https://github.com/DisnakeDev/disnake/blob/master/examples/interactions/low_level_components.py)