Repository: Pycord-Development/pycord

Issue:
Paginators don't display emojis properly after defer
### Summary

If and only if you defer, paginators do not display custom emojis from servers the bot is not in on any pages after the first page.

### Reproduction Steps

Set up a basic paginator that will display custom emojis from servers that the bot is **NOT** in. Defer, and then send the paginator.

### Minimal Reproducible Code

```python
import discord
from discord.ext import pages
bot = discord.Bot()

@bot.slash_command()
async def test(ctx):
    await ctx.defer()
    # Emojis from a server the bot is not in
    paginator = pages.Paginator(["<:pycord:881477002991915048>", "<:LovePycord:881608951668564098>"])
    await paginator.respond(ctx.interaction)

bot.run("Token")
```


### Expected Results

The bot properly displays emojis.
Video of expected behavior after making the change in the additional context section:
https://user-images.githubusercontent.com/56235026/159435321-cda0b348-e45a-4833-bb98-08e43878d1a7.mp4

### Actual Results

The bot does not display emojis once it has switched pages at least once.
Video of broken behavior:
https://user-images.githubusercontent.com/56235026/159435094-6b74f4fa-0e2d-4511-abc0-6c92a491b1ee.mp4

### Intents

None

### System Information

- Python v3.10.1-final
- py-cord v2.0.0-beta
    - py-cord pkg_resources: v2.0.0b5
- aiohttp v3.8.1
- system info: Windows 10 10.0.19044

### Checklist

- [X] I have searched the open issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have removed my token from display, if visible.

### Additional Context

This appears to have been broken by #1088.

Changing:
```py
                # convert from WebhookMessage to Message reference to bypass 15min webhook token timeout
                msg = await msg.channel.fetch_message(msg.id)
            else:
                msg = await interaction.response.send_message(
                    content=page_content.content,
                    embeds=page_content.embeds,
                    view=self,
                    ephemeral=ephemeral,
                )
            if isinstance(msg, discord.WebhookMessage):
                self.message = await msg.channel.fetch_message(msg.id)
            elif isinstance(msg, discord.Message):
                self.message = msg
            elif isinstance(msg, discord.Interaction):
                self.message = await msg.original_message()
```
to
```py
            else:
                msg = await interaction.response.send_message(
                    content=page_content.content,
                    embeds=page_content.embeds,
                    view=self,
                    ephemeral=ephemeral,
                )
            if isinstance(msg, (discord.WebhookMessage, discord.Message)):
                self.message = msg
            elif isinstance(msg, discord.Interaction):
                self.message = await msg.original_message()
```
appears to fix the issue.


Discussion:
Will these changes cause #1083 to be an issue again?
No, any changes will take that into account. 

