Repository: Pycord-Development/pycord

Issue:
Add a "files" arg to Paginator.send/respond for embed thumbnails 
### Summary

A  way to send paginated embeds with local thumbnails

### What is the feature request for?

discord.ext.pages

### The Problem

I am trying to send a paginated list of embeds with thumbnails I have stored locally but there isn't a "files" arg to send my thumbnails as an attachment.

### The Ideal Solution

Add a "files"  arg to Paginator.send/respond

### The Current Solution

_No response_

### Additional Context

_No response_


Discussion:
![image](https://user-images.githubusercontent.com/23067478/148290885-fc85e235-1e4e-421e-8156-708e90cade94.png)

would it be added here?

https://github.com/Pycord-Development/pycord/blob/master/discord/ext/pages/pagination.py
> ![image](https://user-images.githubusercontent.com/23067478/148290885-fc85e235-1e4e-421e-8156-708e90cade94.png)
> 
> would it be added here?
> 
> https://github.com/Pycord-Development/pycord/blob/master/discord/ext/pages/pagination.py

I would assume so.
`    async def send(self, ctx: Union[ApplicationContext, Context], files: List[discord.File], ephemeral: bool = False) -> Union[discord.Message, discord.WebhookMessage]:`

`        if isinstance(ctx, ApplicationContext):
            msg = await ctx.respond(
                content=page if isinstance(page, str) else None,
                embed=page if isinstance(page, discord.Embed) else None,
                view=self,
                files=files,
                ephemeral=ephemeral,
            )
            print(msg)
        else:
            msg = await ctx.send(
                content=page if isinstance(page, str) else None,
                embed=page if isinstance(page, discord.Embed) else None,
                files=files,
                view=self,
            )`


ok here's what I changed in pagination.py

It works...kinda.

if you do 

await paginator.send(ctx, files, ephemeral=False)  - files being a list of discord file objects.

Problem is hiding the other files that send - see below - it does work to change them on the buttons but they all show up there. Not sure what do from here

![image](https://user-images.githubusercontent.com/23067478/148329251-b6dd24ee-f4d2-4d9a-b188-a824b210e3bd.png)

Cool thanks!

I wonder if this is even possible now since you have to attach all at once and you can't send a file when you edit a message (or in this case change a page).
https://github.com/Pycord-Development/pycord/pull/712

This will get us closer to the desired outcome once implemented
Thanks so much!

So can your tweak be also be implemented in `respond` as well?
Planned to be added to the functionality for the `Page` object. 

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
@@ -126,6 +126,8 @@ 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.
+    files: Optional[List[:class:`discord.File`]]
+        A list of local files to be shown with the page.
     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.
     """
@@ -135,6 +137,7 @@ def __init__(
         content: Optional[str] = None,
         embeds: Optional[List[Union[List[discord.Embed], discord.Embed]]] = None,
         custom_view: Optional[discord.ui.View] = None,
+        files: Optional[List[discord.File]] = None,
         **kwargs,
     ):
         if content is None and embeds is None:
@@ -142,6 +145,7 @@ def __init__(
         self._content = content
         self._embeds = embeds or []
         self._custom_view = custom_view
+        self._files = files or []
 
     async def callback(self, interaction: Optional[discord.Interaction] = None):
         """|coro|
@@ -155,6 +159,19 @@ async def callback(self, interaction: Optional[discord.Interaction] = None):
         """
         pass
 
+    def update_files(self) -> Optional[List[discord.File]]:
+        """Updates the files associated with the page by re-uploading them.
+        Typically used when the page is changed."""
+        for file in self._files:
+            with open(file.fp.name, "rb") as fp:  # type: ignore
+                self._files[self._files.index(file)] = discord.File(
+                    fp,  # type: ignore
+                    filename=file.filename,
+                    description=file.description,
+                    spoiler=file.spoiler,
+                )
+        return self._files
+
     @property
     def content(self) -> Optional[str]:
         """Gets the content for the page."""
@@ -185,6 +202,16 @@ 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
 
+    @property
+    def files(self) -> Optional[List[discord.File]]:
+        """Gets the files associated with the page."""
+        return self._files
+
+    @files.setter
+    def files(self, value: Optional[List[discord.File]]):
+        """Sets the files associated with the page."""
+        self._files = value
+
 
 class PageGroup:
     """Creates a group of pages which the user can switch between.
@@ -472,7 +499,14 @@ async def on_timeout(self) -> None:
         if self.disable_on_timeout:
             for item in self.children:
                 item.disabled = True
-            await self.message.edit(view=self)
+            page = self.pages[self.current_page]
+            page = self.get_page_content(page)
+            files = page.update_files()
+            await self.message.edit(
+                view=self,
+                files=files or [],
+                attachments=[],
+            )
 
     async def disable(
         self,
@@ -561,12 +595,24 @@ async def goto_page(self, page_number: int = 0, *, interaction: Optional[discord
         if page.custom_view:
             self.update_custom_view(page.custom_view)
 
+        files = page.update_files()
+
         if interaction:
-            await interaction.response.edit_message(content=page.content, embeds=page.embeds, view=self)
+            await interaction.response.defer()  # needed to force webhook message edit route for files kwarg support
+            await interaction.followup.edit_message(
+                message_id=self.message.id,
+                content=page.content,
+                embeds=page.embeds,
+                attachments=[],
+                files=files or [],
+                view=self,
+            )
         else:
             await self.message.edit(
                 content=page.content,
                 embeds=page.embeds,
+                attachments=[],
+                files=files or [],
                 view=self,
             )
         if self.trigger_on_display:
@@ -729,16 +775,22 @@ def get_page_content(page: Union[Page, str, discord.Embed, List[discord.Embed]])
         if isinstance(page, Page):
             return page
         elif isinstance(page, str):
-            return Page(content=page, embeds=[])
+            return Page(content=page, embeds=[], files=[])
         elif isinstance(page, discord.Embed):
-            return Page(content=None, embeds=[page])
+            return Page(content=None, embeds=[page], files=[])
+        elif isinstance(page, discord.File):
+            return Page(content=None, embeds=[], files=[page])
         elif isinstance(page, List):
             if all(isinstance(x, discord.Embed) for x in page):
-                return Page(content=None, embeds=page)
+                return Page(content=None, embeds=page, files=[])
+            if all(isinstance(x, discord.File) for x in page):
+                return Page(content=None, embeds=[], files=page)
             else:
-                raise TypeError("All list items must be embeds.")
+                raise TypeError("All list items must be embeds or files.")
         else:
-            raise TypeError("Page content must be a Page object, string, an embed, or a list of embeds.")
+            raise TypeError(
+                "Page content must be a Page object, string, an embed, a list of embeds, a file, or a list of files."
+            )
 
     async def page_action(self, interaction: Optional[discord.Interaction] = None) -> None:
         """Triggers the callback associated with the current page, if any.
@@ -832,6 +884,7 @@ async def send(
         self.message = await ctx.send(
             content=page_content.content,
             embeds=page_content.embeds,
+            files=page_content.files,
             view=self,
             reference=reference,
             allowed_mentions=allowed_mentions,
@@ -891,10 +944,13 @@ async def edit(
             self.update_custom_view(page_content.custom_view)
 
         self.user = message.author
+
         try:
             self.message = await message.edit(
                 content=page_content.content,
                 embeds=page_content.embeds,
+                files=page_content.files,
+                attachments=[],
                 view=self,
                 suppress=suppress,
                 allowed_mentions=allowed_mentions,
@@ -965,12 +1021,14 @@ async def respond(
                 msg = await target.send(
                     content=page_content.content,
                     embeds=page_content.embeds,
+                    files=page_content.files,
                     view=self,
                 )
             elif interaction.response.is_done():
                 msg = await interaction.followup.send(
                     content=page_content.content,
                     embeds=page_content.embeds,
+                    files=page_content.files,
                     view=self,
                     ephemeral=ephemeral,
                 )
@@ -981,6 +1039,7 @@ async def respond(
                 msg = await interaction.response.send_message(
                     content=page_content.content,
                     embeds=page_content.embeds,
+                    files=page_content.files,
                     view=self,
                     ephemeral=ephemeral,
                 )
@@ -992,18 +1051,21 @@ async def respond(
                 msg = await ctx.send(
                     content=page_content.content,
                     embeds=page_content.embeds,
+                    files=page_content.files,
                     view=self,
                 )
             else:
                 msg = await ctx.respond(
                     content=page_content.content,
                     embeds=page_content.embeds,
+                    files=page_content.files,
                     view=self,
                 )
         if isinstance(msg, (discord.Message, discord.WebhookMessage)):
             self.message = msg
         elif isinstance(msg, discord.Interaction):
             self.message = await msg.original_message()
+
         return self.message
 
 


Test patch: