Search code examples
discord.pynextcord

My view class is responding weirdly in dpy


i have a view class for confirmation which i am using in multiple commands but is behaving weirdly like sometimes when i use command buttons on response message are already disaled and sometimes when i do click/use button, even after that it calls on_timeout and edit my message to timed out.

heres my view class:

confirm = Button(label="Confirm", emoji=tick, style=discord.ButtonStyle.green, custom_id="confirm")


class ConfirmView(View):
    def __init__(self, ctx, timeout):
        self.ctx = ctx
        super().__init__(timeout=timeout)

    async def on_timeout(self) -> None:
        for i in self.children:
            i.disabled = True
        await self.message.edit(content="Timeout You took too long to respond!", view=None)

    async def interaction_check(self, interaction) -> bool:
        if interaction.user.id == self.ctx.author.id:
            return True
        await interaction.response.send_message("You cannot interact with this view.", ephemeral=True)
        return False

class Cancel(Button):
    def __init__(self):
        super().__init__(label="Cancel", emoji=cross, style=discord.ButtonStyle.red, custom_id="cancel")

    async def callback(self, interaction):
        await interaction.response.edit_message(content="Cancelled!", view=None)

cancel = Cancel()

heres my command examples:

@client.command()
async def my_command_1(ctx):
    
    async def callback(interaction):
        # do my stuff here

    view = ConfirmView(ctx=ctx, timeout=30)
    view.add_item(confirm)
    view.add_item(cancel)

    confirm.callback = callback
    view.message = await ctx.send("One", view=view)

@client.command()
async def my_command_2(ctx):
    
    async def callback(interaction):
        # do my stuff here

    view = ConfirmView(ctx=ctx, timeout=30)
    view.add_item(confirm)
    view.add_item(cancel)

    confirm.callback = callback
    view.message = await ctx.send("Two", view=view)

@client.command()
async def my_command_3(ctx):
    
    async def callback(interaction):
        # do my stuff here

    view = ConfirmView(ctx=ctx, timeout=30)
    view.add_item(confirm)
    view.add_item(cancel)

    confirm.callback = callback
    view.message = await ctx.send("Three", view=view)

Solution

  • There are two problems that you have mentioned; timing out after clicking the button and buttons being disabled.

    1. Timing out even after clicking the buttons

    This occurs because of how the view timeout works. From the documentation,

    The timeout in seconds from last interaction with the UI before no longer accepting input.

    A way to disable on_timeout event, is to stop listening to interaction events in the view all together, with the stop method. In your callbacks,

    async def callback(self, interaction):
        self.view.stop() # <-
        await interaction.response.edit_message(content="Cancelled!", view=None)
    

    2. Disabling after timeout

    The logic from your code seems a bit off. You are setting all the buttons to be disabled, but then edit the message to not have any buttons at all?

    The "acting weird" you're talking about is actually,

    Say you run command_1, and let it timeout. Then run command_2, the buttons will be disabled. But you would notice that they won't be disabled anymore after restart.

    To understand this further, we have to look into how object-oriented programming works.

    When you add these buttons with add_item, these buttons become part of the view's children. In the on_timeout, you set all the children to be disabled. This unexpectedly also causes the buttons cancel and confirm to retain the attribute disabled=True.

    So now, when you use the class again, the buttons will be disabled, until you restart the code, where everything becomes enabled again.

    You can see this by adding a print statement.

    @client.command()
    async def command_2(ctx):
        async def callback(interaction):
            ...
        print(cancel.disabled) # <- prints "True"
        view = ConfirmView(ctx=ctx, timeout=5)
        view.add_item(confirm)
        view.add_item(cancel)
    
        confirm.callback = callback
        view.message = await ctx.send("Two", view=view)
    

    Running command_1, letting it timeout, then running command_2, would show that the disabled is equal to True in the cancel variable itself.

    To avoid this, you can create a new instance of the buttons.

    @client.command()
    async def command_1(ctx):
        async def callback(interaction):
            ...
        view = ConfirmView(ctx=ctx, timeout=5)
        confirm = discord.ui.Button(label="Confirm",
                                    emoji="✅",
                                    style=discord.ButtonStyle.green,
                                    custom_id="confirm") #  Or use a function to create this
        cancel = Cancel()
        view.add_item(confirm)
        view.add_item(cancel)
    
        confirm.callback = callback
        view.message = await ctx.send("One", view=view)