Resolving patch:
diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py
--- a/discord/ext/pages/pagination.py
+++ b/discord/ext/pages/pagination.py
@@ -88,6 +88,15 @@ def __init__(
         self.paginator = None
 
     async def callback(self, interaction: discord.Interaction):
+        """|coro|
+
+        The coroutine that is called when the navigation button is clicked.
+
+        Parameters
+        -----------
+        interaction: :class:`discord.Interaction`
+            The interaction created by clicking the navigation button.
+        """
         if self.button_type == "first":
             self.paginator.current_page = 0
         elif self.button_type == "prev":
@@ -102,7 +111,7 @@ async def callback(self, interaction: discord.Interaction):
                 self.paginator.current_page += 1
         elif self.button_type == "last":
             self.paginator.current_page = self.paginator.page_count
-        await self.paginator.goto_page(page_number=self.paginator.current_page)
+        await self.paginator.goto_page(page_number=self.paginator.current_page, interaction=interaction)
 
 
 class Page:
@@ -116,15 +125,34 @@ class Page:
         The content of the page. Corresponds to the :class:`discord.Message.content` attribute.
     embeds: Optional[List[Union[List[:class:`discord.Embed`], :class:`discord.Embed`]]]
         The embeds of the page. Corresponds to the :class:`discord.Message.embeds` attribute.
+    custom_view: Optional[:class:`discord.ui.View`]
+        The custom view shown when the page is visible. Overrides the `custom_view` attribute of the main paginator.
     """
 
     def __init__(
-        self, content: Optional[str] = None, embeds: Optional[List[Union[List[discord.Embed], discord.Embed]]] = None
+        self,
+        content: Optional[str] = None,
+        embeds: Optional[List[Union[List[discord.Embed], discord.Embed]]] = None,
+        custom_view: Optional[discord.ui.View] = None,
+        **kwargs,
     ):
         if content is None and embeds is None:
             raise discord.InvalidArgument("A page cannot have both content and embeds equal to None.")
         self._content = content
-        self._embeds = embeds
+        self._embeds = embeds or []
+        self._custom_view = custom_view
+
+    async def callback(self, interaction: Optional[discord.Interaction] = None):
+        """|coro|
+
+        The coroutine associated to a specific page. If `Paginator.page_action()` is used, this coroutine is called.
+
+        Parameters
+        ----------
+        interaction: Optional[:class:`discord.Interaction`]
+            The interaction associated with the callback, if any.
+        """
+        pass
 
     @property
     def content(self) -> Optional[str]:
@@ -146,6 +174,16 @@ def embeds(self, value: Optional[List[Union[List[discord.Embed], discord.Embed]]
         """Sets the embeds for the page."""
         self._embeds = value
 
+    @property
+    def custom_view(self) -> Optional[discord.ui.View]:
+        """Gets the custom view assigned to the page."""
+        return self._custom_view
+
+    @custom_view.setter
+    def custom_view(self, value: Optional[discord.ui.View]):
+        """Assigns a custom view to be shown when the page is displayed."""
+        self._custom_view = value
+
 
 class PageGroup:
     """Creates a group of pages which the user can switch between.
@@ -189,6 +227,9 @@ class PageGroup:
     custom_buttons: Optional[List[:class:`PaginatorButton`]]
         A list of PaginatorButtons to initialize the Paginator with.
         If ``use_default_buttons`` is ``True``, this parameter is ignored.
+    trigger_on_display: :class:`bool`
+        Whether to automatically trigger the callback associated with a `Page` whenever it is displayed.
+        Has no effect if no callback exists for a `Page`.
     """
 
     def __init__(
@@ -207,6 +248,7 @@ def __init__(
         custom_view: Optional[discord.ui.View] = None,
         timeout: Optional[float] = None,
         custom_buttons: Optional[List[PaginatorButton]] = None,
+        trigger_on_display: Optional[bool] = None,
     ):
         self.label = label
         self.description = description
@@ -222,6 +264,7 @@ def __init__(
         self.custom_view: discord.ui.View = custom_view
         self.timeout: float = timeout
         self.custom_buttons: List = custom_buttons
+        self.trigger_on_display = trigger_on_display
 
 
 class Paginator(discord.ui.View):
@@ -253,11 +296,15 @@ class Paginator(discord.ui.View):
         Whether to loop the pages when clicking prev/next while at the first/last page in the list.
     custom_view: Optional[:class:`discord.ui.View`]
         A custom view whose items are appended below the pagination components.
+        If the currently displayed page has a `custom_view` assigned, it will replace these view components when that page is displayed.
     timeout: Optional[:class:`float`]
         Timeout in seconds from last interaction with the paginator before no longer accepting input.
     custom_buttons: Optional[List[:class:`PaginatorButton`]]
         A list of PaginatorButtons to initialize the Paginator with.
         If ``use_default_buttons`` is ``True``, this parameter is ignored.
+    trigger_on_display: :class:`bool`
+        Whether to automatically trigger the callback associated with a `Page` whenever it is displayed.
+        Has no effect if no callback exists for a `Page`.
 
     Attributes
     ----------
@@ -292,6 +339,7 @@ def __init__(
         custom_view: Optional[discord.ui.View] = None,
         timeout: Optional[float] = 180.0,
         custom_buttons: Optional[List[PaginatorButton]] = None,
+        trigger_on_display: Optional[bool] = None,
     ) -> None:
         super().__init__(timeout=timeout)
         self.timeout: float = timeout
@@ -310,7 +358,7 @@ def __init__(
                 List[str], List[Page], List[Union[List[discord.Embed], discord.Embed]]
             ] = self.page_groups[0].pages
 
-        self.page_count = len(self.pages) - 1
+        self.page_count = max(len(self.pages), 0)
         self.buttons = {}
         self.custom_buttons: List = custom_buttons
         self.show_disabled = show_disabled
@@ -320,6 +368,7 @@ def __init__(
         self.default_button_row = default_button_row
         self.loop_pages = loop_pages
         self.custom_view: discord.ui.View = custom_view
+        self.trigger_on_display = trigger_on_display
         self.message: Union[discord.Message, discord.WebhookMessage, None] = None
 
         if self.custom_buttons and not self.use_default_buttons:
@@ -348,6 +397,7 @@ async def update(
         custom_view: Optional[discord.ui.View] = None,
         timeout: Optional[float] = None,
         custom_buttons: Optional[List[PaginatorButton]] = None,
+        trigger_on_display: Optional[bool] = None,
     ):
         """Updates the existing :class:`Paginator` instance with the provided options.
 
@@ -379,6 +429,9 @@ async def update(
         custom_buttons: Optional[List[:class:`PaginatorButton`]]
             A list of PaginatorButtons to initialize the Paginator with.
             If ``use_default_buttons`` is ``True``, this parameter is ignored.
+        trigger_on_display: :class:`bool`
+            Whether to automatically trigger the callback associated with a `Page` whenever it is displayed.
+            Has no effect if no callback exists for a `Page`.
         """
 
         # Update pages and reset current_page to 0 (default)
@@ -398,6 +451,7 @@ async def update(
         self.loop_pages = loop_pages if loop_pages is not None else self.loop_pages
         self.custom_view: discord.ui.View = None if custom_view is None else custom_view
         self.timeout: float = timeout if timeout is not None else self.timeout
+        self.trigger_on_display = trigger_on_display if trigger_on_display is not None else self.trigger_on_display
         if custom_buttons and not self.use_default_buttons:
             self.buttons = {}
             for button in custom_buttons:
@@ -470,7 +524,7 @@ async def cancel(
         else:
             await self.message.edit(view=self)
 
-    async def goto_page(self, page_number=0) -> discord.Message:
+    async def goto_page(self, page_number: int = 0, *, interaction: Optional[discord.Interaction] = None) -> None:
         """Updates the paginator message to show the specified page number.
 
         Parameters
@@ -482,6 +536,10 @@ async def goto_page(self, page_number=0) -> discord.Message:
 
                 Page numbers are zero-indexed when referenced internally, but appear as one-indexed when shown to the user.
 
+        interaction: Optional[:class:`discord.Interaction`]
+            The interaction to use when editing the message. If not provided, the message will be edited using the paginator's
+            stored :attr:`message` attribute instead.
+
         Returns
         -------
         :class:`~discord.Message`
@@ -495,11 +553,19 @@ async def goto_page(self, page_number=0) -> discord.Message:
         page = self.pages[page_number]
         page = self.get_page_content(page)
 
-        return await self.message.edit(
-            content=page.content,
-            embeds=page.embeds,
-            view=self,
-        )
+        if page.custom_view:
+            self.update_custom_view(page.custom_view)
+
+        if interaction:
+            await interaction.response.edit_message(content=page.content, embeds=page.embeds, view=self)
+        else:
+            await self.message.edit(
+                content=page.content,
+                embeds=page.embeds,
+                view=self,
+            )
+        if self.trigger_on_display:
+            await self.page_action(interaction=interaction)
 
     async def interaction_check(self, interaction: discord.Interaction) -> bool:
         if self.usercheck:
@@ -508,7 +574,7 @@ async def interaction_check(self, interaction: discord.Interaction) -> bool:
 
     def add_menu(self):
         """Adds the default :class:`PaginatorMenu` instance to the paginator."""
-        self.menu = PaginatorMenu(self.page_groups, placeholder=self.menu_placeholder)
+        self.menu = PaginatorMenu(self.page_groups, placeholder=self.menu_placeholder, custom_id="pages_group_menu")
         self.menu.paginator = self
         self.add_item(self.menu)
 
@@ -521,6 +587,7 @@ def add_default_buttons(self):
                 label="<<",
                 style=discord.ButtonStyle.blurple,
                 row=self.default_button_row,
+                custom_id="pages_first_button",
             ),
             PaginatorButton(
                 "prev",
@@ -528,12 +595,14 @@ def add_default_buttons(self):
                 style=discord.ButtonStyle.red,
                 loop_label="↪",
                 row=self.default_button_row,
+                custom_id="pages_prev_button",
             ),
             PaginatorButton(
                 "page_indicator",
                 style=discord.ButtonStyle.gray,
                 disabled=True,
                 row=self.default_button_row,
+                custom_id="pages_indicator_button",
             ),
             PaginatorButton(
                 "next",
@@ -541,12 +610,14 @@ def add_default_buttons(self):
                 style=discord.ButtonStyle.green,
                 loop_label="↩",
                 row=self.default_button_row,
+                custom_id="pages_next_button",
             ),
             PaginatorButton(
                 "last",
                 label=">>",
                 style=discord.ButtonStyle.blurple,
                 row=self.default_button_row,
+                custom_id="pages_last_button",
             ),
         ]
         for button in default_buttons:
