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()