diff --git a/src/Discord.Net.Interactions/Attributes/HideAttribute.cs b/src/Discord.Net.Interactions/Attributes/HideAttribute.cs
new file mode 100644
index 0000000000..4dcec6f5c0
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/HideAttribute.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Discord.Interactions;
+
+///
+/// Enum values tagged with this attribute will not be displayed as a parameter choice
+///
+///
+/// This attribute must be used along with the default and .
+///
+[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
+public class HideAttribute : Attribute
+{
+ ///
+ /// Can be optionally implemented by inherited types to conditionally hide an enum value.
+ ///
+ ///
+ /// Only runs on prior to modal construction. For slash command parameters, this method is ignored.
+ ///
+ /// Interaction that is called on.
+ ///
+ /// if the attribute should be active and hide the value.
+ ///
+ public virtual bool Predicate(IDiscordInteraction interaction) => true;
+}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs
index fdeb8c4144..0e018f745f 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/InputLabelAttribute.cs
@@ -13,13 +13,20 @@ public class InputLabelAttribute : Attribute
///
public string Label { get; }
+ ///
+ /// Gets the label description of the input.
+ ///
+ public string Description { get; set; }
+
///
/// Creates a custom label for an modal input.
///
/// The label of the input.
- public InputLabelAttribute(string label)
+ /// The label description of the input.
+ public InputLabelAttribute(string label, string description = null)
{
Label = label;
+ Description = description;
}
}
}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs
new file mode 100644
index 0000000000..5e445f231e
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs
@@ -0,0 +1,16 @@
+namespace Discord.Interactions;
+
+///
+/// Marks a property as a channel select.
+///
+public class ModalChannelSelectAttribute : ModalSelectComponentAttribute
+{
+ ///
+ public override ComponentType ComponentType => ComponentType.ChannelSelect;
+
+ ///
+ /// Create a new .
+ ///
+ /// Custom ID of the channel select component.
+ public ModalChannelSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
+}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs
new file mode 100644
index 0000000000..7b6ce3314d
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Discord.Interactions;
+
+///
+/// Mark an property as a modal component field.
+///
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+public abstract class ModalComponentAttribute : Attribute
+{
+ ///
+ /// Gets the type of the component.
+ ///
+ public abstract ComponentType ComponentType { get; }
+
+ internal ModalComponentAttribute() { }
+}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs
new file mode 100644
index 0000000000..5271b64a24
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs
@@ -0,0 +1,32 @@
+namespace Discord.Interactions;
+
+///
+/// Marks a property as a file upload input.
+///
+public class ModalFileUploadAttribute : ModalInputAttribute
+{
+ ///
+ public override ComponentType ComponentType => ComponentType.FileUpload;
+
+ ///
+ /// Get the minimum number of files that can be uploaded.
+ ///
+ public int MinValues { get; set; } = 1;
+
+ ///
+ /// Get the maximum number of files that can be uploaded.
+ ///
+ public int MaxValues { get; set; } = 1;
+
+ ///
+ /// Create a new .
+ ///
+ /// Custom ID of the file upload component.
+ /// Minimum number of files that can be uploaded.
+ /// Maximum number of files that can be uploaded.
+ public ModalFileUploadAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId)
+ {
+ MinValues = minValues;
+ MaxValues = maxValues;
+ }
+}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs
index e9b877268a..5829b363f8 100644
--- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs
@@ -6,23 +6,18 @@ namespace Discord.Interactions
/// Mark an property as a modal input field.
///
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
- public abstract class ModalInputAttribute : Attribute
+ public abstract class ModalInputAttribute : ModalComponentAttribute
{
///
/// Gets the custom id of the text input.
///
public string CustomId { get; }
- ///
- /// Gets the type of the component.
- ///
- public abstract ComponentType ComponentType { get; }
-
///
/// Create a new .
///
/// The custom id of the input.
- protected ModalInputAttribute(string customId)
+ internal ModalInputAttribute(string customId)
{
CustomId = customId;
}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs
new file mode 100644
index 0000000000..57a0b1b991
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs
@@ -0,0 +1,18 @@
+namespace Discord.Interactions;
+
+///
+/// Marks a property as a mentionable select input.
+///
+public class ModalMentionableSelectAttribute : ModalSelectComponentAttribute
+{
+ ///
+ public override ComponentType ComponentType => ComponentType.MentionableSelect;
+
+ ///
+ /// Create a new .
+ ///
+ /// Custom ID of the mentionable select component.
+ /// Minimum number of values that can be selected.
+ /// Maximum number of values that can be selected
+ public ModalMentionableSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
+}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs
new file mode 100644
index 0000000000..3808be32c5
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs
@@ -0,0 +1,18 @@
+namespace Discord.Interactions;
+
+///
+/// Marks a property as a role select input.
+///
+public class ModalRoleSelectAttribute : ModalSelectComponentAttribute
+{
+ ///
+ public override ComponentType ComponentType => ComponentType.RoleSelect;
+
+ ///
+ /// Create a new .
+ ///
+ /// Custom ID of the role select component.
+ /// Minimum number of values that can be selected.
+ /// Maximum number of values that can be selected.
+ public ModalRoleSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
+}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs
new file mode 100644
index 0000000000..ccbc5891b8
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs
@@ -0,0 +1,28 @@
+namespace Discord.Interactions;
+
+///
+/// Base attribute for select-menu, user, channel, role, and mentionable select inputs in modals.
+///
+public abstract class ModalSelectComponentAttribute : ModalInputAttribute
+{
+ ///
+ /// Gets or sets the minimum number of values that can be selected.
+ ///
+ public int MinValues { get; set; } = 1;
+
+ ///
+ /// Gets or sets the maximum number of values that can be selected.
+ ///
+ public int MaxValues { get; set; } = 1;
+
+ ///
+ /// Gets or sets the placeholder text.
+ ///
+ public string Placeholder { get; set; }
+
+ internal ModalSelectComponentAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId)
+ {
+ MinValues = minValues;
+ MaxValues = maxValues;
+ }
+}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs
new file mode 100644
index 0000000000..5d70eb0c8b
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs
@@ -0,0 +1,18 @@
+namespace Discord.Interactions;
+
+///
+/// Marks a property as a select menu input.
+///
+public sealed class ModalSelectMenuAttribute : ModalSelectComponentAttribute
+{
+ ///
+ public override ComponentType ComponentType => ComponentType.SelectMenu;
+
+ ///
+ /// Create a new .
+ ///
+ /// Custom ID of the select menu component.
+ /// Minimum number of values that can be selected.
+ /// Maximum number of values that can be selected.
+ public ModalSelectMenuAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
+}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs
new file mode 100644
index 0000000000..2cf81fb659
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuOptionAttribute.cs
@@ -0,0 +1,58 @@
+using System;
+
+namespace Discord.Interactions;
+
+///
+/// Adds a select menu option to the marked field.
+///
+///
+/// To add additional metadata to enum fields, use instead.
+///
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
+public class ModalSelectMenuOptionAttribute : Attribute
+{
+ ///
+ /// Gets the label of the option.
+ ///
+ public string Label { get; }
+
+ ///
+ /// Gets or sets the description of the option.
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// Gets the value of the option.
+ ///
+ public string Value { get; }
+
+ ///
+ /// Gets or sets the emote of the option.
+ ///
+ ///
+ /// Can be either an or an
+ ///
+ public string Emote { get; set; }
+
+ ///
+ /// Gets or sets whether the option is selected by default.
+ ///
+ public bool IsDefault { get; set; }
+
+ ///
+ /// Create a new .
+ ///
+ /// Label of the option.
+ /// Value of the option.
+ /// Description of the option.
+ /// Emote of the option. Can be either an or an
+ /// Whether the option is selected by default
+ public ModalSelectMenuOptionAttribute(string label, string value, string description = null, string emote = null, bool isDefault = false)
+ {
+ Label = label;
+ Value = value;
+ Description = description;
+ Emote = emote;
+ IsDefault = isDefault;
+ }
+}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs
new file mode 100644
index 0000000000..300db32b24
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs
@@ -0,0 +1,24 @@
+namespace Discord.Interactions;
+
+///
+/// Marks a property as a text input.
+///
+public class ModalTextDisplayAttribute : ModalComponentAttribute
+{
+ ///
+ public override ComponentType ComponentType => ComponentType.TextDisplay;
+
+ ///
+ /// Gets the content of the text display.
+ ///
+ public string Content { get; }
+
+ ///
+ /// Create a new .
+ ///
+ /// Content of the text display.
+ public ModalTextDisplayAttribute(string content = null)
+ {
+ Content = content;
+ }
+}
diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs
new file mode 100644
index 0000000000..cc3cf28802
--- /dev/null
+++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs
@@ -0,0 +1,18 @@
+namespace Discord.Interactions;
+
+///
+/// Marks a property as a user select input.
+///
+public class ModalUserSelectAttribute : ModalSelectComponentAttribute
+{
+ ///
+ public override ComponentType ComponentType => ComponentType.UserSelect;
+
+ ///
+ /// Create a new .
+ ///
+ /// Custom ID of the user select component.
+ /// Minimum number of values that can be selected.
+ /// Maximum number of values that can be selected.
+ public ModalUserSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { }
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs
new file mode 100644
index 0000000000..0164245871
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Discord.Interactions.Builders;
+
+///
+/// Represents a builder for creating .
+///
+public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder
+{
+ protected override ChannelSelectComponentBuilder Instance => this;
+
+ ///
+ /// Initializes a new .
+ ///
+ /// Parent modal of this component.
+ public ChannelSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.ChannelSelect) { }
+
+ ///
+ /// Adds a default value to .
+ ///
+ /// The channel ID to add as a default value.
+ ///
+ /// The builder instance.
+ ///
+ public ChannelSelectComponentBuilder AddDefaulValue(ulong channelId)
+ {
+ _defaultValues.Add(new SelectMenuDefaultValue(channelId, SelectDefaultValueType.Channel));
+ return this;
+ }
+
+ ///
+ /// Adds default values to .
+ ///
+ /// The channels to add as a default value.
+ ///
+ /// The builder instance.
+ ///
+ public ChannelSelectComponentBuilder AddDefaultValues(params IEnumerable channels)
+ {
+ _defaultValues.AddRange(channels.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel)));
+ return this;
+ }
+
+ internal override ChannelSelectComponentInfo Build(ModalInfo modal)
+ => new(this, modal);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/FileUploadComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/FileUploadComponentBuilder.cs
new file mode 100644
index 0000000000..e004f34f5c
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/FileUploadComponentBuilder.cs
@@ -0,0 +1,54 @@
+namespace Discord.Interactions.Builders;
+
+///
+/// Represents a builder for creating .
+///
+public class FileUploadComponentBuilder : InputComponentBuilder
+{
+ protected override FileUploadComponentBuilder Instance => this;
+
+ ///
+ /// Gets and sets the minimum number of files that can be uploaded.
+ ///
+ public int MinValues { get; set; } = 1;
+
+ ///
+ /// Gets and sets the maximum number of files that can be uploaded.
+ ///
+ public int MaxValues { get; set; } = 1;
+
+ ///
+ /// Initializes a new .
+ ///
+ ///
+ public FileUploadComponentBuilder(ModalBuilder modal) : base(modal) { }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public FileUploadComponentBuilder WithMinValues(int minValues)
+ {
+ MinValues = minValues;
+ return this;
+ }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public FileUploadComponentBuilder WithMaxValues(int maxValues)
+ {
+ MaxValues = maxValues;
+ return this;
+ }
+
+ internal override FileUploadComponentInfo Build(ModalInfo modal)
+ => new (this, modal);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/IInputComponentBuilder.cs
new file mode 100644
index 0000000000..4ab2578390
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/IInputComponentBuilder.cs
@@ -0,0 +1,68 @@
+namespace Discord.Interactions.Builders
+{
+ ///
+ /// Represent a builder for creating .
+ ///
+ public interface IInputComponentBuilder : IModalComponentBuilder
+ {
+ ///
+ /// Gets the custom id of this input component.
+ ///
+ string CustomId { get; }
+
+ ///
+ /// Gets the label of this input component.
+ ///
+ string Label { get; }
+
+ ///
+ /// Gets the label description of this input component.
+ ///
+ string Description { get; }
+
+ ///
+ /// Gets whether this input component is required.
+ ///
+ bool IsRequired { get; }
+
+ ///
+ /// Get the assigned to this input.
+ ///
+ ModalComponentTypeConverter TypeConverter { get; }
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ IInputComponentBuilder WithCustomId(string customId);
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ IInputComponentBuilder WithLabel(string label);
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ IInputComponentBuilder WithDescription(string description);
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ IInputComponentBuilder SetIsRequired(bool isRequired);
+ }
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs
new file mode 100644
index 0000000000..9d869e4c57
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace Discord.Interactions.Builders;
+
+public interface IModalComponentBuilder
+{
+ ///
+ /// Gets the parent modal of this input component.
+ ///
+ ModalBuilder Modal { get; }
+
+ ///
+ /// Gets the component type of this input component.
+ ///
+ ComponentType ComponentType { get; }
+
+ ///
+ /// Get the reference type of this input component.
+ ///
+ Type Type { get; }
+
+ ///
+ /// Get the of this component's property.
+ ///
+ PropertyInfo PropertyInfo { get; }
+
+ ///
+ /// Gets the default value of this input component property.
+ ///
+ object DefaultValue { get; }
+
+ ///
+ /// Gets a collection of the attributes of this component.
+ ///
+ IReadOnlyCollection Attributes { get; }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ IModalComponentBuilder WithType(Type type);
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ IModalComponentBuilder SetDefaultValue(object value);
+
+ ///
+ /// Adds attributes to .
+ ///
+ /// New attributes to be added to .
+ ///
+ /// The builder instance.
+ ///
+ IModalComponentBuilder WithAttributes(params Attribute[] attributes);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/ISnowflakeSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ISnowflakeSelectComponentBuilder.cs
new file mode 100644
index 0000000000..f310e4bb71
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/ISnowflakeSelectComponentBuilder.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+
+namespace Discord.Interactions.Builders;
+
+///
+/// Represent a builder for creating .
+///
+public interface ISnowflakeSelectComponentBuilder : IInputComponentBuilder
+{
+ ///
+ /// Gets the minimum number of values that can be selected.
+ ///
+ int MinValues { get; }
+
+ ///
+ /// Gets the maximum number of values that can be selected.
+ ///
+ int MaxValues { get; }
+
+ ///
+ /// Gets the placeholder text for this select component.
+ ///
+ string Placeholder { get; set; }
+
+ ///
+ /// Gets the default value collection for this select component.
+ ///
+ IReadOnlyCollection DefaultValues { get; }
+
+ ///
+ /// Gets the default value type of this select component.
+ ///
+ SelectDefaultValueType? DefaultValuesType { get; }
+
+ ///
+ /// Adds a default value to the .
+ ///
+ /// Default value to be added.
+ /// The builder instance.
+ ISnowflakeSelectComponentBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue);
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the
+ /// The builder instance.
+ ISnowflakeSelectComponentBuilder WithMinValues(int minValues);
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the
+ /// The builder instance.
+ ISnowflakeSelectComponentBuilder WithMaxValues(int maxValues);
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the
+ /// The builder instance.
+ ISnowflakeSelectComponentBuilder WithPlaceholder(string placeholder);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/InputComponentBuilder.cs
similarity index 55%
rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs
rename to src/Discord.Net.Interactions/Builders/Modals/Components/InputComponentBuilder.cs
index af0ab3a70e..000df98ce5 100644
--- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/InputComponentBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/InputComponentBuilder.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Reflection;
namespace Discord.Interactions.Builders
{
@@ -9,15 +8,11 @@ namespace Discord.Interactions.Builders
///
/// The this builder yields when built.
/// Inherited type.
- public abstract class InputComponentBuilder : IInputComponentBuilder
+ public abstract class InputComponentBuilder : ModalComponentBuilder, IInputComponentBuilder
where TInfo : InputComponentInfo
where TBuilder : InputComponentBuilder
{
private readonly List _attributes;
- protected abstract TBuilder Instance { get; }
-
- ///
- public ModalBuilder Modal { get; }
///
public string CustomId { get; set; }
@@ -26,33 +21,20 @@ public abstract class InputComponentBuilder : IInputComponentBu
public string Label { get; set; }
///
- public bool IsRequired { get; set; } = true;
-
- ///
- public ComponentType ComponentType { get; internal set; }
-
- ///
- public Type Type { get; private set; }
-
- ///
- public PropertyInfo PropertyInfo { get; internal set; }
-
- ///
- public ComponentTypeConverter TypeConverter { get; private set; }
+ public string Description { get; set; }
///
- public object DefaultValue { get; set; }
+ public bool IsRequired { get; set; } = true;
///
- public IReadOnlyCollection Attributes => _attributes;
+ public ModalComponentTypeConverter TypeConverter { get; private set; }
///
/// Creates an instance of
///
/// Parent modal of this input component.
- public InputComponentBuilder(ModalBuilder modal)
+ internal InputComponentBuilder(ModalBuilder modal) : base(modal)
{
- Modal = modal;
_attributes = new();
}
@@ -83,28 +65,28 @@ public TBuilder WithLabel(string label)
}
///
- /// Sets .
+ /// Sets .
///
- /// New value of the .
+ /// New value of the .
///
/// The builder instance.
///
- public TBuilder SetIsRequired(bool isRequired)
+ public TBuilder WithDescription(string description)
{
- IsRequired = isRequired;
+ Description = description;
return Instance;
}
///
- /// Sets .
+ /// Sets .
///
- /// New value of the .
+ /// New value of the .
///
/// The builder instance.
///
- public TBuilder WithComponentType(ComponentType componentType)
+ public TBuilder SetIsRequired(bool isRequired)
{
- ComponentType = componentType;
+ IsRequired = isRequired;
return Instance;
}
@@ -115,56 +97,20 @@ public TBuilder WithComponentType(ComponentType componentType)
///
/// The builder instance.
///
- public TBuilder WithType(Type type)
- {
- Type = type;
- TypeConverter = Modal._interactionService.GetComponentTypeConverter(type);
- return Instance;
- }
-
- ///
- /// Sets .
- ///
- /// New value of the .
- ///
- /// The builder instance.
- ///
- public TBuilder SetDefaultValue(object value)
- {
- DefaultValue = value;
- return Instance;
- }
-
- ///
- /// Adds attributes to .
- ///
- /// New attributes to be added to .
- ///
- /// The builder instance.
- ///
- public TBuilder WithAttributes(params Attribute[] attributes)
+ public override TBuilder WithType(Type type)
{
- _attributes.AddRange(attributes);
- return Instance;
+ TypeConverter = Modal._interactionService.GetModalInputTypeConverter(type);
+ return base.WithType(type);
}
- internal abstract TInfo Build(ModalInfo modal);
-
- //IInputComponentBuilder
///
IInputComponentBuilder IInputComponentBuilder.WithCustomId(string customId) => WithCustomId(customId);
///
- IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithCustomId(label);
-
- ///
- IInputComponentBuilder IInputComponentBuilder.WithType(Type type) => WithType(type);
-
- ///
- IInputComponentBuilder IInputComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value);
+ IInputComponentBuilder IInputComponentBuilder.WithLabel(string label) => WithLabel(label);
///
- IInputComponentBuilder IInputComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
+ IInputComponentBuilder IInputComponentBuilder.WithDescription(string description) => WithDescription(description);
///
IInputComponentBuilder IInputComponentBuilder.SetIsRequired(bool isRequired) => SetIsRequired(isRequired);
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/MentionableSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/MentionableSelectComponentBuilder.cs
new file mode 100644
index 0000000000..86e1662063
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/MentionableSelectComponentBuilder.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Discord.Interactions.Builders;
+
+///
+/// Represents a builder for creating a .
+///
+public class MentionableSelectComponentBuilder : SnowflakeSelectComponentBuilder
+{
+ protected override MentionableSelectComponentBuilder Instance => this;
+
+ ///
+ /// Initialize a new .
+ ///
+ /// Parent modal of this input component.
+ public MentionableSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.MentionableSelect) { }
+
+ ///
+ /// Adds a snowflake ID as a default value to .
+ ///
+ /// The ID to add as a default value.
+ /// Enitity type of the snowflake ID.
+ ///
+ /// The builder instance.
+ ///
+ public MentionableSelectComponentBuilder AddDefaultValue(ulong id, SelectDefaultValueType type)
+ {
+ _defaultValues.Add(new SelectMenuDefaultValue(id, type));
+ return this;
+ }
+
+ ///
+ /// Add users as a default value to .
+ ///
+ /// The users to add as a default value.
+ ///
+ /// The builder instance.
+ ///
+ public MentionableSelectComponentBuilder AddDefaultValue(params IEnumerable users)
+ {
+ _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User)));
+ return this;
+ }
+
+ ///
+ /// Adds channels as a default value to .
+ ///
+ /// The channel to add as a default value.
+ ///
+ /// The builder instance.
+ ///
+ public MentionableSelectComponentBuilder AddDefaultValue(params IEnumerable channels)
+ {
+ _defaultValues.AddRange(channels.Select(x =>new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Channel)));
+ return this;
+ }
+
+ ///
+ /// Adds roles as a default value to .
+ ///
+ /// The role to add as a default value.
+ ///
+ /// The builder instance.
+ ///
+ public MentionableSelectComponentBuilder AddDefaulValue(params IEnumerable roles)
+ {
+ _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role)));
+ return this;
+ }
+
+ internal override MentionableSelectComponentInfo Build(ModalInfo modal)
+ => new(this, modal);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs
new file mode 100644
index 0000000000..f7c41b3f82
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace Discord.Interactions.Builders;
+
+public abstract class ModalComponentBuilder : IModalComponentBuilder
+ where TInfo : ModalComponentInfo
+ where TBuilder : ModalComponentBuilder
+{
+ private readonly List _attributes;
+ protected abstract TBuilder Instance { get; }
+
+ ///
+ public ModalBuilder Modal { get; }
+
+ ///
+ public ComponentType ComponentType { get; internal set; }
+
+ ///
+ public Type Type { get; private set; }
+
+ ///
+ public PropertyInfo PropertyInfo { get; internal set; }
+
+ ///
+ public object DefaultValue { get; set; }
+
+ ///
+ public IReadOnlyCollection Attributes => _attributes;
+
+ internal ModalComponentBuilder(ModalBuilder modal)
+ {
+ Modal = modal;
+ _attributes = new();
+ }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public virtual TBuilder WithComponentType(ComponentType componentType)
+ {
+ ComponentType = componentType;
+ return Instance;
+ }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public virtual TBuilder WithType(Type type)
+ {
+ Type = type;
+ return Instance;
+ }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public virtual TBuilder SetDefaultValue(object value)
+ {
+ DefaultValue = value;
+ return Instance;
+ }
+
+ ///
+ /// Adds attributes to .
+ ///
+ /// New attributes to be added to .
+ ///
+ /// The builder instance.
+ ///
+ public virtual TBuilder WithAttributes(params Attribute[] attributes)
+ {
+ _attributes.AddRange(attributes);
+ return Instance;
+ }
+
+ internal abstract TInfo Build(ModalInfo modal);
+
+ ///
+ IModalComponentBuilder IModalComponentBuilder.WithType(Type type) => WithType(type);
+
+ ///
+ IModalComponentBuilder IModalComponentBuilder.SetDefaultValue(object value) => SetDefaultValue(value);
+
+ ///
+ IModalComponentBuilder IModalComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/RoleSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/RoleSelectComponentBuilder.cs
new file mode 100644
index 0000000000..49e1a6dd0f
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/RoleSelectComponentBuilder.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Discord.Interactions.Builders;
+
+///
+/// Represents a builder for creating a .
+///
+public class RoleSelectComponentBuilder : SnowflakeSelectComponentBuilder
+{
+ protected override RoleSelectComponentBuilder Instance => this;
+
+ ///
+ /// Initialize a new .
+ ///
+ /// Parent modal of this input component.
+ public RoleSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.RoleSelect) { }
+
+ ///
+ /// Adds a default value to .
+ ///
+ /// The role ID to add as a default value.
+ ///
+ /// The builder instance.
+ ///
+ public RoleSelectComponentBuilder AddDefaulValue(ulong roleId)
+ {
+ _defaultValues.Add(new SelectMenuDefaultValue(roleId, SelectDefaultValueType.Role));
+ return this;
+ }
+
+ ///
+ /// Adds default values to .
+ ///
+ /// The roles to add as a default value.
+ ///
+ /// The builder instance.
+ ///
+ public RoleSelectComponentBuilder AddDefaultValues(params IEnumerable roles)
+ {
+ _defaultValues.AddRange(roles.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.Role)));
+ return this;
+ }
+
+ internal override RoleSelectComponentInfo Build(ModalInfo modal)
+ => new(this, modal);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/SelectMenuComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/SelectMenuComponentBuilder.cs
new file mode 100644
index 0000000000..1826dec756
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/SelectMenuComponentBuilder.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+
+namespace Discord.Interactions.Builders;
+
+///
+/// Represents a builder for creating .
+///
+public class SelectMenuComponentBuilder : InputComponentBuilder
+{
+ private readonly List _options;
+
+ protected override SelectMenuComponentBuilder Instance => this;
+
+ ///
+ /// Gets and sets the placeholder for the select menu iput.
+ ///
+ public string Placeholder { get; set; }
+
+ ///
+ /// Gets and sets the minimum number of values that can be selected.
+ ///
+ public int MinValues { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of values that can be selected.
+ ///
+ public int MaxValues { get; set; }
+
+ ///
+ /// Gets the options of this select menu component.
+ ///
+ public IReadOnlyCollection Options => _options;
+
+ ///
+ /// Initialize a new .
+ ///
+ /// Parent modal of this component.
+ public SelectMenuComponentBuilder(ModalBuilder modal) : base(modal)
+ {
+ _options = new();
+ }
+
+ ///
+ /// Adds an option to .
+ ///
+ /// Option to be added to .
+ /// The builder instance.
+ public SelectMenuComponentBuilder AddOption(SelectMenuOptionBuilder option)
+ {
+ _options.Add(option);
+ return this;
+ }
+
+ ///
+ /// Adds an option to .
+ ///
+ /// Select menu option builder factory.
+ /// The builder instance.
+ public SelectMenuComponentBuilder AddOption(Action configure)
+ {
+ var builder = new SelectMenuOptionBuilder();
+ configure(builder);
+ _options.Add(builder);
+ return this;
+ }
+
+ internal override SelectMenuComponentInfo Build(ModalInfo modal)
+ => new(this, modal);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/SnowflakeSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/SnowflakeSelectComponentBuilder.cs
new file mode 100644
index 0000000000..8de18ad051
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/SnowflakeSelectComponentBuilder.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+
+namespace Discord.Interactions.Builders;
+
+///
+/// Represents a builder for creating .
+///
+/// The this builder yields when built.
+/// Inherited type.
+public abstract class SnowflakeSelectComponentBuilder : InputComponentBuilder, ISnowflakeSelectComponentBuilder
+ where TInfo : InputComponentInfo
+ where TBuilder : InputComponentBuilder, ISnowflakeSelectComponentBuilder
+{
+ protected readonly List _defaultValues;
+
+ ///
+ public int MinValues { get; set; } = 1;
+
+ ///
+ public int MaxValues { get; set; } = 1;
+
+ ///
+ public string Placeholder { get; set; }
+
+ ///
+ public IReadOnlyCollection DefaultValues => _defaultValues.AsReadOnly();
+
+ ///
+ public SelectDefaultValueType? DefaultValuesType
+ {
+ get
+ {
+ return ComponentType switch
+ {
+ ComponentType.UserSelect => SelectDefaultValueType.User,
+ ComponentType.RoleSelect => SelectDefaultValueType.Role,
+ ComponentType.ChannelSelect => SelectDefaultValueType.Channel,
+ ComponentType.MentionableSelect => null,
+ _ => throw new InvalidOperationException("Component type must be a snowflake select type."),
+ };
+ }
+ }
+
+ ///
+ /// Initialize a new .
+ ///
+ /// Parent modal of this input component.
+ /// Type of this component.
+ public SnowflakeSelectComponentBuilder(ModalBuilder modal, ComponentType componentType) : base(modal)
+ {
+ ValidateComponentType(componentType);
+
+ ComponentType = componentType;
+ _defaultValues = new();
+ }
+
+ ///
+ public TBuilder AddDefaultValue(SelectMenuDefaultValue defaultValue)
+ {
+ if (DefaultValuesType.HasValue && defaultValue.Type != DefaultValuesType.Value)
+ throw new ArgumentException($"Only default values with {Enum.GetName(typeof(SelectDefaultValueType), DefaultValuesType.Value)} are support by {nameof(TInfo)} select type.", nameof(defaultValue));
+
+ _defaultValues.Add(defaultValue);
+ return Instance;
+ }
+
+ ///
+ public override TBuilder WithComponentType(ComponentType componentType)
+ {
+ ValidateComponentType(componentType);
+ return base.WithComponentType(componentType);
+ }
+
+ ///
+ public TBuilder WithMinValues(int minValues)
+ {
+ MinValues = minValues;
+ return Instance;
+ }
+
+ ///
+ public TBuilder WithMaxValues(int maxValues)
+ {
+ MaxValues = maxValues;
+ return Instance;
+ }
+
+ ///
+ public TBuilder WithPlaceholder(string placeholder)
+ {
+ Placeholder = placeholder;
+ return Instance;
+ }
+
+ private void ValidateComponentType(ComponentType componentType)
+ {
+ if (componentType is not (ComponentType.UserSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.ChannelSelect))
+ throw new ArgumentException("Component type must be a snowflake select type.", nameof(componentType));
+
+ }
+
+ ///
+ ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.AddDefaultValue(SelectMenuDefaultValue defaultValue) => AddDefaultValue(defaultValue);
+
+ ///
+ ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.WithMinValues(int minValues) => WithMinValues(minValues);
+
+ ///
+ ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.WithMaxValues(int maxValues) => WithMaxValues(maxValues);
+
+ ///
+ ISnowflakeSelectComponentBuilder ISnowflakeSelectComponentBuilder.WithPlaceholder(string placeholder) => WithPlaceholder(placeholder);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/TextDisplayComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/TextDisplayComponentBuilder.cs
new file mode 100644
index 0000000000..7d6d1e3107
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/TextDisplayComponentBuilder.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace Discord.Interactions.Builders;
+
+///
+/// Represents a builder for creating .
+///
+public class TextDisplayComponentBuilder : ModalComponentBuilder
+{
+ protected override TextDisplayComponentBuilder Instance => this;
+
+ ///
+ /// Gets and sets the content of the text display.
+ ///
+ public string Content { get; set; }
+
+ ///
+ /// Initialize a new .
+ ///
+ /// Parent modal of this input component.
+ public TextDisplayComponentBuilder(ModalBuilder modal) : base(modal)
+ {
+ }
+
+ ///
+ /// Sets .
+ ///
+ /// New value of the .
+ ///
+ /// The builder instance.
+ ///
+ public TextDisplayComponentBuilder WithContent(string content)
+ {
+ Content = content;
+ return this;
+ }
+
+ public override TextDisplayComponentBuilder WithType(Type type)
+ {
+ if(type != typeof(string))
+ {
+ throw new ArgumentException($"Text display components can be only used with {typeof(string).Name} properties. {type.Name} provided instead.");
+ }
+
+ return base.WithType(type);
+ }
+
+ internal override TextDisplayComponentInfo Build(ModalInfo modal)
+ => new(this, modal);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/TextInputComponentBuilder.cs
similarity index 100%
rename from src/Discord.Net.Interactions/Builders/Modals/Inputs/TextInputComponentBuilder.cs
rename to src/Discord.Net.Interactions/Builders/Modals/Components/TextInputComponentBuilder.cs
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/UserSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/UserSelectComponentBuilder.cs
new file mode 100644
index 0000000000..6189b1cfd4
--- /dev/null
+++ b/src/Discord.Net.Interactions/Builders/Modals/Components/UserSelectComponentBuilder.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Discord.Interactions.Builders;
+
+///
+/// Represents a builder for creating .
+///
+public class UserSelectComponentBuilder : SnowflakeSelectComponentBuilder
+{
+ protected override UserSelectComponentBuilder Instance => this;
+
+ ///
+ /// Initialize a new .
+ ///
+ /// Parent modal of this input component.
+ public UserSelectComponentBuilder(ModalBuilder modal) : base(modal, ComponentType.UserSelect) { }
+
+ ///
+ /// Adds a default value to .
+ ///
+ /// The user ID to add as a default value.
+ ///
+ /// The builder instance.
+ ///
+ public UserSelectComponentBuilder AddDefaulValue(ulong userId)
+ {
+ _defaultValues.Add(new SelectMenuDefaultValue(userId, SelectDefaultValueType.User));
+ return this;
+ }
+
+ ///
+ /// Adds default values to .
+ ///
+ /// The users to add as a default value.
+ ///
+ /// The builder instance.
+ ///
+ public UserSelectComponentBuilder AddDefaultValues(params IEnumerable users)
+ {
+ _defaultValues.AddRange(users.Select(x => new SelectMenuDefaultValue(x.Id, SelectDefaultValueType.User)));
+ return this;
+ }
+
+ internal override UserSelectComponentInfo Build(ModalInfo modal)
+ => new(this, modal);
+}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs
deleted file mode 100644
index 68c26fd037..0000000000
--- a/src/Discord.Net.Interactions/Builders/Modals/Inputs/IInputComponentBuilder.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-
-namespace Discord.Interactions.Builders
-{
- ///
- /// Represent a builder for creating .
- ///
- public interface IInputComponentBuilder
- {
- ///
- /// Gets the parent modal of this input component.
- ///
- ModalBuilder Modal { get; }
-
- ///
- /// Gets the custom id of this input component.
- ///
- string CustomId { get; }
-
- ///
- /// Gets the label of this input component.
- ///
- string Label { get; }
-
- ///
- /// Gets whether this input component is required.
- ///
- bool IsRequired { get; }
-
- ///
- /// Gets the component type of this input component.
- ///
- ComponentType ComponentType { get; }
-
- ///
- /// Get the reference type of this input component.
- ///
- Type Type { get; }
-
- ///
- /// Get the of this component's property.
- ///
- PropertyInfo PropertyInfo { get; }
-
- ///
- /// Get the assigned to this input.
- ///
- ComponentTypeConverter TypeConverter { get; }
-
- ///
- /// Gets the default value of this input component.
- ///
- object DefaultValue { get; }
-
- ///
- /// Gets a collection of the attributes of this component.
- ///
- IReadOnlyCollection Attributes { get; }
-
- ///
- /// Sets .
- ///
- /// New value of the .
- ///
- /// The builder instance.
- ///
- IInputComponentBuilder WithCustomId(string customId);
-
- ///
- /// Sets .
- ///
- /// New value of the .
- ///
- /// The builder instance.
- ///
- IInputComponentBuilder WithLabel(string label);
-
- ///
- /// Sets .
- ///
- /// New value of the .
- ///
- /// The builder instance.
- ///
- IInputComponentBuilder SetIsRequired(bool isRequired);
-
- ///
- /// Sets .
- ///
- /// New value of the .
- ///
- /// The builder instance.
- ///
- IInputComponentBuilder WithType(Type type);
-
- ///
- /// Sets .
- ///
- /// New value of the .
- ///
- /// The builder instance.
- ///
- IInputComponentBuilder SetDefaultValue(object value);
-
- ///
- /// Adds attributes to .
- ///
- /// New attributes to be added to .
- ///
- /// The builder instance.
- ///
- IInputComponentBuilder WithAttributes(params Attribute[] attributes);
- }
-}
diff --git a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
index 66aeadf75b..419ceadb36 100644
--- a/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/Modals/ModalBuilder.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
namespace Discord.Interactions.Builders
{
@@ -10,7 +9,7 @@ namespace Discord.Interactions.Builders
public class ModalBuilder
{
internal readonly InteractionService _interactionService;
- internal readonly List _components;
+ internal readonly List _components;
///
/// Gets the initialization delegate for this modal.
@@ -30,7 +29,7 @@ public class ModalBuilder
///
/// Gets a collection of the components of this modal.
///
- public IReadOnlyCollection Components => _components;
+ public IReadOnlyCollection Components => _components.AsReadOnly();
internal ModalBuilder(Type type, InteractionService interactionService)
{
@@ -72,7 +71,7 @@ public ModalBuilder WithTitle(string title)
///
/// The builder instance.
///
- public ModalBuilder AddTextComponent(Action configure)
+ public ModalBuilder AddTextInputComponent(Action configure)
{
var builder = new TextInputComponentBuilder(this);
configure(builder);
@@ -80,6 +79,111 @@ public ModalBuilder AddTextComponent(Action configure
return this;
}
+ ///
+ /// Adds a select menu component to .
+ ///
+ /// Select menu component builder factory.
+ ///
+ /// The builder instance.
+ ///
+ public ModalBuilder AddSelectMenuInputComponent(Action configure)
+ {
+ var builder = new SelectMenuComponentBuilder(this);
+ configure(builder);
+ _components.Add(builder);
+ return this;
+ }
+
+ ///
+ /// Adds a user select component to .
+ ///
+ /// User select component builder factory.
+ ///
+ /// The builder instance.
+ ///
+ public ModalBuilder AddUserSelectInputComponent(Action configure)
+ {
+ var builder = new UserSelectComponentBuilder(this);
+ configure(builder);
+ _components.Add(builder);
+ return this;
+ }
+
+ ///
+ /// Adds a role select component to .
+ ///
+ /// Role select component builder factory.
+ ///
+ /// The builder instance.
+ ///
+ public ModalBuilder AddRoleSelectInputComponent(Action configure)
+ {
+ var builder = new RoleSelectComponentBuilder(this);
+ configure(builder);
+ _components.Add(builder);
+ return this;
+ }
+
+ ///
+ /// Adds a mentionable select component to .
+ ///
+ /// Mentionable select component builder factory.
+ ///
+ /// The builder instance.
+ ///
+ public ModalBuilder AddMentionableSelectInputComponent(Action configure)
+ {
+ var builder = new MentionableSelectComponentBuilder(this);
+ configure(builder);
+ _components.Add(builder);
+ return this;
+ }
+
+ ///
+ /// Adds a channel select component to .
+ ///
+ /// Channel select component builder factory.
+ ///
+ /// The builder instance.
+ ///
+ public ModalBuilder AddChannelSelectInputComponent(Action configure)
+ {
+ var builder = new ChannelSelectComponentBuilder(this);
+ configure(builder);
+ _components.Add(builder);
+ return this;
+ }
+
+ ///
+ /// Adds a file upload component to .
+ ///
+ /// File upload component builder factory.
+ ///
+ /// The builder instance.
+ ///
+ public ModalBuilder AddFileUploadInputComponent(Action configure)
+ {
+ var builder = new FileUploadComponentBuilder(this);
+ configure(builder);
+ _components.Add(builder);
+ return this;
+ }
+
+ ///
+ /// Adds a text display component to .
+ ///
+ /// Text display component builder factory.
+ ///
+ /// The builder instance.
+ ///
+ public ModalBuilder AddTextDisplayComponent(Action configure)
+ {
+ var builder = new TextDisplayComponentBuilder(this);
+ configure(builder);
+ _components.Add(builder);
+ return this;
+ }
+
internal ModalInfo Build() => new(this);
}
}
diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
index 87aefa8f24..c76552ef9c 100644
--- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
+++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
@@ -95,7 +94,7 @@ private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, Intera
#pragma warning restore CS0618 // Type or member is obsolete
#pragma warning disable CS0618 // Type or member is obsolete
case EnabledInDmAttribute enabledInDm:
- {
+ {
builder.IsEnabledInDm = enabledInDm.IsEnabled;
}
break;
@@ -604,16 +603,37 @@ public static ModalInfo BuildModalInfo(Type modalType, InteractionService intera
Title = instance.Title
};
- var inputs = modalType.GetProperties().Where(IsValidModalInputDefinition);
+ var components = modalType.GetProperties().Where(IsValidModalComponentDefinition);
- foreach (var prop in inputs)
+ foreach (var prop in components)
{
- var componentType = prop.GetCustomAttribute()?.ComponentType;
+ var componentType = prop.GetCustomAttribute()?.ComponentType;
switch (componentType)
{
case ComponentType.TextInput:
- builder.AddTextComponent(x => BuildTextInput(x, prop, prop.GetValue(instance)));
+ builder.AddTextInputComponent(x => BuildTextInputComponent(x, prop, prop.GetValue(instance)));
+ break;
+ case ComponentType.SelectMenu:
+ builder.AddSelectMenuInputComponent(x => BuildSelectMenuComponent(x, prop, prop.GetValue(instance)));
+ break;
+ case ComponentType.UserSelect:
+ builder.AddUserSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance)));
+ break;
+ case ComponentType.RoleSelect:
+ builder.AddRoleSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance)));
+ break;
+ case ComponentType.MentionableSelect:
+ builder.AddMentionableSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance)));
+ break;
+ case ComponentType.ChannelSelect:
+ builder.AddChannelSelectInputComponent(x => BuildSnowflakeSelectComponent(x, prop, prop.GetValue(instance)));
+ break;
+ case ComponentType.FileUpload:
+ builder.AddFileUploadInputComponent(x => BuildFileUploadComponent(x, prop, prop.GetValue(instance)));
+ break;
+ case ComponentType.TextDisplay:
+ builder.AddTextDisplayComponent(x => BuildTextDisplayComponent(x, prop, prop.GetValue(instance)));
break;
case null:
throw new InvalidOperationException($"{prop.Name} of {prop.DeclaringType.Name} isn't a valid modal input field.");
@@ -632,7 +652,7 @@ public static ModalInfo BuildModalInfo(Type modalType, InteractionService intera
}
}
- private static void BuildTextInput(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
+ private static void BuildTextInputComponent(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
{
var attributes = propertyInfo.GetCustomAttributes();
@@ -659,6 +679,149 @@ private static void BuildTextInput(TextInputComponentBuilder builder, PropertyIn
break;
case InputLabelAttribute inputLabel:
builder.Label = inputLabel.Label;
+ builder.Description = inputLabel.Description;
+ break;
+ default:
+ builder.WithAttributes(attribute);
+ break;
+ }
+ }
+ }
+
+ private static void BuildSelectMenuComponent(SelectMenuComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
+ {
+ var attributes = propertyInfo.GetCustomAttributes();
+
+ builder.Label = propertyInfo.Name;
+ builder.DefaultValue = defaultValue;
+ builder.WithType(propertyInfo.PropertyType);
+ builder.PropertyInfo = propertyInfo;
+
+ foreach (var attribute in attributes)
+ {
+ switch (attribute)
+ {
+ case ModalSelectMenuAttribute selectMenuInput:
+ builder.CustomId = selectMenuInput.CustomId;
+ builder.ComponentType = selectMenuInput.ComponentType;
+ builder.MinValues = selectMenuInput.MinValues;
+ builder.MaxValues = selectMenuInput.MaxValues;
+ builder.Placeholder = selectMenuInput.Placeholder;
+ break;
+ case RequiredInputAttribute requiredInput:
+ builder.IsRequired = requiredInput.IsRequired;
+ break;
+ case InputLabelAttribute inputLabel:
+ builder.Label = inputLabel.Label;
+ builder.Description = inputLabel.Description;
+ break;
+ case ModalSelectMenuOptionAttribute selectMenuOption:
+ Emoji emoji = null;
+ Emote emote = null;
+
+ if (!string.IsNullOrEmpty(selectMenuOption?.Emote) && !(Emote.TryParse(selectMenuOption.Emote, out emote) || Emoji.TryParse(selectMenuOption.Emote, out emoji)))
+ throw new ArgumentException($"Unable to parse {selectMenuOption.Emote} of {propertyInfo.DeclaringType}.{propertyInfo.Name} into an {typeof(Emote).Name} or an {typeof(Emoji).Name}");
+
+ builder.AddOption(new SelectMenuOptionBuilder
+ {
+ Label = selectMenuOption.Label,
+ Description = selectMenuOption.Description,
+ Value = selectMenuOption.Value,
+ Emote = emote != null ? emote : emoji,
+ IsDefault = selectMenuOption.IsDefault
+ });
+ break;
+ default:
+ builder.WithAttributes(attribute);
+ break;
+ }
+ }
+ }
+
+ private static void BuildSnowflakeSelectComponent(SnowflakeSelectComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
+ where TInfo : SnowflakeSelectComponentInfo
+ where TBuilder : SnowflakeSelectComponentBuilder
+ {
+ var attributes = propertyInfo.GetCustomAttributes();
+
+ builder.Label = propertyInfo.Name;
+ builder.DefaultValue = defaultValue;
+ builder.WithType(propertyInfo.PropertyType);
+ builder.PropertyInfo = propertyInfo;
+
+ foreach (var attribute in attributes)
+ {
+ switch (attribute)
+ {
+ case ModalSelectComponentAttribute selectInput:
+ builder.CustomId = selectInput.CustomId;
+ builder.ComponentType = selectInput.ComponentType;
+ builder.MinValues = selectInput.MinValues;
+ builder.MaxValues = selectInput.MaxValues;
+ builder.Placeholder = selectInput.Placeholder;
+ break;
+ case RequiredInputAttribute requiredInput:
+ builder.IsRequired = requiredInput.IsRequired;
+ break;
+ case InputLabelAttribute inputLabel:
+ builder.Label = inputLabel.Label;
+ builder.Description = inputLabel.Description;
+ break;
+ default:
+ builder.WithAttributes(attribute);
+ break;
+ }
+ }
+ }
+
+ private static void BuildFileUploadComponent(FileUploadComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
+ {
+ var attributes = propertyInfo.GetCustomAttributes();
+
+ builder.Label = propertyInfo.Name;
+ builder.DefaultValue = defaultValue;
+ builder.WithType(propertyInfo.PropertyType);
+ builder.PropertyInfo = propertyInfo;
+
+ foreach (var attribute in attributes)
+ {
+ switch (attribute)
+ {
+ case ModalFileUploadAttribute fileUploadInput:
+ builder.CustomId = fileUploadInput.CustomId;
+ builder.ComponentType = fileUploadInput.ComponentType;
+ builder.MinValues = fileUploadInput.MinValues;
+ builder.MaxValues = fileUploadInput.MaxValues;
+ break;
+ case RequiredInputAttribute requiredInput:
+ builder.IsRequired = requiredInput.IsRequired;
+ break;
+ case InputLabelAttribute inputLabel:
+ builder.Label = inputLabel.Label;
+ builder.Description = inputLabel.Description;
+ break;
+ default:
+ builder.WithAttributes(attribute);
+ break;
+ }
+ }
+ }
+
+ private static void BuildTextDisplayComponent(TextDisplayComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue)
+ {
+ var attributes = propertyInfo.GetCustomAttributes();
+
+ builder.DefaultValue = defaultValue;
+ builder.WithType(propertyInfo.PropertyType);
+ builder.PropertyInfo = propertyInfo;
+
+ foreach (var attribute in attributes)
+ {
+ switch (attribute)
+ {
+ case ModalTextDisplayAttribute textDisplay:
+ builder.ComponentType = textDisplay.ComponentType;
+ builder.Content = textDisplay.Content;
break;
default:
builder.WithAttributes(attribute);
@@ -717,11 +880,11 @@ private static bool IsValidModalCommandDefinition(MethodInfo methodInfo)
typeof(IModal).IsAssignableFrom(methodInfo.GetParameters().Last().ParameterType);
}
- private static bool IsValidModalInputDefinition(PropertyInfo propertyInfo)
+ private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo)
{
return propertyInfo.SetMethod?.IsPublic == true &&
propertyInfo.SetMethod?.IsStatic == false &&
- propertyInfo.IsDefined(typeof(ModalInputAttribute));
+ propertyInfo.IsDefined(typeof(ModalComponentAttribute));
}
private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter)
diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
index c244640b45..5d2a0af335 100644
--- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
+++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using System.Threading.Tasks;
namespace Discord.Interactions
@@ -20,7 +21,7 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction
if (!ModalUtils.TryGet(out var modalInfo))
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");
- return SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal);
+ return SendModalResponseAsync(interaction, customId, modalInfo, null, options, modifyModal);
}
///
@@ -43,7 +44,7 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction
{
var modalInfo = ModalUtils.GetOrAdd(interactionService);
- return SendModalResponseAsync(interaction, customId, modalInfo, options, modifyModal);
+ return SendModalResponseAsync(interaction, customId, modalInfo, null, options, modifyModal);
}
///
@@ -64,20 +65,78 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction
if (!ModalUtils.TryGet(out var modalInfo))
throw new ArgumentException($"{typeof(T).FullName} isn't referenced by any registered Modal Interaction Command and doesn't have a cached {typeof(ModalInfo)}");
- var builder = new ModalBuilder(modal.Title, customId);
+ return SendModalResponseAsync(interaction, customId, modalInfo, modal, options, modifyModal);
+ }
+
+ private static async Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, T modalInstance = null, RequestOptions options = null, Action modifyModal = null)
+ where T : class, IModal
+ {
+ if (!modalInfo.Type.IsAssignableFrom(typeof(T)))
+ throw new ArgumentException($"{modalInfo.Type.FullName} isn't assignable from {typeof(T).FullName}.");
+
+ var builder = new ModalBuilder(modalInstance.Title, customId);
foreach (var input in modalInfo.Components)
switch (input)
{
case TextInputComponentInfo textComponent:
{
- var boxedValue = textComponent.Getter(modal);
- var value = textComponent.TypeOverridesToString
- ? boxedValue?.ToString()
- : boxedValue as string;
+ var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
+ textComponent.MaxLength, textComponent.IsRequired);
+
+ if (modalInstance != null)
+ {
+ await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, textComponent.Getter(modalInstance));
+ }
+
+ var labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description);
+ builder.AddLabel(labelBuilder);
+ }
+ break;
+ case SelectMenuComponentInfo selectMenuComponent:
+ {
+ var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false, isRequired: selectMenuComponent.IsRequired);
+
+ if (modalInstance != null)
+ {
+ await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, selectMenuComponent.Getter(modalInstance));
+ }
- builder.AddTextInput(textComponent.Label, textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null,
- textComponent.MaxLength, textComponent.IsRequired, value);
+ var labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description);
+ builder.AddLabel(labelBuilder);
+ }
+ break;
+ case SnowflakeSelectComponentInfo snowflakeSelectComponent:
+ {
+ var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList(), null, snowflakeSelectComponent.IsRequired);
+
+ if (modalInstance != null)
+ {
+ await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance));
+ }
+
+ var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description);
+ builder.AddLabel(labelBuilder);
+ }
+ break;
+ case FileUploadComponentInfo fileUploadComponent:
+ {
+ var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired);
+
+ if (modalInstance != null)
+ {
+ await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, fileUploadComponent.Getter(modalInstance));
+ }
+
+ var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description);
+ builder.AddLabel(labelBuilder);
+ }
+ break;
+ case TextDisplayComponentInfo textDisplayComponent:
+ {
+ var content = textDisplayComponent.Getter(modalInstance).ToString() ?? textDisplayComponent.Content;
+ var componentBuilder = new TextDisplayBuilder(content);
+ builder.AddTextDisplay(componentBuilder);
}
break;
default:
@@ -86,13 +145,7 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction
modifyModal?.Invoke(builder);
- return interaction.RespondWithModalAsync(builder.Build(), options);
- }
-
- private static Task SendModalResponseAsync(IDiscordInteraction interaction, string customId, ModalInfo modalInfo, RequestOptions options = null, Action modifyModal = null)
- {
- var modal = modalInfo.ToModal(customId, modifyModal);
- return interaction.RespondWithModalAsync(modal, options);
+ await interaction.RespondWithModalAsync(builder.Build(), options);
}
}
}
diff --git a/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs
new file mode 100644
index 0000000000..6161a3c845
--- /dev/null
+++ b/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs
@@ -0,0 +1,9 @@
+namespace Discord.Interactions;
+
+///
+/// Represents the class for type.
+///
+public class ChannelSelectComponentInfo : SnowflakeSelectComponentInfo
+{
+ internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { }
+}
diff --git a/src/Discord.Net.Interactions/Info/Components/FileUploadComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/FileUploadComponentInfo.cs
new file mode 100644
index 0000000000..41713e0c88
--- /dev/null
+++ b/src/Discord.Net.Interactions/Info/Components/FileUploadComponentInfo.cs
@@ -0,0 +1,23 @@
+namespace Discord.Interactions;
+
+///
+/// Represents the class for type.
+///
+public class FileUploadComponentInfo : InputComponentInfo
+{
+ ///
+ /// Gets the minimum number of values that can be selected.
+ ///
+ public int MinValues { get; }
+
+ ///
+ /// Gets the maximum number of values that can be selected.
+ ///
+ public int MaxValues { get; }
+
+ internal FileUploadComponentInfo(Builders.FileUploadComponentBuilder builder, ModalInfo modal) : base(builder, modal)
+ {
+ MinValues = builder.MinValues;
+ MaxValues = builder.MaxValues;
+ }
+}
diff --git a/src/Discord.Net.Interactions/Info/Components/InputComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/InputComponentInfo.cs
new file mode 100644
index 0000000000..7a5aa67d4b
--- /dev/null
+++ b/src/Discord.Net.Interactions/Info/Components/InputComponentInfo.cs
@@ -0,0 +1,43 @@
+namespace Discord.Interactions
+{
+ ///
+ /// Represents the base info class for input components.
+ ///
+ public abstract class InputComponentInfo : ModalComponentInfo
+ {
+ ///
+ /// Gets the custom id of this component.
+ ///
+ public string CustomId { get; }
+
+ ///
+ /// Gets the label of this component.
+ ///
+ public string Label { get; }
+
+ ///
+ /// Gets the description of this component.
+ ///
+ public string Description { get; }
+
+ ///
+ /// Gets whether or not this component requires a user input.
+ ///
+ public bool IsRequired { get; }
+
+ ///
+ /// Gets the assigned to this component.
+ ///
+ public ModalComponentTypeConverter TypeConverter { get; }
+
+ internal InputComponentInfo(Builders.IInputComponentBuilder builder, ModalInfo modal)
+ : base(builder, modal)
+ {
+ CustomId = builder.CustomId;
+ Label = builder.Label;
+ Description = builder.Description;
+ IsRequired = builder.IsRequired;
+ TypeConverter = builder.TypeConverter;
+ }
+ }
+}
diff --git a/src/Discord.Net.Interactions/Info/Components/MentionableSelectComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/MentionableSelectComponentInfo.cs
new file mode 100644
index 0000000000..498c500c0a
--- /dev/null
+++ b/src/Discord.Net.Interactions/Info/Components/MentionableSelectComponentInfo.cs
@@ -0,0 +1,9 @@
+namespace Discord.Interactions;
+
+///
+/// Represents the class for type.
+///
+public class MentionableSelectComponentInfo : SnowflakeSelectComponentInfo
+{
+ internal MentionableSelectComponentInfo(Builders.MentionableSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { }
+}
diff --git a/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs
new file mode 100644
index 0000000000..b92455892d
--- /dev/null
+++ b/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Reflection;
+
+namespace Discord.Interactions;
+
+///
+/// Represents the base info class for components.
+///
+public abstract class ModalComponentInfo
+{
+ private Lazy> _getter;
+ internal Func