@@ -640,13 +711,21 @@ def update_buttons(self) -> Dict:
         # We're done adding standard buttons and menus, so we can now add any specified custom view items below them
         # The bot developer should handle row assignments for their view before passing it to Paginator
         if self.custom_view:
-            for item in self.custom_view.children:
-                self.add_item(item)
+            self.update_custom_view(self.custom_view)
+
         return self.buttons
 
+    def update_custom_view(self, custom_view: discord.ui.View):
+        """Updates the custom view shown on the paginator."""
+        if isinstance(self.custom_view, discord.ui.View):
+            for item in self.custom_view.children:
+                self.remove_item(item)
+        for item in custom_view.children:
+            self.add_item(item)
+
     @staticmethod
     def get_page_content(page: Union[Page, str, discord.Embed, List[discord.Embed]]) -> Page:
-        """Returns the correct content type for a page based on its content."""
+        """Converts a page into a :class:`Page` object based on its content."""
         if isinstance(page, Page):
             return page
         elif isinstance(page, str):
@@ -659,6 +738,17 @@ def get_page_content(page: Union[Page, str, discord.Embed, List[discord.Embed]])
             else:
                 raise TypeError("All list items must be embeds.")
 
+    async def page_action(self, interaction: Optional[discord.Interaction] = None) -> None:
+        """Triggers the callback associated with the current page, if any.
+
+        Parameters
+        ----------
+        interaction: Optional[:class:`discord.Interaction`]
+            The interaction that was used to trigger the page action.
+        """
+        if self.get_page_content(self.pages[self.current_page]).callback:
+            await self.get_page_content(self.pages[self.current_page]).callback(interaction=interaction)
+
     async def send(
         self,
         ctx: Context,
@@ -719,6 +809,9 @@ async def send(
         page = self.pages[self.current_page]
         page_content = self.get_page_content(page)
 
+        if page_content.custom_view:
+            self.update_custom_view(page_content.custom_view)
+
         self.user = ctx.author
 
         if target:
@@ -790,6 +883,9 @@ async def respond(
         page: Union[Page, str, discord.Embed, List[discord.Embed]] = self.pages[self.current_page]
         page_content: Page = self.get_page_content(page)
 
+        if page_content.custom_view:
+            self.update_custom_view(page_content.custom_view)
+
         self.user = interaction.user
         if target:
             await interaction.response.send_message(target_message, ephemeral=ephemeral)


Test patch: