A library to create a discord.py 2.0+ paginator (reaction menu/buttons menu). Supports pagination with buttons, reactions, and category selection using selects.
class reactionmenu.ReactionMenu(method: Union[Context, discord.Interaction], /, *, menu_type: MenuType, **kwargs)
A ReactionMenu is a menu that uses emojis which are either custom guild emojis or a normal emoji to control the pagination process. If you're not looking for any of the fancy features and just want something simple, this is the one to use.
class reactionmenu.ViewMenu(method: Union[Context, discord.Interaction], /, *, menu_type: MenuType, **kwargs)
A ViewMenu is a menu that uses discords Buttons feature. With buttons, you can enable and disable them, set a certain color for them with emojis, have buttons that send hidden messages, and add hyperlinks. This library offers a broader range of functionalities such as who pressed the button, how many times it was pressed and more. It uses views (discord.ui.View) to implement the Buttons functionality, but uses some of its own methods in order to make a Button pagination menu simple.
Click to show ViewMenu documentation
### How to import
```py
from reactionmenu import ViewMenu, ViewButton
```
---
### Parameters of the ViewMenu constructor
* `method` (`Union[discord.ext.commands.Context, discord.Interaction]`) A context or interaction object
* `menu_type` (`MenuType`) The configuration of the menu
* `ViewMenu.TypeEmbed`, a normal embed pagination menu
* `ViewMenu.TypeEmbedDynamic`, an embed pagination menu with dynamic data
* `ViewMenu.TypeText`, a text only pagination menu
---
### Kwargs of the ViewMenu constructor
| Name | Type | Default Value | Used for | Info
-------|------|---------------|----------|------
| `wrap_in_codeblock` | `str` | `None` | `ViewMenu.TypeEmbedDynamic` | The discord codeblock language identifier to wrap your data in. Example: `ViewMenu(ctx, ..., wrap_in_codeblock='py')`
| `custom_embed` | `discord.Embed` | `None` | `ViewMenu.TypeEmbedDynamic` | Embed object to use when adding data with `ViewMenu.add_row()`. Used for styling purposes
| `delete_on_timeout` | `bool` | `False` | `All menu types` | Delete the menu when it times out
| `disable_items_on_timeout` | `bool` | `True` | `All menu types` | Disable the items on the menu when the menu times out
| `remove_items_on_timeout` | `bool` | `False` | `All menu types` | Remove the items on the menu when the menu times out
| `only_roles` | `List[discord.Role]` | `None` | `All menu types` | If set, only members with any of the given roles are allowed to control the menu. The menu owner can always control the menu
| `timeout` | `Union[int, float, None]` | `60.0` | `All menu types` | The timer for when the menu times out. Can be `None` for no timeout
| `show_page_director` | `bool` | `True` | `All menu types` | Shown at the bottom of each embed page. "Page 1/20"
| `name` | `str` | `None` | `All menu types` | A name you can set for the menu
| `style` | `str` | `"Page $/&"` | `All menu types` | A custom page director style you can select. "$" represents the current page, "&" represents the total amount of pages. Example: `ViewMenu(ctx, ..., style='On $ out of &')`
| `all_can_click` | `bool` | `False` | `All menu types` | Sets if everyone is allowed to control when pages are 'turned' when buttons are clicked
| `delete_interactions` | `bool` | `True` | `All menu types` | Delete the prompt message by the bot and response message by the user when asked what page they would like to go to when using `ViewButton.ID_GO_TO_PAGE`
| `rows_requested` | `int` | `None` | `ViewMenu.TypeEmbedDynamic` | The amount of information per `ViewMenu.add_row()` you would like applied to each embed page
---
### Pages for ViewMenu
Depending on the `menu_type`, pages can either be a `str`, `discord.Embed`, or a combination of `content` or `files` ([example below](#stacked-pages-1))
* If the `menu_type` is `ViewMenu.TypeEmbed`, use embeds
* If the `menu_type` is `ViewMenu.TypeText` (text only menu) or `ViewMenu.TypeEmbedDynamic` (embed only menu), use strings.
* Associated methods
* `ViewMenu.add_page(embed: discord.Embed=MISSING, content: Optional[str]=None, files: Optional[Sequence[discord.File]]=None)`
* `ViewMenu.add_pages(pages: Sequence[Union[discord.Embed, str]])`
* `ViewMenu.add_row(data: str)`
* `ViewMenu.remove_all_pages()`
* `ViewMenu.clear_all_row_data()`
* `ViewMenu.remove_page(page_number: int)`
* `ViewMenu.set_main_pages(*embeds: Embed)`
* `ViewMenu.set_last_pages(*embeds: Embed)`
#### Adding Pages
```py
# ViewMenu.TypeEmbed
menu = ViewMenu(method, menu_type=ViewMenu.TypeEmbed)
menu.add_page(summer_embed)
menu.add_page(winter_embed)
# ViewMenu.TypeText
menu = ViewMenu(method, menu_type=ViewMenu.TypeText)
menu.add_page(content='Its so hot!')
menu.add_page(content='Its so cold!')
```
#### ViewMenu.TypeText
A `TypeText` menu is a text based pagination menu. No embeds are involved in the pagination process, only plain text is used.
![text_view_showcase](https://imgur.com/X9me743.gif)
#### Stacked Pages
With `v3.1.0+`, you can paginate with more than just an embed or text. You can combine text, embeds, as well as files. But depending on the `menu_type` the combination can be restricted. Here is an example of a menu with a `menu_type` of `TypeEmbed` that is stacked.
```py
# You can use regular commands as well
@bot.tree.command(description="These are stacked pages", guild=discord.Object(id=...))
async def stacked(interaction: discord.Interaction):
menu = ViewMenu(interaction, menu_type=ViewMenu.TypeEmbed)
menu.add_page(discord.Embed(title="My Embed"), content="This content is stacked on top of a file", files=[discord.File("stacked.py")])
menu.add_page(discord.Embed(title="Hey Wumpos, can you say hi to the person reading this? π"))
menu.add_page(discord.Embed(title="Hi, I'm Wumpos!"), files=[discord.File("wumpos.gif")])
menu.add_button(ViewButton.back())
menu.add_button(ViewButton.next())
await menu.start()
```
![stacked_view](https://imgur.com/Nis9iPP.gif)
Since the `menu_type` is `TypeEmbed`, there always has to be an embed on each page. If the `menu_type` was `TypeText`, embeds aren't allowed and you will be restricted to only using the `files` parameter.
#### ViewMenu.TypeEmbedDynamic
A dynamic menu is used when you do not know how much information will be applied to the menu. For example, if you were to request information from a database, that information can always change. You query something and you might get 1,500 results back, and the next maybe only 800. A dynamic menu pieces all this information together for you and adds it to an embed page by rows of data. `ViewMenu.add_row()` is best used in some sort of `Iterable` where everything can be looped through, but only add the amount of data you want to the menu page.
> **NOTE:** In a dynamic menu, all added data is placed in the description section of an embed. If you choose to use a `custom_embed`, all text in the description will be overridden with the data you add
* Associated methods
* `ViewMenu.add_row(data: str)`
* `ViewMenu.clear_all_row_data()`
* `ViewMenu.set_main_pages(*embeds: Embed)`
* `ViewMenu.set_last_pages(*embeds: Embed)`
* The kwargs specifically made for a dynamic menu are:
* `rows_requested` - The amount of rows you would like on each embed page before making a new page
* `ViewMenu(..., rows_requested=5)`
* `custom_embed` - An embed you have created to use as the embed pages. Used for your menu aesthetic
* `ViewMenu(..., custom_embed=red_embed)`
* `wrap_in_codeblock` - The language identifier when wrapping your data in a discord codeblock.
* `ViewMenu(..., wrap_in_codeblock='py')`
##### Adding Rows/data
```py
menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbedDynamic, rows_requested=5)
for data in database.request('SELECT * FROM customers'):
menu.add_row(data)
```
##### Deleting Data
You can remove all the data you've added to a menu by using `menu.clear_all_row_data()`
##### Main/Last Pages
When using a dynamic menu, the only embed pages you see are from the data you've added. But if you would like to show more pages other than just the data, you can use methods `ViewMenu.set_main_pages()` and `ViewMenu.set_last_pages()`. Setting the main page(s), the embeds you set will be the first embeds that are shown when the menu starts. Setting the last page(s) are the last embeds shown
```py
menu.set_main_pages(welcome_embed, announcement_embed)
for data in get_information():
menu.add_row(data)
menu.set_last_pages(additional_info_embed)
# NOTE: setting main/last pages can be set in any order
```
### Buttons for ViewMenu
Buttons are what you use to interact with the menu. Unlike reactions, they look cleaner, provides less rate limit issues, and offer more in terms of interactions. Enable and disable buttons, use markdown hyperlinks in it's messages, and even send hidden messages.
![discord_buttons](https://discord.com/assets/7bb017ce52cfd6575e21c058feb3883b.png)
* Associated methods
* `ViewMenu.add_button(button: ViewButton)`
* `ViewMenu.disable_all_buttons()`
* `ViewMenu.disable_button(button: ViewButton)`
* `ViewMenu.enable_all_buttons()`
* `ViewMenu.enable_button(button: ViewButton)`
* `ViewMenu.get_button(identity: str, *, search_by='label')`
* `ViewMenu.remove_all_buttons()`
* `ViewMenu.remove_button(button: ViewButton)`
* `await ViewMenu.refresh_menu_items()`
#### ViewButton
```
class reactionmenu.ViewButton(*, style=discord.ButtonStyle.secondary, label=None, disabled=False, custom_id=None, url=None, emoji=None, followup=None, event=None, **kwargs)
```
A `ViewButton` is a class that represents the discord button. It is a subclass of `discord.ui.Button`.
The following are the rules set by Discord for Buttons:
* Link buttons don't send interactions to the Discord App, so link button statistics (it's properties) are not tracked
* Non-link buttons **must** have a `custom_id`, and cannot have a `url`
* Link buttons **must** have a `url`, and cannot have a `custom_id`
* There cannot be more than 25 buttons per message
---
#### Parameters of the ViewButton constructor
* `style` (`discord.ButtonStyle`) The button style
* `label` (`str`) The text on the button
* `custom_id` (`str`) An ID to determine what action that button should take. Available IDs:
* `ViewButton.ID_NEXT_PAGE`
* `ViewButton.ID_PREVIOUS_PAGE`
* `ViewButton.ID_GO_TO_FIRST_PAGE`
* `ViewButton.ID_GO_TO_LAST_PAGE`
* `ViewButton.ID_GO_TO_PAGE`
* `ViewButton.ID_END_SESSION`
* `ViewButton.ID_CALLER`
* `ViewButton.ID_SEND_MESSAGE`
* `ViewButton.ID_CUSTOM_EMBED`
* `ViewButton.ID_SKIP`
* `emoji` (`Union[str, discord.PartialEmoji]`) Emoji used for the button
* `ViewButton(..., emoji='π')`
* `ViewButton(..., emoji='<:miscTwitter:705423192818450453>')`
* `ViewButton(..., emoji='\U000027a1')`
* `ViewButton(..., emoji='\N{winking face}')`
* `url` (`str`) URL for a button with style `discord.ButtonStyle.link`
* `disabled` (`bool`) If the button should be disabled
* `followup` (`ViewButton.Followup`) The message sent after the button is pressed. Only available for buttons that have a `custom_id` of `ViewButton.ID_CALLER` or `ViewButton.ID_SEND_MESSAGE`. `ViewButton.Followup` is a class that has parameters similar to `discord.abc.Messageable.send()`, and is used to control if a message is ephemeral, contains a file, embed, tts, etc...
* `event` (`ViewButton.Event`) Set a button to be disabled or removed when it has been pressed a certain amount of times
#### Kwargs of the ViewButton constructor
| Name | Type | Default Value | Used for
|------|------|---------------|----------
| `name` | `str` | `None` | The name of the button
| `skip` | `ViewButton.Skip` | `None` | Set the action and the amount of pages to skip when using a `custom_id` of `ViewButton.ID_SKIP`. For example, setting the action to "+" and the amount 3. If you are on "Page 1/20", pressing that button will bring you to "Page 4/20"
| `persist` | `bool` | `False` | Prevents link buttons from being disabled/removed when the menu times out or is stopped so they can remain clickable
#### Attributes for ViewButton
| Property | Return Type | Info
|----------|-------------|------
| `clicked_by` | `Set[discord.Member]` | The members who clicked the button
| `total_clicks` | `int` | Amount of clicks from the button
| `last_clicked` | `Optional[datetime.datetime]` | The time in UTC for when the button was last clicked
| `menu` | `Optional[ViewMenu]` | The menu the button is attached to
#### Adding a ViewButton
```py
from reactionmenu import ViewMenu, ViewButton
menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed)
# Link button
link_button = ViewButton(style=discord.ButtonStyle.link, emoji='π', label='Link to Google', url='https://google.com')
menu.add_button(link_button)
# Skip button
skip = ViewButton(style=discord.ButtonStyle.primary, label='+5', custom_id=ViewButton.ID_SKIP, skip=ViewButton.Skip(action='+', amount=5))
menu.add_button(skip)
# ViewButton.ID_PREVIOUS_PAGE
back_button = ViewButton(style=discord.ButtonStyle.primary, label='Back', custom_id=ViewButton.ID_PREVIOUS_PAGE)
menu.add_button(back_button)
# ViewButton.ID_NEXT_PAGE
next_button = ViewButton(style=discord.ButtonStyle.secondary, label='Next', custom_id=ViewButton.ID_NEXT_PAGE)
menu.add_button(next_button)
# All other ViewButton are created the same way as the last 2 EXCEPT
# 1 - ViewButton.ID_CALLER
# 2 - ViewButton.ID_SEND_MESSAGE
# 3 - ViewButton.ID_CUSTOM_EMBED
# ViewButton.ID_CALLER
def say_hello(name: str):
print('Hello', name)
call_followup = ViewButton.Followup(details=ViewButton.Followup.set_caller_details(say_hello, 'John'))
menu.add_button(ViewButton(label='Say hi', custom_id=ViewButton.ID_CALLER, followup=call_followup))
# ViewButton.ID_SEND_MESSAGE
msg_followup = ViewButton.Followup('This message is hidden!', ephemeral=True)
menu.add_button(ViewButton(style=discord.ButtonStyle.green, label='Message', custom_id=ViewButton.ID_SEND_MESSAGE, followup=msg_followup))
# ViewButton.ID_CUSTOM_EMBED
custom_embed_button = ViewButton(style=discord.ButtonStyle.blurple, label='Social Media Info', custom_id=ViewButton.ID_CUSTOM_EMBED, followup=ViewButton.Followup(embed=discord.Embed(...)))
```
---
> **NOTE:** When it comes to buttons with a `custom_id` of `ViewButton.ID_CALLER`, `ViewButton.ID_SEND_MESSAGE`, `ViewButton.ID_CUSTOM_EMBED`, or link buttons, you can add as many as you'd like as long as in total it's 25 buttons or less. For all other button ID's, each menu can only have one.
### Using Selects
Selects are used when you'd like to categorize information in your menu. Selects can only be used when the menu's `menu_type` is `TypeEmbed`. You should keep in mind that discords limitations on how many menu UI items (rows) can be applied to each message.
![select_showcase](https://imgur.com/3X54KnY.gif)
* Associated Methods
* `Page.from_embeds(embeds: Sequence[Embed])`
* `ViewMenu.add_select(select: ViewSelect)`
* `ViewMenu.remove_select(select: ViewSelect)`
* `ViewMenu.remove_all_selects()`
* `ViewMenu.disable_select(select: ViewSelect)`
* `ViewMenu.disable_all_selects()`
* `ViewMenu.enable_select(select: ViewSelect)`
* `ViewMenu.enable_all_selects()`
* `ViewMenu.get_select(title: Union[str, None])`
Example:
```py
from reactionmenu import ViewMenu, ViewSelect, Page
menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed)
menu.add_page(discord.Embed(title="A showcase of console video games", color=discord.Color.blurple()))
menu.add_select(ViewSelect(title="Console Video Games", options={
# NOTE: The discord.SelectOption parameter "default" cannot be set to True
discord.SelectOption(label="PlayStation", emoji="<:PlayStation:549638412538478602>") : [
Page(embed=discord.Embed(title="Ratchet & Clank", description=..., color=discord.Color.yellow()).set_image(url=...)),
Page(embed=discord.Embed(title="God of War", description=..., color=discord.Color.blue()).set_image(url=...))
],
discord.SelectOption(label="Xbox", emoji="<:Xbox:501880493285834752>") : [
Page(embed=discord.Embed(title="Halo Infinite", description=..., color=discord.Color.green()).set_image(url=...)),
Page(embed=discord.Embed(title="Gears of War 4", description=..., color=discord.Color.red()).set_image(url=...))
]
}))
menu.add_button(ViewButton.back())
menu.add_button(ViewButton.next())
await menu.start()
```
#### Go to page navigation
You can use this type of select when you'd like to use the UI to select a page to go to.
![goto_showcase](https://imgur.com/rfHdOPX.gif)
* Associated methods
* `ViewMenu.add_go_to_select(goto: ViewSelect.GoTo)`
* `ViewMenu.enable_go_to_select(goto: ViewSelect.GoTo)`
* `ViewMenu.enable_all_go_to_selects()`
* `ViewMenu.disable_go_to_select(goto: ViewSelect.GoTo)`
* `ViewMenu.disable_all_go_to_selects()`
* `ViewMenu.remove_go_to_select(goto: ViewSelect.GoTo)`
* `ViewMenu.remove_all_go_to_selects()`
The `page_numbers` parameter for `ViewSelect.GoTo` can be used with 3 different types
1. `List[int]` If set to a list of integers, those specified values are the only options that are available when the select is clicked
1. `page_numbers=[1, 5, 10]`
2. `Dict[int, Union[str, discord.Emoji, discord.PartialEmoji]]` You can use this type if you'd like to utilize emojis in your select
1. `page_numbers={1 : "π―οΈ", 2 : "πΊ"}`
3. `ellipsis` You can set a *literal* ellipsis to have the library automatically assign all page numbers to the amount of pages that you've added to the menu. This can come in handy if you have 25 pages or less
1. `page_numbers=...`
> **NOTE**: Setting the `page_numbers` parameter to an ellipsis (...) only works as intended if you've added the go to select AFTER you've added pages to the menu
```py
@bot.command()
async def navigate(ctx):
menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed)
menu.add_page(discord.Embed(title="Twitter").set_image(url="..."))
menu.add_page(discord.Embed(title="YouTube").set_image(url="..."))
menu.add_page(discord.Embed(title="Discord").set_image(url="..."))
# ...
menu.add_go_to_select(ViewSelect.GoTo(title="Go to page...", page_numbers=...))
menu.add_button(ViewButton.back())
menu.add_button(ViewButton.next())
await menu.start()
```
### Updating ViewButton and Pages
* Associated methods
* `await ViewMenu.refresh_menu_items()`
* `await ViewMenu.update(*, new_pages: Union[List[Union[Embed, str]], None], new_buttons: Union[List[ViewButton], None])`
When the menu is running, you can update the pages or buttons on the menu. Using `ViewMenu.update()`, you can replace the pages and buttons. Using `ViewMenu.refresh_menu_items()` updates the buttons you have changed.
#### Updating a Button
```py
@bot.command()
async def menu(ctx):
menu = ViewMenu(..., name='test')
link_button = ViewButton(..., label='Link')
menu.add_button(link_button)
menu.add_page(...)
await menu.start()
@bot.command()
async def disable(ctx):
menu = ViewMenu.get_session('test')
link_button = menu[0].get_button('Link', search_by='label')
menu.disable_button(link_button)
await menu.refresh_menu_items()
```
If the buttons are not refreshed with `ViewMenu.refresh_menu_items()`, the menu will not be updated when changing a button.
#### Updating Pages and Buttons
Method `ViewMenu.update(...)` is used when you want to replace all or a few of the buttons on the menu.
```py
menu = ViewMenu(...)
# in a different .command()
await menu.update(new_pages=[hello_embed, goodbye_embed], new_buttons=[link_button, next_button])
```
> **NOTE**: When using `ViewMenu.update(...)`, there is no need to use `ViewMenu.refresh_menu_items()` because they are updated during the update call.
---
#### ViewButton Methods
The `ViewButton` class comes with a set factory methods (class methods) that returns a `ViewButton` with parameters set according to their `custom_id` (excluding link buttons).
* `ViewButton.link(label: str, url: str)`
* `style`: `discord.ButtonStyle.link`
* `label`: `