diff --git a/guide/docs/interactions/images/string-select.png b/guide/docs/interactions/images/string-select.png new file mode 100644 index 00000000..f2c80a00 Binary files /dev/null and b/guide/docs/interactions/images/string-select.png differ diff --git a/guide/docs/interactions/select-menus.mdx b/guide/docs/interactions/select-menus.mdx index d6c26adf..1e81b067 100644 --- a/guide/docs/interactions/select-menus.mdx +++ b/guide/docs/interactions/select-menus.mdx @@ -1,8 +1,298 @@ --- description: They refer to views, buttons and select menus that can be added to the messages your bot sends. -hide_table_of_contents: true --- # Select Menus - +Select Menus allow users to interact with your bot through an interactive dropdown component. This component provides selectable options for your users to choose from. +You can require users to select a minimum/maximum number of options using min_values and max_values. + +Select Menus are available as the following types: + +- disnake.ui.StringSelect Allows you to input custom string options + for users to select from, up to 25 +- disnake.ui.UserSelect User or Member objects as options +- disnake.ui.RoleSelect Role objects as options +- disnake.ui.ChannelSelect Channel objects as options + +The options for user, role, and channel select menus are populated by Discord, and currently cannot be limited to a specific subset. See [below](#other-selects) for details. + +This guide will walk you through a pretty basic use-case for a `StringSelect` dropdowns using [Views](#example-of-stringselects) and [low-level components](#views-vs-low-level-components). +If you need more examples, you can always check the examples in the [disnake repo](https://github.com/DisnakeDev/disnake/tree/master/examples). +  +:::note +A message can have a maximum of 5 rows of components. Select components each take a single row, therefore you cannot have more than 5 Select components per message +::: + +
+

+ StringSelect example +

