From f6e0d5a5f3b7f71d66a4fab6e91dcd7b160fc9c6 Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Tue, 15 Aug 2023 15:44:09 -0500
Subject: [PATCH 01/11] complete buttons.mdx
- Add basic View example
- Add example for handling timeout (disabling buttons and clear_items)
- Move low level example to Views vs low-level section
---
guide/docs/interactions/buttons.mdx | 230 +++++++++++++++++++++++-----
1 file changed, 192 insertions(+), 38 deletions(-)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index 9f82ce35..b8166371 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,199 @@ async def help_listener(inter: disnake.MessageInteraction):
:::note
-
`Link` buttons _cannot_ have a `custom_id`, and _do not_ send an interaction event when clicked.
-
:::
-### Disabled buttons
+### 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).
+
+```python title="button_view.py"
+# At top of file.
+import disnake
+from disnake.ext import commands
+
+
+class ButtonView(disnake.ui.View):
+ def __init__(self):
+ # Views by default have a 180s `timeout`
+ super().__init__()
+
+ # In this example, the buttons are created using `@disnake.ui.button` (Notice the lower `b`)
+ # to create the buttons and assign the decorated functions as the button callback.
+
+ @disnake.ui.button(label="Yes", style=disnake.ButtonStyle.success)
+ async def success(self, button: disnake.ui.Button, inter: disnake.MessageInteraction):
+
+ # In this example we simply respond to the interaction with a message
+ # then explicitly stop the View from continuing to listen.
+ 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"
+# At top of file
+import disnake
+from disnake.ext import commands
-## Receiving button callback
+
+class TimeoutButtonView(disnake.ui.View):
+
+ # 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 the buttons, then editing the original message
+ # the View was attached to, not the response message sent above.
+ # You could also do this as part of the interaction.response by using
+ # `inter.response.edit_message` which would handle the required response
+ # and editing of the original message.
+ self.disable_children()
+ await inter.message.edit(view=self)
+ # We still want to explicitly stop the View after a user has interacted
+ # to ensure it's no longer usable.
+ 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!")
+
+ # Update the View by disabling the buttons, then editing the original message
+ # the View was attached to, not the response message sent above.
+ # You could also do this as part of the interaction.response by using
+ # `inter.response.edit_message` which would handle the required response
+ # and editing of the original message.
+ self.disable_children()
+ await inter.message.edit(view=self)
+ # We still want to explicitly stop the View after a user has interacted
+ # to ensure it's no longer usable.
+ 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 this type of interaction response does 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 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!")
+```
+
+## 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)
From d6a32e2a4e233787615fa64ae6a306b948c18d2c Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Wed, 16 Aug 2023 07:23:50 -0500
Subject: [PATCH 02/11] add max button note
---
guide/docs/interactions/buttons.mdx | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index b8166371..556ef841 100644
--- a/guide/docs/interactions/buttons.mdx
+++ b/guide/docs/interactions/buttons.mdx
@@ -64,6 +64,10 @@ Components allow users to interact with your bot through interactive UI elements
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.
+:::
+
```python title="button_view.py"
# At top of file.
import disnake
From b517a001c906fb13e854e9e6441a425cfbc6653c Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Sun, 27 Aug 2023 10:01:02 -0500
Subject: [PATCH 03/11] Update guide/docs/interactions/buttons.mdx
remove unnecessary "default timeout" comment
Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com>
Signed-off-by: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
---
guide/docs/interactions/buttons.mdx | 4 ----
1 file changed, 4 deletions(-)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index 556ef841..c3c8e891 100644
--- a/guide/docs/interactions/buttons.mdx
+++ b/guide/docs/interactions/buttons.mdx
@@ -75,10 +75,6 @@ from disnake.ext import commands
class ButtonView(disnake.ui.View):
- def __init__(self):
- # Views by default have a 180s `timeout`
- super().__init__()
-
# In this example, the buttons are created using `@disnake.ui.button` (Notice the lower `b`)
# to create the buttons and assign the decorated functions as the button callback.
From 7a659cc0d9ae56073ab116c152436887b9b8909f Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Sun, 27 Aug 2023 10:01:53 -0500
Subject: [PATCH 04/11] Update guide/docs/interactions/buttons.mdx
correct View Example link
Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com>
Signed-off-by: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
---
guide/docs/interactions/buttons.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index c3c8e891..0eb9df43 100644
--- a/guide/docs/interactions/buttons.mdx
+++ b/guide/docs/interactions/buttons.mdx
@@ -250,5 +250,5 @@ async def help_listener(inter: disnake.MessageInteraction):
## 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)
+[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)
From 09eaf5601210c78cf01f888c5558263d4050b8ff Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Sun, 27 Aug 2023 10:03:01 -0500
Subject: [PATCH 05/11] Update guide/docs/interactions/buttons.mdx
remove unneeded comment in disabled button example
Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com>
Signed-off-by: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
---
guide/docs/interactions/buttons.mdx | 1 -
1 file changed, 1 deletion(-)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index 0eb9df43..184193e7 100644
--- a/guide/docs/interactions/buttons.mdx
+++ b/guide/docs/interactions/buttons.mdx
@@ -69,7 +69,6 @@ A message can include up to 25 button components, organized with a maximum of 5
:::
```python title="button_view.py"
-# At top of file.
import disnake
from disnake.ext import commands
From cde1dabc6aa8302a27962736dc2bd14433ad3702 Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Sun, 27 Aug 2023 10:07:04 -0500
Subject: [PATCH 06/11] Update guide/docs/interactions/buttons.mdx
adjust comment structure in button_view example
Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com>
Signed-off-by: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
---
guide/docs/interactions/buttons.mdx | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index 184193e7..48836ff0 100644
--- a/guide/docs/interactions/buttons.mdx
+++ b/guide/docs/interactions/buttons.mdx
@@ -74,20 +74,18 @@ from disnake.ext import commands
class ButtonView(disnake.ui.View):
- # In this example, the buttons are created using `@disnake.ui.button` (Notice the lower `b`)
- # to create the buttons and assign the decorated functions as the button callback.
+ # 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):
-
- # In this example we simply respond to the interaction with a message
- # then explicitly stop the View from continuing to listen.
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()
From 7c72a305b5e24b9d0c62150d70eca76c53a19b62 Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Sun, 27 Aug 2023 10:07:56 -0500
Subject: [PATCH 07/11] Update guide/docs/interactions/buttons.mdx
remove ellipses from example
Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com>
Signed-off-by: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
---
guide/docs/interactions/buttons.mdx | 2 --
1 file changed, 2 deletions(-)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index 48836ff0..6f2aaa6d 100644
--- a/guide/docs/interactions/buttons.mdx
+++ b/guide/docs/interactions/buttons.mdx
@@ -90,8 +90,6 @@ class ButtonView(disnake.ui.View):
self.stop()
-...
-...
# The slash command that will respond with a message and the View.
@bot.slash_command()
async def buttons_View(inter: disnake.ApplicationCommandInteraction):
From b919751f7c8b0e9a59348a4ab4996915360cdd54 Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Sun, 27 Aug 2023 10:08:12 -0500
Subject: [PATCH 08/11] Update guide/docs/interactions/buttons.mdx
remove unnecessary comment in timeout example
Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com>
Signed-off-by: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
---
guide/docs/interactions/buttons.mdx | 1 -
1 file changed, 1 deletion(-)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index 6f2aaa6d..1757c2b4 100644
--- a/guide/docs/interactions/buttons.mdx
+++ b/guide/docs/interactions/buttons.mdx
@@ -109,7 +109,6 @@ method and then updating our button callbacks to manage the state of components
Below is an example of handling this type of implementation.
```python title="timeout.py"
-# At top of file
import disnake
from disnake.ext import commands
From 222c413629fd747394a6452f06e2986d6945c31b Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Sun, 27 Aug 2023 10:10:39 -0500
Subject: [PATCH 09/11] Update guide/docs/interactions/buttons.mdx
wording in timeout example slash command
Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com>
Signed-off-by: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
---
guide/docs/interactions/buttons.mdx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index 1757c2b4..ec45652c 100644
--- a/guide/docs/interactions/buttons.mdx
+++ b/guide/docs/interactions/buttons.mdx
@@ -190,9 +190,9 @@ async def buttons(inter: disnake.ApplicationCommandInteraction):
view = TimeoutButtonView(timeout=120)
await inter.response.send_message("Need help?", view=view)
- # Because this type of interaction response does not return a message, we will need
+ # 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()
+ view.message = await inter.original_response()
```
## Views vs. low-level components
From f7c17bfd03296185ca9a64b1b0d824402424304a Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Sun, 27 Aug 2023 10:11:35 -0500
Subject: [PATCH 10/11] Update guide/docs/interactions/buttons.mdx
remove unneeded custom_id check in low level example
Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com>
Signed-off-by: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
---
guide/docs/interactions/buttons.mdx | 5 -----
1 file changed, 5 deletions(-)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index ec45652c..01bf7d3d 100644
--- a/guide/docs/interactions/buttons.mdx
+++ b/guide/docs/interactions/buttons.mdx
@@ -230,11 +230,6 @@ async def buttons(inter: disnake.ApplicationCommandInteraction):
# 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 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":
From d0bc2a3b589c8d37ca20de4fc6a7d0401fa1f7bd Mon Sep 17 00:00:00 2001
From: DLCHAMP <36091350+dlchamp@users.noreply.github.com>
Date: Sun, 27 Aug 2023 12:17:39 -0500
Subject: [PATCH 11/11] reword and reduce comments
reword comments in timeout example
remove duplicate comments from second button callback.
---
guide/docs/interactions/buttons.mdx | 21 +++++++--------------
1 file changed, 7 insertions(+), 14 deletions(-)
diff --git a/guide/docs/interactions/buttons.mdx b/guide/docs/interactions/buttons.mdx
index 01bf7d3d..2348b0c1 100644
--- a/guide/docs/interactions/buttons.mdx
+++ b/guide/docs/interactions/buttons.mdx
@@ -150,15 +150,14 @@ class TimeoutButtonView(disnake.ui.View):
await inter.response.send_message("Contact us at https://discord.gg/disnake!")
- # Update the View by disabling the buttons, then editing the original message
- # the View was attached to, not the response message sent above.
- # You could also do this as part of the interaction.response by using
- # `inter.response.edit_message` which would handle the required response
- # and editing of the original message.
+ # 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)
- # We still want to explicitly stop the View after a user has interacted
- # to ensure it's no longer usable.
+
+ # 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)
@@ -166,15 +165,9 @@ class TimeoutButtonView(disnake.ui.View):
await inter.response.send_message("Got it. Signing off!")
- # Update the View by disabling the buttons, then editing the original message
- # the View was attached to, not the response message sent above.
- # You could also do this as part of the interaction.response by using
- # `inter.response.edit_message` which would handle the required response
- # and editing of the original message.
self.disable_children()
await inter.message.edit(view=self)
- # We still want to explicitly stop the View after a user has interacted
- # to ensure it's no longer usable.
+
self.stop()