d4n3436 / Fergun.Interactive

An addon that provides interactive functionality to Discord commands.
MIT License
31 stars 5 forks source link

Add func to when a paginator is closed either by cancellation or timeout #13

Closed TheStachelfisch closed 2 years ago

TheStachelfisch commented 2 years ago

Description

It would be useful to have a way to pass a func into a LazyPaginatorBuilder and StaticPaginatorBuilder.

How is this gonna be useful?

This would be useful for doing certain actions when a paginator is closed. My use case for this would be checking if a user already has an open paginator or not.

Example implementation

var paginator = new LazyPaginatorBuilder()
    .AddUser(Context.User)
    .WithPageFactory(GeneratePages)
    .WithMaxPageIndex(10 - 1)

    .AddOption(new Emoji("⏪"), PaginatorAction.SkipToStart) // Use different emojis and option order.
    .AddOption(new Emoji("◀"), PaginatorAction.Backward)
    .AddOption(new Emoji("⏹"), PaginatorAction.Exit)
    .AddOption(new Emoji("▶"), PaginatorAction.Forward)
    .AddOption(new Emoji("⏩"), PaginatorAction.SkipToEnd)

    .WithCacheLoadedPages(true)
    .WithActionOnCancellation(ActionOnStop.DisableInput)
    .WithActionOnTimeout(ActionOnStop.DisableInput)
    // New implementation.
    .WithActionOnStopped(RemoveUserCooldown)
    .Build();

await Interactive.SendPaginatorAsync(paginator, Context.Interaction, TimeSpan.FromSeconds(30), resetTimeoutOnInput: true);

async Task RemoveUserCooldown(StopType type)
{
    ...
    return Task.CompletedTask;
}

StopType here indicates whether the paginator timed out or was cancelled

Alternative ways this can currently be done

InteractionModuleBase has a AfterExecute method that can be overridden, in that method you could then check for that exact command and take appropriate actions. This method is only called after the paginator has stopped.

d4n3436 commented 2 years ago

I feel that this way of handling the result of paginators (via delegates) is somewhat clumsy, a cleaner way to do this would be to simply check the result of SendPaginatorAsync:

var result = await Interactive.SendPaginatorAsync(...);

if (result.IsCanceled)
{
    // paginator stopped because it was canceled
}
else if (result.IsTimeout)
{
    // paginator stopped because of a timeout
}

If SendPaginatorAsync has to be discarded, then wrapping the code inside Task.Run also works:

_ = Task.Run(async () =>
{
    var result = await Interactive.SendPaginatorAsync(...);

    // post-execution logic
});

A way to hide this logic (and use delegates) is by creating an extension method that sends a paginator and accepts a delegate parameter:

public static class InteractiveServiceExtensions
{
    public static async Task SendPaginatorWithDelegateAsync(this InteractiveService interactiveService, Paginator paginator, SocketInteraction interaction,
        Func<InteractiveMessageResult, Task> resultAction, TimeSpan? timeout = null, InteractionResponseType responseType = InteractionResponseType.ChannelMessageWithSource,
        bool ephemeral = false, Action<IUserMessage> messageAction = null, bool resetTimeoutOnInput = false, CancellationToken cancellationToken = default)
    {
        var result = await interactiveService.SendPaginatorAsync(paginator, interaction, timeout, responseType, ephemeral, messageAction, resetTimeoutOnInput, cancellationToken);
        await resultAction(result);
    }
}