+
+ +### Example of `StringSelect`s + + + + +```python +import os + +import disnake +from disnake.ext import commands + + +# Defines the StringSelect that contains animals that your users can choose from +class AnimalDropdown(disnake.ui.StringSelect): + def __init__(self): + + # Define the options that will be displayed inside the dropdown. + # You may not have more than 25 options. + # There is a `value` keyword that is being omitted, which is useful if + # you wish to display a label to the user, but handle a different value + # here within the code, like an index number or database id. + options = [ + disnake.SelectOption(label="Dog", description="Dogs are your favorite type of animal"), + disnake.SelectOption(label="Cat", description="Cats are your favorite type of animal"), + disnake.SelectOption( + label="Snake", description="Snakes are your favorite type of animal" + ), + disnake.SelectOption( + label="Gerbil", description="Gerbils are your favorite type of animal" + ), + ] + + # We will include a placeholder that will be shown until an option has been selected. + # The min and max values indicate the minimum and maximum number of options to be selected - + # in this example we will only allow one option to be selected. + super().__init__( + placeholder="Choose an animal", + min_values=1, + max_values=1, + options=options, + ) + + # This callback is called each time a user has selected an option + async def callback(self, inter: disnake.MessageInteraction): + # Use the interaction object to respond to the interaction. + # `self` refers to this StringSelect object, and the `values` + # attribute contains a list of the user's selected options. + # We only want the first (and in this case, only) one. + await inter.response.send_message(f"Your favorite type of animal is: {self.values[0]}") + + +# Now that we have created the Select object with its options and callback, we need to attach it to a +# View that will be displayed to the user in the command response. +# +# Views have a default timeout of 180s, at which the bot will stop listening for those events. +# You may pass any float here, or `None` if you wish to remove the timeout. +# Note: If `None` is passed, this view will persist only until the bot is restarted. If you wish to have persistent views, +# consider using low-level components or check out the persistent view example: +# https://github.com/DisnakeDev/disnake/blob/master/examples/views/persistent.py +class DropDownView(disnake.ui.View): + def __init__(self): + # You would pass a new `timeout=` if you wish to alter it, but + # we will leave it empty for this example so that it uses the default 180s. + super().__init__() + + # Now let's add the `StringSelect` object we created above to this view + self.add_item(AnimalDropdown()) + + +# Finally, we create a basic bot instance with a command that will utilize the created view and dropdown. +bot = commands.Bot() + + +@bot.listen() +async def on_ready(): + print(f"Logged in as {bot.user} (ID: {bot.user.id})\n------") + + +@bot.slash_command() +async def animals(inter: disnake.ApplicationCommandInteraction): + """Sends a message with our dropdown containing the animals""" + + # Create the view with our dropdown object + view = DropDownView() + + # Respond to the interaction with a message and our view + await inter.response.send_message("What is your favorite type of animal?", view=view) + + +if __name__ == "__main__": + bot.run(os.getenv("BOT_TOKEN")) +``` + + + + +```python +# Instead of subclassing `disnake.ui.StringSelect`, this example shows how to use the +# `@disnake.ui.string_select` decorator directly inside the View to create the dropdown +# component. +class AnimalView(disnake.ui.View): + def __init__(self): + super().__init__() + + # If you wish to pass a previously defined sequence of values to this `View` so that + # you may have dynamic options, you can do so by defining them within this __init__ method. + # `self.animal_callback.options = [...]` + + @disnake.ui.string_select( + placeholder="Choose an animal", + options=[ + disnake.SelectOption(label="Dog", description="Dogs are your favorite type of animal"), + disnake.SelectOption(label="Cat", description="Cats are your favorite type of animal"), + disnake.SelectOption( + label="Snake", description="Snakes are your favorite type of animal" + ), + disnake.SelectOption( + label="Gerbil", description="Gerbils are your favorite type of animal" + ), + ], + min_values=1, + max_values=1, + ) + async def animal_callback( + self, select: disnake.ui.StringSelect, inter: disnake.MessageInteraction + ): + # The main difference in this callback is that we access the `StringSelect` through the + # parameter passed to the callback, vs the subclass example where we access it via `self` + await inter.response.send_message(f"You favorite type of animal is: {select.values[0]}") +``` + + + + +### Other Selects + +The three other select components available are constructed and can be used in the same manner. +The main difference is that we do not create nor pass any options to these Select objects as Discord will provide these options to the user automatically. +The selected option(s) that are available in `self.values` will be the selected object(s) rather than the string values. + +- `UserSelect.values` will return a list of disnake.User or disnake.Member +- `RoleSelect.values` will return a list of disnake.Role +- `ChannelSelect.values` returns a list of disnake.abc.GuildChannel, disnake.Thread, or disnake.PartialMessageable + +:::note + +disnake.ui.ChannelSelect has an extra keyword argument that is +not available to the other SelectMenu types. + +`channel_types` will allow you to specify the types of channels made available as options (omit to show all available channels): + +- `channel_types=[ChannelType.text]` to display only guild text channels as options +- `channel_types=[ChannelType.voice, ChannelType.stage_voice]` to display only guild voice and stage channels as options +- etc. + +See disnake.ChannelType to see more channel types. +::: + +### Handling View Timeouts + +When a View times out, the bot will no longer listen for these events, and your users will receive an error `This interaction failed`. + +To avoid this, you have a couple of options when the view times out: + +1. Disable the components within the view so that they are no longer interactive. +2. Remove the view altogether, leaving only the original message without components. + +For this example we will disable the components using an `on_timeout` method. However, if you wish to remove the View completely you can pass `view=None` (instead of `view=self` like the example below). + +We'll continue with the `subclassing.py` example from above, only altering the relevant parts: + +```python title="viewtimeout.py" +... + + +class DropDownView(disnake.ui.View): + + message: disnake.Message # adding to typehint the future `message` attribute + + def __init__(self): + # this time we will set the timeout to 30.0s + super().__init__(timeout=30.0) + # now let's add the `StringSelect` object we created above to this view + self.add_item(AnimalDropdown()) + + # To handle this timeout, we'll need to override the default `on_timeout` method within the `View`` + async def on_timeout(self): + # Now we will edit the original command response so that it displays the disabled view. + # Since `self.children` returns a list of the components attached to this `View` and we want + # to prevent the components from remaining interactive after the timeout, we can easily loop + # over its components and disable them. + for child in self.children: + if isinstance(child, (disnake.ui.Button, disnake.ui.BaseSelect)): + child.disabled = True + + # Now, edit the message with this updated `View` + await self.message.edit(view=self) + + +... +# The only changes we need to make to handle the updated view is to fetch the original response after, +# so that the message can be edited later. +# This is necessary because `interaction.send` methods do not return a message object +@bot.slash_command() +async def animals(inter: disnake.ApplicationCommandInteraction): + """Sends a message with our dropdown containing the animals""" + + # Create the view with our dropdown object. + view = DropDownView() + + # Respond to the interaction with a message and our view. + await inter.response.send_message("What is your favorite type of animal?", view=view) + + # This will add a new `message` attribute to the `DropDownView` that will be edited + # once the view has timed out + view.message = await inter.original_response() + + +... +``` + +### Views vs. low-level components + +As an alternative to using `View`s, it is possible to use Select Menus as low-level components. +These components do not need to be sent as part of a View and can be sent as is. + +Note that any component being sent in this manner must have a `custom_id` explicitly set. Component interactions are sent to all listeners, +which means the `custom_id` should be unique for each component to be able to identify the component in your code. + +The main advantage of this is that listeners, by nature, are persistent and will remain fully functional over bot reloads. Listeners are stored in the bot +strictly once, and are shared by all components. Because of this, the memory footprint will generally be smaller than that of an equivalent view. + +The example below will be similar to the above examples, however will be executed as a low level component instead. + +```python title="low_level_dropdown.py" +@bot.slash_command() +async def animals(inter: disnake.ApplicationCommandInteraction): + """Sends a message with our dropdown containing the animals""" + + await inter.response.send_message( + "What is your favorite type of animal?", + components=[ + disnake.ui.StringSelect( + custom_id="fav_animal", + options=["Dog", "Cat", "Snake", "Gerbil"], + ) + ], + ) + + +# Now we create the listener that will handle the users's selection(s), similarly to the callback we used above. +@bot.listen("on_dropdown") +async def fav_animal_listener(inter: disnake.MessageInteraction): + # First we should check if the interaction is for the `fav_animal` dropdown we created + # and ignore if it isn't. + if inter.component.custom_id != "fav_animal": + return + + # Now we can respond with the user's favorite animal + await inter.response.send_message(f"Your favorite type of animal is: {inter.values[0]}") +``` + +:::note +These component listeners can be used inside cogs as well. Simply replace `@bot.listen()` with `@commands.Cog.listener()` and +be sure to pass `self` as the first argument of the listener function +::: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..089a65bb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "disnake-guide", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}