Repository: Cog-Creators/Red-DiscordBot

Issue:
A ConfirmView similar to SimpleView
### Type of feature request

API functionality, Other

### Description of the feature you're suggesting

So now that we already have a view for buttons for menus with `SimpleView`, would've been great to add something with confirmations as well, like `ConfirmView` to confirm the actions with buttons as well. right now they're only the normal reactions so an option to have them buttons as well would've been good!

### Anything else?

_No response_


Discussion:

Resolving patch:
diff --git a/redbot/core/utils/views.py b/redbot/core/utils/views.py
--- a/redbot/core/utils/views.py
+++ b/redbot/core/utils/views.py
@@ -11,7 +11,7 @@
 if TYPE_CHECKING:
     from redbot.core.commands import Context
 
-__all__ = ("SimpleMenu", "SetApiModal", "SetApiView")
+__all__ = ("SimpleMenu", "SetApiModal", "SetApiView", "ConfirmView")
 
 _ = Translator("UtilsViews", __file__)
 
@@ -431,3 +431,190 @@ async def auth_button(self, interaction: discord.Interaction, button: discord.Bu
         return await interaction.response.send_modal(
             SetApiModal(self.default_service, self.default_keys)
         )
+
+
+class ConfirmView(discord.ui.View):
+    """
+    A simple `discord.ui.View` used for confirming something.
+
+    Parameters
+    ----------
+    author: Optional[discord.abc.User]
+        The user who you want to be interacting with the confirmation.
+        If this is omitted anyone can click yes or no.
+    timeout: float
+        The timeout of the view in seconds. Defaults to ``180`` seconds.
+    disable_buttons: bool
+        Whether to disable the buttons instead of removing them from the message after the timeout.
+        Defaults to ``False``.
+
+    Examples
+    --------
+    Using the view::
+
+        view = ConfirmView(ctx.author)
+        # attach the message to the view after sending it.
+        # This way, the view will be automatically removed
+        # from the message after the timeout.
+        view.message = await ctx.send("Are you sure you about that?", view=view)
+        await view.wait()
+        if view.result:
+            await ctx.send("Okay I will do that.")
+        else:
+            await ctx.send("I will not be doing that then.")
+
+    Auto-disable the buttons after timeout if nothing is pressed::
+
+        view = ConfirmView(ctx.author, disable_buttons=True)
+        view.message = await ctx.send("Are you sure you about that?", view=view)
+        await view.wait()
+        if view.result:
+            await ctx.send("Okay I will do that.")
+        else:
+            await ctx.send("I will not be doing that then.")
+
+    Attributes
+    ----------
+    result: Optional[bool]
+        The result of the confirm view.
+    author: Optional[discord.abc.User]
+        The author of the message who is allowed to press the buttons.
+    message: Optional[discord.Message]
+        The message the confirm view is sent on. This can be set while
+        sending the message. This can also be left as ``None`` in which case
+        nothing will happen in `on_timeout()`, if the view is never interacted with.
+    disable_buttons: bool
+        Whether to disable the buttons isntead of removing them on timeout
+        (if the `message` attribute has been set on the view).
+    """
+
+    def __init__(
+        self,
+        author: Optional[discord.abc.User] = None,
+        *,
+        timeout: float = 180.0,
+        disable_buttons: bool = False,
+    ):
+        if timeout is None:
+            raise TypeError("This view should not be used as a persistent view.")
+        super().__init__(timeout=timeout)
+        self.result: Optional[bool] = None
+        self.author: Optional[discord.abc.User] = author
+        self.message: Optional[discord.Message] = None
+        self.disable_buttons = disable_buttons
+
+    async def on_timeout(self):
+        """
+        A callback that is called by the provided (default) callbacks for `confirm_button`
+        and `dismiss_button` as well as when a view’s timeout elapses without being
+        explicitly stopped.
+
+        The default implementation will either disable the buttons
+        when `disable_buttons` is ``True``, or remove the view from the message otherwise.
+
+        .. note::
+
+            This will not do anything if `message` is ``None``.
+        """
+        if self.message is None:
+            # we can't do anything here if message is none
+            return
+
+        if self.disable_buttons:
+            self.confirm_button.disabled = True
+            self.dismiss_button.disabled = True
+            await self.message.edit(view=self)
+        else:
+            await self.message.edit(view=None)
+
+    @discord.ui.button(label=_("Yes"), style=discord.ButtonStyle.green)
+    async def confirm_button(self, interaction: discord.Interaction, button: discord.ui.Button):
+        # Warning: The Sphinx documentation for this method/attribute does not use this docstring.
+        """
+        A `discord.ui.Button` to confirm the message.
+
+        The button's callback will set `result` to ``True``, defer the response,
+        and call `on_timeout()` to clean up the view.
+
+        Example
+        -------
+        Changing the style and label of this `discord.ui.Button`::
+
+            view = ConfirmView(ctx.author)
+            view.confirm_button.style = discord.ButtonStyle.red
+            view.confirm_button.label = "Delete"
+            view.dismiss_button.label = "Cancel"
+            view.message = await ctx.send(
+                "Are you sure you want to remove #very-important-channel?", view=view
+            )
+            await view.wait()
+            if view.result:
+                await ctx.send("Channel #very-important-channel deleted.")
+            else:
+                await ctx.send("Canceled.")
+        """
+        self.result = True
+        self.stop()
+        # respond to the interaction so the user does not see "interaction failed".
+        await interaction.response.defer()
+        # call `on_timeout` explicitly here since it's not called when `stop()` is called.
+        await self.on_timeout()
+
+    @discord.ui.button(label=_("No"), style=discord.ButtonStyle.secondary)
+    async def dismiss_button(self, interaction: discord.Interaction, button: discord.ui.Button):
+        # Warning: The Sphinx documentation for this method/attribute does not use this docstring.
+        """
+        A `discord.ui.Button` to dismiss the message.
+
+        The button's callback will set `result` to ``False``, defer the response,
+        and call `on_timeout()` to clean up the view.
+
+        Example
+        -------
+        Changing the style and label of this `discord.ui.Button`::
+
+            view = ConfirmView(ctx.author)
+            view.confirm_button.style = discord.ButtonStyle.red
+            view.confirm_button.label = "Delete"
+            view.dismiss_button.label = "Cancel"
+            view.message = await ctx.send(
+                "Are you sure you want to remove #very-important-channel?", view=view
+            )
+            await view.wait()
+            if view.result:
+                await ctx.send("Channel #very-important-channel deleted.")
+            else:
+                await ctx.send("Canceled.")
+        """
+        self.result = False
+        self.stop()
+        # respond to the interaction so the user does not see "interaction failed".
+        await interaction.response.defer()
+        # call `on_timeout` explicitly here since it's not called when `stop()` is called.
+        await self.on_timeout()
+
+    async def interaction_check(self, interaction: discord.Interaction):
+        """
+        A callback that is called when an interaction happens within the view
+        that checks whether the view should process item callbacks for the interaction.
+
+        The default implementation of this will assign value of `discord.Interaction.message`
+        to the `message` attribute and either:
+
+        - send an ephemeral failure message and return ``False``,
+          if `author` is set and isn't the same as the interaction user, or
+        - return ``True``
+
+        .. seealso::
+
+            The documentation of the callback in the base class:
+            :meth:`discord.ui.View.interaction_check()`
+        """
+        if self.message is None:
+            self.message = interaction.message
+        if self.author and interaction.user.id != self.author.id:
+            await interaction.response.send_message(
+                content=_("You are not authorized to interact with this."), ephemeral=True
+            )
+            return False
+        return True


Test patch: