Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modals Improvements #2241

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 73 additions & 7 deletions src/Discord.Net.Interactions/Utilities/InteractionUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ Task HandleInteraction (SocketInteraction interaction)
/// A Task representing the asyncronous waiting operation with a <see cref="IDiscordInteraction"/> result,
/// the result is null if the process timed out before receiving a valid Interaction.
/// </returns>
public static Task<SocketInteraction> WaitForMessageComponentAsync(BaseSocketClient client, IUserMessage fromMessage, TimeSpan timeout,
public static async Task<SocketMessageComponent> WaitForMessageComponentAsync(BaseSocketClient client, IUserMessage fromMessage, TimeSpan timeout,
CancellationToken cancellationToken = default)
{
bool Predicate (SocketInteraction interaction) => interaction is SocketMessageComponent component &&
component.Message.Id == fromMessage.Id;

return WaitForInteractionAsync(client, timeout, Predicate, cancellationToken);
return await WaitForInteractionAsync(client, timeout, Predicate, cancellationToken) as SocketMessageComponent;
}

/// <summary>
Expand Down Expand Up @@ -100,14 +100,80 @@ public static async Task<bool> ConfirmAsync (BaseSocketClient client, IMessageCh

var prompt = await channel.SendMessageAsync(message, components: component).ConfigureAwait(false);

var response = await WaitForMessageComponentAsync(client, prompt, timeout, cancellationToken).ConfigureAwait(false) as SocketMessageComponent;

var response = await WaitForMessageComponentAsync(client, prompt, timeout, cancellationToken).ConfigureAwait(false);
await prompt.DeleteAsync().ConfigureAwait(false);

if (response != null && response.Data.CustomId == confirmId)
return true;
return response is not null && response.Data.CustomId == confirmId;
}

/// <summary>
/// Create a confirmation dialog and wait for user input asynchronously.
/// </summary>
/// <param name="interaction">Interaction to send the response/followup message to.</param>
/// <param name="timeout">Timeout duration of this operation.</param>
/// <param name="message">Optional custom prompt message.</param>
/// <param name="cancellationToken">Token for canceling the wait operation.</param>
/// <returns>
/// A Task representing the asyncronous waiting operation with a <see cref="bool"/> result,
/// the result is <see langword="false"/> if the user declined the prompt or didnt answer in time, <see langword="true"/> if the user confirmed the prompt.
/// </returns>
public static async Task<bool> ConfirmAsync(SocketInteraction interaction, TimeSpan timeout, string message = null, Action<MessageProperties> updateMessage = null,
CancellationToken cancellationToken = default)
{
message ??= "Would you like to continue?";
var confirmId = $"confirm";
var declineId = $"decline";

var component = new ComponentBuilder()
.WithButton("Confirm", confirmId, ButtonStyle.Success)
.WithButton("Cancel", declineId, ButtonStyle.Danger)
.Build();

IUserMessage prompt;

if (!interaction.HasResponded)
{
await interaction.RespondAsync(message, components: component, ephemeral: true);
prompt = await interaction.GetOriginalResponseAsync();
}
else
return false;
prompt = await interaction.FollowupAsync(message, components: component, ephemeral: true);

var response = await WaitForMessageComponentAsync(interaction.Discord, prompt, timeout, cancellationToken).ConfigureAwait(false);

if(updateMessage is not null)
await response.UpdateAsync(updateMessage);

return response is not null && response.Data.CustomId == confirmId;
}

/// <summary>
/// Responds to an interaction with a modal and asyncronously wait for the users response.
/// </summary>
/// <typeparam name="TModal">The type of <see cref="IModal"/> to respond with.</typeparam>
/// <param name="interaction">The interaction to respond to.</param>
/// <param name="timeout">Timeout duration of this operation.</param>
/// <param name="contextFactory">Delegate for creating <see cref="IInteractionContext"/>s to be passed on to the <see cref="ComponentTypeConverter"/>s.</param>
/// <param name="services">Service collection to be passed on to the <see cref="ComponentTypeConverter"/>s.</param>
/// <param name="cancellationToken">Token for canceling the wait operation.</param>
/// <returns>
/// A Task representing the asyncronous waiting operation with a <typeparamref name="TModal"/> result,
/// the result is <see langword="null"/>q if the process timed out before receiving a valid Interaction.
/// </returns>
public static async Task<TModal> SendModalAsync<TModal>(this SocketInteraction interaction, TimeSpan timeout,
Func<SocketModal, DiscordSocketClient, IInteractionContext> contextFactory, IServiceProvider services = null, CancellationToken cancellationToken = default)
where TModal : class, IModal
{
var customId = Guid.NewGuid().ToString();
await interaction.RespondWithModalAsync<TModal>(customId);
var response = await WaitForInteractionAsync(interaction.Discord, timeout, interaction =>
{
return interaction is SocketModal socketModal &&
socketModal.Data.CustomId == customId;
}, cancellationToken) as SocketModal;

var modal = await ModalUtils.CreateModalAsync<TModal>(contextFactory(response, response.Discord), services).ConfigureAwait(false);
return modal;
}
}
}
92 changes: 90 additions & 2 deletions src/Discord.Net.Interactions/Utilities/ModalUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,31 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.Interactions
{
internal static class ModalUtils
/// <summary>
/// General utility class regarding <see cref="IModal"/> implementations.
/// </summary>
public static class ModalUtils
{
private static readonly ConcurrentDictionary<Type, ModalInfo> _modalInfos = new();

/// <summary>
/// Get a collection of built <see cref="ModalInfo"/> object of cached <see cref="IModal"/> implementatios.
/// </summary>
public static IReadOnlyCollection<ModalInfo> Modals => _modalInfos.Values.ToReadOnlyCollection();

/// <summary>
/// Get or add a <see cref="ModalInfo"/> to the shared cache.
/// </summary>
/// <param name="type">Type of the <see cref="IModal"/> implementation.</param>
/// <param name="interactionService">Instance of <see cref="InteractionService"/> in use.</param>
/// <returns>
/// The built instance of <see cref="ModalInfo"/>.
/// </returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="type"/> isn't an implementation of <see cref="IModal"/>.</exception>
public static ModalInfo GetOrAdd(Type type, InteractionService interactionService)
{
if (!typeof(IModal).IsAssignableFrom(type))
Expand All @@ -19,9 +35,26 @@ public static ModalInfo GetOrAdd(Type type, InteractionService interactionServic
return _modalInfos.GetOrAdd(type, ModuleClassBuilder.BuildModalInfo(type, interactionService));
}

/// <summary>
/// Get or add a <see cref="ModalInfo"/> to the shared cache.
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="interactionService">Instance of <see cref="InteractionService"/> in use.</param>
/// <returns>
/// The built instance of <see cref="ModalInfo"/>.
/// </returns>
public static ModalInfo GetOrAdd<T>(InteractionService interactionService) where T : class, IModal
=> GetOrAdd(typeof(T), interactionService);

/// <summary>
/// Gets the <see cref="ModalInfo"/> associated with an <see cref="IModal"/> implementation.
/// </summary>
/// <param name="type">Type of the <see cref="IModal"/> implementation.</param>
/// <param name="modalInfo">The built instance of <see cref="ModalInfo"/>.</param>
/// <returns>
/// A bool representing whether the fetch operation was successful.
/// </returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="type"/> isn't an implementation of <see cref="IModal"/>.</exception>
public static bool TryGet(Type type, out ModalInfo modalInfo)
{
if (!typeof(IModal).IsAssignableFrom(type))
Expand All @@ -30,9 +63,26 @@ public static bool TryGet(Type type, out ModalInfo modalInfo)
return _modalInfos.TryGetValue(type, out modalInfo);
}

/// <summary>
/// Gets the <see cref="ModalInfo"/> associated with an <see cref="IModal"/> implementation.
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="modalInfo">The built instance of <see cref="ModalInfo"/>.</param>
/// <returns>
/// A bool representing whether the fetch operation was successful.
/// </returns>
public static bool TryGet<T>(out ModalInfo modalInfo) where T : class, IModal
=> TryGet(typeof(T), out modalInfo);

/// <summary>
/// Remove the <see cref="ModalInfo"/> entry from the cache associated with an <see cref="IModal"/> implementation.
/// </summary>
/// <param name="type">Type of the <see cref="IModal"/> implementation.</param>
/// <param name="modalInfo">The instance of the removed <see cref="ModalInfo"/> entry.</param>
/// <returns>
/// A bool representing whether the removal operation was successful.
/// </returns>
/// <exception cref="ArgumentException">Thrown when <paramref name="type"/> isn't an implementation of <see cref="IModal"/>.</exception>
public static bool TryRemove(Type type, out ModalInfo modalInfo)
{
if (!typeof(IModal).IsAssignableFrom(type))
Expand All @@ -41,11 +91,49 @@ public static bool TryRemove(Type type, out ModalInfo modalInfo)
return _modalInfos.TryRemove(type, out modalInfo);
}

/// <summary>
/// Remove the <see cref="ModalInfo"/> entry from the cache associated with an <see cref="IModal"/> implementation.
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="modalInfo">The instance of the removed <see cref="ModalInfo"/> entry.</param>
/// <returns>
/// A bool representing whether the removal operation was successful.
/// </returns>
public static bool TryRemove<T>(out ModalInfo modalInfo) where T : class, IModal
=> TryRemove(typeof(T), out modalInfo);

/// <summary>
/// Initialize an <see cref="IModal"/> implementation from a <see cref="IModalInteraction"/> based <see cref="IInteractionContext"/>.
/// </summary>
/// <typeparam name="TModal">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="context">Context of the <see cref="IModalInteraction"/>.</param>
/// <param name="services">Service provider to be passed on to the <see cref="ComponentTypeConverter"/>s.</param>
/// <returns>
/// A Task representing the asyncronous <see cref="IModal"/> initialization operation with a <typeparamref name="TModal"/> result,
/// the result is <see langword="null"/> if the process was unsuccessful.
/// </returns>
public static async Task<TModal> CreateModalAsync<TModal>(IInteractionContext context, IServiceProvider services = null)
where TModal : class, IModal
{
if (!TryGet<TModal>(out var modalInfo))
return null;

var result = await modalInfo.CreateModalAsync(context, services, true).ConfigureAwait(false);

if (!result.IsSuccess || result is not ParseResult parseResult)
return null;

return parseResult.Value as TModal;
}

/// <summary>
/// Clears the <see cref="ModalInfo"/> cache.
/// </summary>
public static void Clear() => _modalInfos.Clear();

public static int Count() => _modalInfos.Count;
/// <summary>
/// Gets the count <see cref="ModalInfo"/> entries in the cache.
/// </summary>
public static int Count => _modalInfos.Count;
}
}
1 change: 1 addition & 0 deletions src/Discord.Net.WebSocket/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
[assembly: InternalsVisibleTo("Discord.Net.Relay")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]
[assembly: InternalsVisibleTo("Discord.Net.Interactions")]