diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs
index 0b4b5f3330..06ddbd5e08 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ButtonBuilder.cs
@@ -9,6 +9,7 @@ namespace Discord;
///
public class ButtonBuilder : IInteractableComponentBuilder
{
+ ///
public ComponentType Type => ComponentType.Button;
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs
index e2a3b65798..28e6cce5ce 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentBuilder.cs
@@ -10,7 +10,7 @@ namespace Discord;
public class ComponentBuilder
{
///
- /// The max length of a .
+ /// The max length of and .
///
public const int MaxCustomIdLength = 100;
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs
index 5d57d39654..c9b3059c63 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/ComponentContainerExtensions.cs
@@ -469,6 +469,54 @@ public static BuilderT WithActionRow(this BuilderT container,
return container.WithActionRow(cont);
}
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithFileUpload(this BuilderT container, FileUploadComponentBuilder fileUpload)
+ where BuilderT : class, IInteractableComponentContainer
+ {
+ container.AddComponent(fileUpload);
+ return container;
+ }
+
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithFileUpload(this BuilderT container,
+ string customId,
+ int? minValues = null,
+ int? maxValues = null,
+ bool isRequired = true,
+ int? id = null)
+ where BuilderT : class, IInteractableComponentContainer
+ => container.WithFileUpload(new FileUploadComponentBuilder()
+ .WithCustomId(customId)
+ .WithMinValues(minValues)
+ .WithMaxValues(maxValues)
+ .WithRequired(isRequired)
+ .WithId(id));
+
+ ///
+ /// Adds a to the container.
+ ///
+ ///
+ /// The current container.
+ ///
+ public static BuilderT WithFileUpload(this BuilderT container,
+ Action options)
+ where BuilderT : class, IInteractableComponentContainer
+ {
+ var comp = new FileUploadComponentBuilder();
+ options(comp);
+ return container.WithFileUpload(comp);
+ }
+
///
/// Finds the first in the
/// or any of its child s with matching id.
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/FileComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/FileComponentBuilder.cs
index 951ef063a1..f5ebb13f62 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/FileComponentBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/FileComponentBuilder.cs
@@ -2,6 +2,9 @@
namespace Discord;
+///
+/// Represents a class used to build 's.
+///
public class FileComponentBuilder : IMessageComponentBuilder
{
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/FileUploadComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/FileUploadComponentBuilder.cs
new file mode 100644
index 0000000000..582d998a10
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/FileUploadComponentBuilder.cs
@@ -0,0 +1,192 @@
+using System;
+
+namespace Discord;
+
+///
+/// Represents a class used to build 's.
+///
+public class FileUploadComponentBuilder : IInteractableComponentBuilder
+{
+ ///
+ /// The maximum number of values for the and properties.
+ ///
+ public const int MaxFileCount = 10;
+
+ ///
+ public ComponentType Type => ComponentType.FileUpload;
+
+ ///
+ public int? Id { get; set; }
+
+ ///
+ /// Gets or sets the custom id of the current file upload.
+ ///
+ /// length exceeds .
+ /// length subceeds 1.
+ public string CustomId
+ {
+ get => _customId;
+ set
+ {
+ if (value is not null)
+ {
+ Preconditions.AtLeast(value.Length, 1, nameof(CustomId));
+ Preconditions.AtMost(value.Length, ModalComponentBuilder.MaxCustomIdLength, nameof(CustomId));
+ }
+
+ _customId = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the minimum number of items that must be uploaded (defaults to 1).
+ ///
+ /// exceeds .
+ /// length subceeds 0.
+ public int? MinValues
+ {
+ get => _minValues;
+ set
+ {
+ if (value is not null)
+ {
+ Preconditions.AtLeast(value.Value, 0, nameof(MinValues));
+ Preconditions.AtMost(value.Value, MaxFileCount, nameof(MinValues));
+ }
+
+ _minValues = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum number of items that can be uploaded (defaults to 1).
+ ///
+ /// exceeds .
+ public int? MaxValues
+ {
+ get => _maxValues;
+ set
+ {
+ if (value is not null)
+ {
+ Preconditions.AtMost(value.Value, MaxFileCount, nameof(MaxValues));
+ }
+
+ _maxValues = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the current file upload requires files to be uploaded before submitting the modal (defaults to ).
+ ///
+ public bool IsRequired { get; set; } = true;
+
+ ///
+ /// Sets the custom id of the current file upload.
+ ///
+ /// The id to use for the current file upload.
+ ///
+ /// The current builder.
+ public FileUploadComponentBuilder WithCustomId(string customId)
+ {
+ CustomId = customId;
+ return this;
+ }
+
+ ///
+ /// Sets the minimum number of items that must be uploaded (defaults to 1).
+ ///
+ /// Sets the minimum number of items that must be uploaded.
+ ///
+ ///
+ /// The current builder.
+ ///
+ public FileUploadComponentBuilder WithMinValues(int? minValues)
+ {
+ MinValues = minValues;
+ return this;
+ }
+
+ ///
+ /// Sets the maximum number of items that can be uploaded (defaults to 1).
+ ///
+ /// The maximum number of items that can be uploaded.
+ ///
+ ///
+ /// The current builder.
+ ///
+ public FileUploadComponentBuilder WithMaxValues(int? maxValues)
+ {
+ MaxValues = maxValues;
+ return this;
+ }
+
+ ///
+ /// Sets whether the current file upload requires files to be uploaded before submitting the modal.
+ ///
+ /// Whether the current file upload requires files to be uploaded before submitting the modal.
+ ///
+ /// The current builder.
+ ///
+ public FileUploadComponentBuilder WithRequired(bool isRequired)
+ {
+ IsRequired = isRequired;
+ return this;
+ }
+
+ private string _customId;
+ private int? _minValues;
+ private int? _maxValues;
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ public FileUploadComponentBuilder() {}
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// The custom id of the current file upload.
+ /// The minimum number of items that must be uploaded (defaults to 1).
+ /// the maximum number of items that can be uploaded (defaults to 1).
+ /// Whether the current file upload requires files to be uploaded before submitting the modal.
+ /// The id for the component.
+ public FileUploadComponentBuilder(string customId, int? minValues = null, int? maxValues = null, bool isRequired = true, int? id = null)
+ {
+ CustomId = customId;
+ MinValues = minValues;
+ MaxValues = maxValues;
+ IsRequired = isRequired;
+ Id = id;
+ }
+
+ ///
+ /// Initializes a new instance of the class from an existing .
+ ///
+ /// The component.
+ public FileUploadComponentBuilder(FileUploadComponent fileUpload)
+ {
+ CustomId = fileUpload.CustomId;
+ MinValues = fileUpload.MinValues;
+ MaxValues = fileUpload.MaxValues;
+ IsRequired = fileUpload.IsRequired;
+ Id = fileUpload.Id;
+ }
+
+ ///
+ public FileUploadComponent Build()
+ {
+ Preconditions.NotNullOrWhitespace(CustomId, nameof(CustomId));
+
+ if (MinValues is not null && MaxValues is not null)
+ Preconditions.AtLeast(MaxValues.Value, MinValues.Value, nameof(MaxValues));
+
+ Preconditions.AtMost(MinValues ?? 0, MaxFileCount, nameof(MinValues));
+ Preconditions.AtMost(MaxValues ?? 0, MaxFileCount, nameof(MaxValues));
+
+ return new FileUploadComponent(Id, CustomId, MinValues, MaxValues, IsRequired);
+ }
+
+ ///
+ IMessageComponent IMessageComponentBuilder.Build() => Build();
+}
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/LabelBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/LabelBuilder.cs
new file mode 100644
index 0000000000..29e42ce4b2
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/LabelBuilder.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Immutable;
+
+namespace Discord;
+
+///
+/// Represents a class used to build 's.
+///
+public class LabelBuilder : IMessageComponentBuilder
+{
+ ///
+ public ImmutableArray SupportedComponentTypes { get; } =
+ [
+ ComponentType.SelectMenu,
+ ComponentType.TextInput,
+ ComponentType.UserSelect,
+ ComponentType.RoleSelect,
+ ComponentType.MentionableSelect,
+ ComponentType.ChannelSelect,
+ ComponentType.FileUpload
+ ];
+
+ ///
+ /// The maximum length of the label.
+ ///
+ public const int MaxLabelLength = 45;
+
+ ///
+ /// The maximum length of the description.
+ ///
+ public const int MaxDescriptionLength = 100;
+
+ ///
+ public ComponentType Type => ComponentType.Label;
+
+ ///
+ public int? Id { get; set; }
+
+ ///
+ /// Gets or sets the label text.
+ ///
+ public string Label
+ {
+ get => _label;
+ set
+ {
+ if (value is not null)
+ {
+ Preconditions.AtMost(value.Length, MaxLabelLength, nameof(Label));
+ }
+
+ _label = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the description text for the label.
+ ///
+ public string Description
+ {
+ get => _description;
+ set
+ {
+ if (value is not null)
+ {
+ Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description));
+ }
+
+ _description = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the component within the label.
+ ///
+ public IMessageComponentBuilder Component { get; set; }
+
+ ///
+ /// Sets the label text.
+ ///
+ /// The label text.
+ ///
+ /// The current builder.
+ ///
+ public LabelBuilder WithLabel(string label)
+ {
+ Label = label;
+ return this;
+ }
+
+ ///
+ /// Sets the description text for the label.
+ ///
+ /// The description text for the label.
+ ///
+ /// The current builder.
+ ///
+ public LabelBuilder WithDescription(string description)
+ {
+ Description = description;
+ return this;
+ }
+
+ ///
+ /// Sets the component within the label.
+ ///
+ /// The component within the label.
+ ///
+ /// The current builder.
+ ///
+ public LabelBuilder WithComponent(IMessageComponentBuilder component)
+ {
+ Component = component;
+ return this;
+ }
+
+ private string _label;
+ private string _description;
+
+ ///
+ /// Initializes a new .
+ ///
+ public LabelBuilder() { }
+
+ ///
+ /// Initializes a new with the specified content.
+ ///
+ /// The label text.
+ /// The component within the label.
+ /// The description text for the label.
+ /// The id for the component.
+ public LabelBuilder(string label, IMessageComponentBuilder component, string description = null, int? id = null)
+ {
+ Id = id;
+ Label = label;
+ Component = component;
+ Description = description;
+ }
+
+ ///
+ /// Initializes a new from existing component.
+ ///
+ public LabelBuilder(LabelComponent label)
+ {
+ Label = label.Label;
+ Description = label.Description;
+ Id = label.Id;
+ Component = label.Component.ToBuilder();
+ }
+
+ ///
+ public LabelComponent Build()
+ {
+ Preconditions.NotNullOrWhitespace(Label, nameof(Label));
+ Preconditions.AtMost(Label.Length, MaxLabelLength, nameof(Label));
+
+ Preconditions.AtMost(Description?.Length ?? 0, MaxDescriptionLength, nameof(Description));
+
+ Preconditions.NotNull(Component, nameof(Component));
+
+ if (!SupportedComponentTypes.Contains(Component.Type))
+ throw new InvalidOperationException($"Component can only be {nameof(SelectMenuBuilder)}, {nameof(TextInputBuilder)} or {nameof(FileUploadComponentBuilder)}.");
+
+ return new LabelComponent(Id, Label, Description, Component.Build());
+ }
+
+ ///
+ IMessageComponent IMessageComponentBuilder.Build() => Build();
+}
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/MediaGalleryBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/MediaGalleryBuilder.cs
index a6fcb65b55..94e3cf8f73 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/MediaGalleryBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/MediaGalleryBuilder.cs
@@ -18,7 +18,7 @@ public class MediaGalleryBuilder : IMessageComponentBuilder
///
public int? Id { get; set; }
- private List _items = new();
+ private List _items = [];
///
/// Initializes a new instance of the .
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextDisplayBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextDisplayBuilder.cs
index e50d011814..e0bd5d9f0e 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextDisplayBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextDisplayBuilder.cs
@@ -2,6 +2,9 @@
namespace Discord;
+///
+/// Represents a class used to build 's.
+///
public class TextDisplayBuilder : IMessageComponentBuilder
{
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs
index abecb1f50e..3659ecd6ad 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/Builders/TextInputBuilder.cs
@@ -14,12 +14,16 @@ public class TextInputBuilder : IInteractableComponentBuilder
/// The max length of a .
///
public const int MaxPlaceholderLength = 100;
+
+ ///
+ /// The max value for and , and the max length for .
+ ///
public const int LargestMaxLength = 4000;
///
/// Gets or sets the custom id of the current text input.
///
- /// length exceeds
+ /// length exceeds .
/// length subceeds 1.
public string CustomId
{
@@ -29,7 +33,7 @@ public string CustomId
if (value is not null)
{
Preconditions.AtLeast(value.Length, 1, nameof(CustomId));
- Preconditions.AtMost(value.Length, ComponentBuilder.MaxCustomIdLength, nameof(CustomId));
+ Preconditions.AtMost(value.Length, ModalComponentBuilder.MaxCustomIdLength, nameof(CustomId));
}
_customId = value;
@@ -44,6 +48,7 @@ public string CustomId
///
/// Gets or sets the label of the current text input.
///
+ [Obsolete("Label is no longer supported", error: false)]
public string Label { get; set; }
///
@@ -55,7 +60,8 @@ public string Placeholder
get => _placeholder;
set => _placeholder = (value?.Length ?? 0) <= MaxPlaceholderLength
? value
- : throw new ArgumentException($"Placeholder cannot have more than {MaxPlaceholderLength} characters. Value: \"{value}\"");
+ : throw new ArgumentException(
+ $"Placeholder cannot have more than {MaxPlaceholderLength} characters. Value: \"{value}\"");
}
///
@@ -72,7 +78,8 @@ public int? MinLength
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must not be less than 0");
if (value > LargestMaxLength)
- throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must not be greater than {LargestMaxLength}");
+ throw new ArgumentOutOfRangeException(nameof(value),
+ $"MinLength must not be greater than {LargestMaxLength}");
if (value > (MaxLength ?? LargestMaxLength))
throw new ArgumentOutOfRangeException(nameof(value), $"MinLength must be less than MaxLength");
_minLength = value;
@@ -93,9 +100,11 @@ public int? MaxLength
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength must not be less than 0");
if (value > LargestMaxLength)
- throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength most not be greater than {LargestMaxLength}");
+ throw new ArgumentOutOfRangeException(nameof(value),
+ $"MaxLength most not be greater than {LargestMaxLength}");
if (value < (MinLength ?? -1))
- throw new ArgumentOutOfRangeException(nameof(value), $"MaxLength must be greater than MinLength ({MinLength})");
+ throw new ArgumentOutOfRangeException(nameof(value),
+ $"MaxLength must be greater than MinLength ({MinLength})");
_maxLength = value;
}
}
@@ -105,6 +114,7 @@ public int? MaxLength
///
public bool? Required { get; set; }
+ ///
public int? Id { get; set; }
///
@@ -123,9 +133,11 @@ public string Value
set
{
if (value?.Length > (MaxLength ?? LargestMaxLength))
- throw new ArgumentOutOfRangeException(nameof(value), $"Value must not be longer than {MaxLength ?? LargestMaxLength}. Value: \"{value}\"");
+ throw new ArgumentOutOfRangeException(nameof(value),
+ $"Value must not be longer than {MaxLength ?? LargestMaxLength}. Value: \"{value}\"");
if (value?.Length < (MinLength ?? 0))
- throw new ArgumentOutOfRangeException(nameof(value), $"Value must not be shorter than {MinLength}. Value: \"{value}\"");
+ throw new ArgumentOutOfRangeException(nameof(value),
+ $"Value must not be shorter than {MinLength}. Value: \"{value}\"");
_value = value;
}
@@ -140,17 +152,25 @@ public string Value
///
/// Creates a new instance of a .
///
- /// The text input's label.
/// The text input's style.
/// The text input's custom id.
/// The text input's placeholder.
/// The text input's minimum length.
/// The text input's maximum length.
/// The text input's required value.
- public TextInputBuilder(string label, string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null,
- int? minLength = null, int? maxLength = null, bool? required = null, string value = null, int? id = null)
+ /// The text input's default value.
+ /// The id for the component.
+ public TextInputBuilder(
+ string customId,
+ TextInputStyle style = TextInputStyle.Short,
+ string placeholder = null,
+ int? minLength = null,
+ int? maxLength = null,
+ bool? required = null,
+ string value = null,
+ int? id = null
+ )
{
- Label = label;
Style = style;
CustomId = customId;
Placeholder = placeholder;
@@ -164,9 +184,34 @@ public TextInputBuilder(string label, string customId, TextInputStyle style = Te
///
/// Creates a new instance of a .
///
- public TextInputBuilder()
+ /// The text input's label.
+ /// The text input's style.
+ /// The text input's custom id.
+ /// The text input's placeholder.
+ /// The text input's minimum length.
+ /// The text input's maximum length.
+ /// The text input's required value.
+ [Obsolete("label is no longer supported", error: false)]
+ public TextInputBuilder(
+ string label,
+ string customId,
+ TextInputStyle style = TextInputStyle.Short,
+ string placeholder = null,
+ int? minLength = null,
+ int? maxLength = null,
+ bool? required = null,
+ string value = null,
+ int? id = null
+ ) : this(customId, style, placeholder, minLength, maxLength, required, value, id)
{
+ Label = label;
+ }
+ ///
+ /// Creates a new instance of a .
+ ///
+ public TextInputBuilder()
+ {
}
///
@@ -174,7 +219,9 @@ public TextInputBuilder()
///
public TextInputBuilder(TextInputComponent textInput)
{
+#pragma warning disable CS0618 // Type or member is obsolete
Label = textInput.Label;
+#pragma warning restore CS0618 // Type or member is obsolete
Style = textInput.Style;
CustomId = textInput.CustomId;
Placeholder = textInput.Placeholder;
@@ -190,6 +237,7 @@ public TextInputBuilder(TextInputComponent textInput)
///
/// The value to set.
/// The current builder.
+ [Obsolete("Label is no longer supported", error: false)]
public TextInputBuilder WithLabel(string label)
{
Label = label;
@@ -273,16 +321,19 @@ public TextInputBuilder WithRequired(bool required)
return this;
}
+ ///
public TextInputComponent Build()
{
if (string.IsNullOrEmpty(CustomId))
throw new ArgumentException("TextInputComponents must have a custom id.", nameof(CustomId));
- if (string.IsNullOrWhiteSpace(Label))
- throw new ArgumentException("TextInputComponents must have a label.", nameof(Label));
+
if (Style is TextInputStyle.Short && Value?.Any(x => x == '\n') is true)
- throw new ArgumentException($"Value must not contain new line characters when style is {TextInputStyle.Short}.", nameof(Value));
+ throw new ArgumentException(
+ $"Value must not contain new line characters when style is {TextInputStyle.Short}.", nameof(Value));
+#pragma warning disable CS0618 // Type or member is obsolete
return new TextInputComponent(CustomId, Label, Placeholder, MinLength, MaxLength, Style, Required, Value, Id);
+#pragma warning restore CS0618 // Type or member is obsolete
}
IMessageComponent IMessageComponentBuilder.Build() => Build();
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs
index 5d5cce5a77..4595fafa46 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs
@@ -45,18 +45,49 @@ public enum ComponentType
///
ChannelSelect = 8,
+ ///
+ /// A container to display text alongside an accessory component.
+ ///
Section = 9,
+ ///
+ /// A component displaying Markdown text.
+ ///
TextDisplay = 10,
+ ///
+ /// A small image that can be used as an accessory.
+ ///
Thumbnail = 11,
+ ///
+ /// A component displaying images and other media.
+ ///
MediaGallery = 12,
+ ///
+ /// A component displaying an attached file.
+ ///
File = 13,
+ ///
+ /// A component to add vertical padding between other components.
+ ///
Separator = 14,
+ ///
+ /// A container that visually groups a set of components.
+ ///
Container = 17,
+
+ ///
+ /// A layout component that wraps modal components (text input, select menu or file upload) with a label and description.
+ ///
+ Label = 18,
+
+ ///
+ /// A component that allows users to upload files in modals.
+ ///
+ FileUpload = 19
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/FileUploadComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/FileUploadComponent.cs
new file mode 100644
index 0000000000..26a23e85ab
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/FileUploadComponent.cs
@@ -0,0 +1,51 @@
+namespace Discord;
+
+///
+/// Represents a component that allows users to upload files in modals.
+///
+public class FileUploadComponent : IInteractableComponent
+{
+ ///
+ public ComponentType Type => ComponentType.FileUpload;
+
+ ///
+ /// Gets the ID of this component.
+ ///
+ public int? Id { get; }
+
+ ///
+ /// Gets the custom ID of this component.
+ ///
+ public string CustomId { get; }
+
+ ///
+ /// Gets the minimum number of files a user must upload.
+ ///
+ public int? MinValues { get; }
+
+ ///
+ /// Gets the maximum number of files a user can upload.
+ ///
+ public int? MaxValues { get; }
+
+ ///
+ /// Gets whether this component requires a file upload to be submitted.
+ ///
+ public bool IsRequired { get; }
+
+ internal FileUploadComponent(int? id, string customId, int? minValues, int? maxValues, bool isRequired)
+ {
+ Id = id;
+ CustomId = customId;
+ MinValues = minValues;
+ MaxValues = maxValues;
+ IsRequired = isRequired;
+ }
+
+ ///
+ public FileUploadComponentBuilder ToBuilder()
+ => new(this);
+
+ ///
+ IMessageComponentBuilder IMessageComponent.ToBuilder() => ToBuilder();
+}
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs
index 1d043d10ad..deeb2f850b 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IMessageComponent.cs
@@ -11,7 +11,7 @@ public interface IMessageComponent
ComponentType Type { get; }
///
- ///
+ /// Gets the id for the component.
///
int? Id { get; }
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/LabelComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/LabelComponent.cs
new file mode 100644
index 0000000000..e9c1cfb91c
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/LabelComponent.cs
@@ -0,0 +1,40 @@
+namespace Discord;
+
+///
+/// Represents a layout component that wraps modal components (text input, select menu or file upload) with a label and description.
+///
+public class LabelComponent : IMessageComponent
+{
+ ///
+ public ComponentType Type => ComponentType.Label;
+
+ ///
+ public int? Id { get; }
+
+ ///
+ /// Gets the label text.
+ ///
+ public string Label { get; }
+
+ ///
+ /// Gets the description text for the label.
+ ///
+ public string Description { get; }
+
+ ///
+ /// Gets the component within the label.
+ ///
+ public IMessageComponent Component { get; }
+
+ internal LabelComponent(int? id, string label, string description, IMessageComponent component)
+ {
+ Id = id;
+ Label = label;
+ Description = description;
+ Component = component;
+ }
+
+ ///
+ public IMessageComponentBuilder ToBuilder()
+ => new LabelBuilder(this);
+}
diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs
index 01eae3dcc2..9cd612bdb5 100644
--- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/TextInputComponent.cs
@@ -49,15 +49,23 @@ public class TextInputComponent : IInteractableComponent
///
public string Value { get; }
-
///
/// Converts a to a .
///
public TextInputBuilder ToBuilder()
- => new TextInputBuilder(this);
+ => new(this);
- internal TextInputComponent(string customId, string label, string placeholder, int? minLength, int? maxLength,
- TextInputStyle style, bool? required, string value, int? id)
+ internal TextInputComponent(
+ string customId,
+ string label,
+ string placeholder,
+ int? minLength,
+ int? maxLength,
+ TextInputStyle style,
+ bool? required,
+ string value,
+ int? id
+ )
{
CustomId = customId;
Label = label;
diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteractionData.cs
index 767dd5df75..264c53190f 100644
--- a/src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteractionData.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/Modals/IModalInteractionData.cs
@@ -16,5 +16,30 @@ public interface IModalInteractionData : IDiscordInteractionData
/// Gets the components submitted by the user.
///
IReadOnlyCollection Components { get; }
+
+ ///
+ /// Gets the channels(s) of a component within the modal.
+ ///
+ IReadOnlyCollection Channels { get; }
+
+ ///
+ /// Gets the user(s) of a or component within the modal.
+ ///
+ IReadOnlyCollection Users { get; }
+
+ ///
+ /// Gets the roles(s) of a or component within the modal.
+ ///
+ IReadOnlyCollection Roles { get; }
+
+ ///
+ /// Gets the guild member(s) of a or component within the modal.
+ ///
+ IReadOnlyCollection Members { get; }
+
+ ///
+ /// Gets the attachment(s) of a component within the modal.
+ ///
+ IReadOnlyCollection Attachments { get; }
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs
index 6062db0cc0..1d61030381 100644
--- a/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs
@@ -10,7 +10,9 @@ public class Modal
///
public string Title { get; set; }
- ///
+ ///
+ /// Gets the custom id of the modal.
+ ///
public string CustomId { get; set; }
///
diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs
index 016071ced6..7afe85c315 100644
--- a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalBuilder.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -11,7 +12,13 @@ public class ModalBuilder
{
private string _customId;
- public ModalBuilder() { }
+ ///
+ /// Creates a new and empty .
+ ///
+ public ModalBuilder()
+ {
+ Components = new();
+ }
///
/// Creates a new instance of the .
@@ -19,7 +26,6 @@ public ModalBuilder() { }
/// The modal's title.
/// The modal's customId.
/// The modal's components.
- /// Only TextInputComponents are allowed.
public ModalBuilder(string title, string customId, ModalComponentBuilder components = null)
{
Title = title;
@@ -27,6 +33,28 @@ public ModalBuilder(string title, string customId, ModalComponentBuilder compone
Components = components ?? new();
}
+ ///
+ /// Creates a new instance of the .
+ ///
+ /// The modal's title.
+ /// The modal's customId.
+ /// The modal's components.
+ public ModalBuilder(string title, string customId, params IEnumerable components)
+ : this(title, customId, new ModalComponentBuilder(components))
+ {
+ }
+
+ ///
+ /// Creates a new instance of the .
+ ///
+ /// The modal's title.
+ /// The modal's customId.
+ /// The modal's components.
+ public ModalBuilder(string title, string customId, params IEnumerable components)
+ : this(title, customId, new ModalComponentBuilder(components))
+ {
+ }
+
///
/// Gets or sets the title of the current modal.
///
@@ -43,7 +71,7 @@ public string CustomId
if (value is not null)
{
Preconditions.AtLeast(value.Length, 1, nameof(CustomId));
- Preconditions.AtMost(value.Length, ComponentBuilder.MaxCustomIdLength, nameof(CustomId));
+ Preconditions.AtMost(value.Length, ModalComponentBuilder.MaxCustomIdLength, nameof(CustomId));
}
_customId = value;
@@ -53,7 +81,7 @@ public string CustomId
///
/// Gets or sets the components of the current modal.
///
- public ModalComponentBuilder Components { get; set; } = new();
+ public ModalComponentBuilder Components { get; set; }
///
/// Sets the title of the current modal.
@@ -83,52 +111,227 @@ public ModalBuilder WithCustomId(string customId)
/// The component to add.
/// The row to add the text input.
/// The current builder.
- public ModalBuilder AddTextInput(TextInputBuilder component, int row = 0)
+ [Obsolete("Modal components no longer have rows", error: false)]
+ public ModalBuilder AddTextInput(TextInputBuilder component, int row)
{
Components.WithTextInput(component, row);
return this;
}
- ///
- /// Adds a to the current builder.
- ///
- /// The input's custom id.
- /// The input's label.
- /// The input's placeholder text.
- /// The input's minimum length.
- /// The input's maximum length.
- /// The input's style.
- /// The current builder.
- public ModalBuilder AddTextInput(string label, string customId, TextInputStyle style = TextInputStyle.Short,
- string placeholder = "", int? minLength = null, int? maxLength = null, bool? required = null, string value = null)
- => AddTextInput(new(label, customId, style, placeholder, minLength, maxLength, required, value));
+ ///
+ /// The current .
+ public ModalBuilder AddTextInput(
+ string label,
+ string customId,
+ TextInputStyle style = TextInputStyle.Short,
+ string placeholder = null,
+ int? minLength = null,
+ int? maxLength = null,
+ bool? required = null,
+ string value = null,
+ int? id = null,
+ string description = null,
+ int? labelId = null
+ )
+ {
+ Components.WithTextInput(
+ label, customId, style, placeholder, minLength, maxLength, 0, required, value, id, description,
+ labelId
+ );
+
+ return this;
+ }
+
+ ///
+ /// The current .
+ public ModalBuilder AddLabel(LabelBuilder label)
+ {
+ Components.WithLabel(label);
+ return this;
+ }
+
+ ///
+ /// The current .
+ public ModalBuilder AddLabel(
+ string label,
+ IMessageComponentBuilder component,
+ string description = null,
+ int? id = null
+ )
+ {
+ Components.WithLabel(label, component, description, id);
+ return this;
+ }
+
+ ///
+ /// The current .
+ public ModalBuilder AddSelectMenu(
+ string label,
+ string customId,
+ List options = null,
+ string placeholder = null,
+ int minValues = 1,
+ int maxValues = 1,
+ bool disabled = false,
+ ComponentType type = ComponentType.SelectMenu,
+ ChannelType[] channelTypes = null,
+ int? id = null,
+ string description = null,
+ int? labelId = null
+ )
+ {
+ Components.WithSelectMenu(
+ label,
+ customId,
+ options,
+ placeholder,
+ minValues,
+ maxValues,
+ disabled,
+ type,
+ channelTypes,
+ id,
+ description,
+ labelId
+ );
+
+ return this;
+ }
+
+ ///
+ /// The current .
+ public ModalBuilder AddSelectMenu(
+ string label,
+ SelectMenuBuilder menu,
+ string description = null,
+ int? labelId = null
+ )
+ {
+ Components.WithSelectMenu(label, menu, description, labelId);
+ return this;
+ }
+
+ ///
+ /// The current .
+ public ModalBuilder AddFileUpload(
+ string label,
+ FileUploadComponentBuilder fileUpload,
+ string description = null,
+ int? labelId = null
+ )
+ {
+ Components.WithFileUpload(label, fileUpload, description, labelId);
+ return this;
+ }
+
+ ///
+ /// The current .
+ public ModalBuilder AddFileUpload(
+ string label,
+ string customId,
+ int? minValues = null,
+ int? maxValues = null,
+ bool isRequired = true,
+ int? id = null,
+ string description = null,
+ int? labelId = null
+ )
+ {
+ Components.WithFileUpload(label, customId, minValues, maxValues, isRequired, id, description, labelId);
+ return this;
+ }
+
+ ///
+ /// The current .
+ public ModalBuilder AddTextDisplay(TextDisplayBuilder textDisplay)
+ {
+ Components.WithTextDisplay(textDisplay);
+ return this;
+ }
+
+ ///
+ /// The current .
+ public ModalBuilder AddTextDisplay(string content, int? id = null)
+ {
+ Components.WithTextDisplay(content, id);
+ return this;
+ }
///
/// Adds multiple components to the current builder.
///
/// The components to add.
/// The current builder
+ [Obsolete("Modal components no longer have rows", error: false)]
public ModalBuilder AddComponents(List components, int row)
{
components.ForEach(x => Components.AddComponent(x, row));
return this;
}
+ ///
+ /// Adds multiple components to the current builder.
+ ///
+ /// The components to add.
+ /// The current builder
+ public ModalBuilder AddComponents(params IEnumerable components)
+ {
+ Components.With(components);
+ return this;
+ }
+
+ ///
+ /// Gets a by the specified .
+ ///
+ ///
+ /// The of the component to get.
+ ///
+ ///
+ /// The component that was found, otherwise.
+ ///
+ public IInteractableComponentBuilder GetComponent(string customId) =>
+ GetComponent(customId);
+
///
/// Gets a by the specified .
///
/// The type of the component to get.
- /// The of the component to get.
+ ///
+ /// The of the component to get.
+ ///
///
- /// The component of type that was found, otherwise.
+ /// The component of type that was found,
+ /// otherwise.
///
public TMessageComponentBuilder GetComponent(string customId)
where TMessageComponentBuilder : class, IInteractableComponentBuilder
{
Preconditions.NotNull(customId, nameof(customId));
- return Components.ActionRows?.SelectMany(r => r.Components.OfType())
- .FirstOrDefault(c => c.CustomId == customId);
+ var components = Components.SelectMany(ExtractComponent);
+
+ // optimization: no need for the of type call if we're checking the root type.
+ if (typeof(TMessageComponentBuilder) != typeof(IInteractableComponentBuilder))
+ components = components.OfType();
+
+ return (TMessageComponentBuilder)components.FirstOrDefault(x => x.CustomId == customId);
+
+ /*
+ * Used to extract depth=1 components from the modal. Allows for the same behaviour of the previous
+ * iteration of the builder, whilst adding support for label components.
+ *
+ * This is not a long-term solution, and can break if more component types are added or nesting is changed.
+ */
+ static IEnumerable ExtractComponent(IMessageComponentBuilder builder)
+ => builder switch
+ {
+ LabelBuilder { Component: IInteractableComponentBuilder target } => [target],
+ ActionRowBuilder { Components: { } components }
+ => components.OfType(),
+ _ => []
+ };
}
///
@@ -144,29 +347,25 @@ public ModalBuilder UpdateTextInput(string customId, Action up
{
Preconditions.NotNull(customId, nameof(customId));
- var component = GetComponent(customId) ?? throw new ArgumentException($"There is no component of type {nameof(TextInputComponent)} with the specified custom ID in this modal builder.", nameof(customId));
- var row = Components.ActionRows.First(r => r.Components.Contains(component));
+ var component = GetComponent(customId) ?? throw new ArgumentException(
+ $"There is no component of type {nameof(TextInputComponent)} with the specified custom ID in this modal builder.",
+ nameof(customId));
- var builder = new TextInputBuilder
- {
- Label = component.Label,
- CustomId = component.CustomId,
- Style = component.Style,
- Placeholder = component.Placeholder,
- MinLength = component.MinLength,
- MaxLength = component.MaxLength,
- Required = component.Required,
- Value = component.Value
- };
-
- updateTextInput(builder);
+ /*
+ * We can just update the instance in-place, we don't need to update the parent here.
+ *
+ * NOTE:
+ * this does change the behaviour of this function, since in the previous iteration, we would've removed
+ * and re-added the component to/from the row, which has the inverse effect of sliding it to the end of the
+ * row. With this change, we no longer update the position within the row, but I think the position
+ * shifting was an unintended side effect- and therefor a bug.
+ */
- row.Components.Remove(component);
- row.AddComponent(builder);
+ updateTextInput(component);
return this;
}
-
+
///
/// Updates the value of a by the specified .
///
@@ -188,7 +387,35 @@ public ModalBuilder RemoveComponent(string customId)
{
Preconditions.NotNull(customId, nameof(customId));
- Components.ActionRows?.ForEach(r => r.Components.RemoveAll(c => c is IInteractableComponentBuilder ic && ic.CustomId == customId));
+ /*
+ * This function actually removed any component with the provided custom id, and could remove
+ * more than one. To keep this behaviour, the below code attempts to do the same.
+ *
+ * For reference, this was the old implementation
+ * Components.ActionRows?.ForEach(r => r
+ * .Components
+ * .RemoveAll(c => c is IInteractableComponentBuilder ic && ic.CustomId == customId)
+ * );
+ */
+
+ foreach (var parent in Components.ToArray())
+ {
+ switch (parent)
+ {
+ case LabelBuilder { Component: IInteractableComponentBuilder target } label
+ when target.CustomId == customId:
+ // you cannot have a label without a component, so we actually remove the label here
+ Components.Remove(label);
+ break;
+ case ActionRowBuilder row:
+ row.Components.RemoveAll(x =>
+ x is IInteractableComponentBuilder ic &&
+ ic.CustomId == customId
+ );
+ break;
+ }
+ }
+
return this;
}
@@ -199,7 +426,11 @@ public ModalBuilder RemoveComponent(string customId)
/// The current builder.
public ModalBuilder RemoveComponentsOfType(ComponentType type)
{
- Components.ActionRows?.ForEach(r => r.Components.RemoveAll(c => c.Type == type));
+ foreach (var component in Components.ToArray())
+ {
+ if (component.Type == type) Components.Remove(component);
+ }
+
return this;
}
@@ -216,8 +447,6 @@ public Modal Build()
throw new ArgumentException("Modals must have a custom ID.", nameof(CustomId));
if (string.IsNullOrWhiteSpace(Title))
throw new ArgumentException("Modals must have a title.", nameof(Title));
- if (Components.ActionRows?.SelectMany(r => r.Components).Any(c => c.Type != ComponentType.TextInput) ?? false)
- throw new ArgumentException($"Only components of type {nameof(TextInputComponent)} are allowed.", nameof(Components));
return new(Title, CustomId, Components.Build());
}
@@ -226,7 +455,7 @@ public Modal Build()
///
/// Represents a builder for creating a .
///
- public class ModalComponentBuilder
+ public class ModalComponentBuilder : IList
{
///
/// The max length of a .
@@ -236,126 +465,448 @@ public class ModalComponentBuilder
///
/// The max amount of rows a can have.
///
+ [Obsolete("Modal components no longer support action rows", error: true)]
public const int MaxActionRowCount = 5;
///
- /// Gets or sets the Action Rows for this Component Builder.
+ /// Gets the number of components in the builder.
+ ///
+ public int Count => _components.Count;
+
+ ///
+ /// Gets or sets the component at the specified index.
///
- /// cannot be null.
- /// count exceeds .
- public List ActionRows
+ /// The index of the component to get or set
+ public IMessageComponentBuilder this[int index]
{
- get => _actionRows;
+ get => _components[index];
set
{
- if (value == null)
- throw new ArgumentNullException(nameof(value), $"{nameof(ActionRows)} cannot be null.");
- if (value.Count > MaxActionRowCount)
- throw new ArgumentOutOfRangeException(nameof(value), $"Action row count must be less than or equal to {MaxActionRowCount}.");
- _actionRows = value;
+ ValidateComponentBuilder(value);
+ _components[index] = value;
}
}
- private List _actionRows;
+ private readonly List _components;
///
- /// Creates a new builder from the provided list of components.
+ /// Constructs an empty .
///
- /// The components to create the builder from.
- /// The newly created builder.
- public static ComponentBuilder FromComponents(IReadOnlyCollection components)
+ public ModalComponentBuilder()
+ {
+ _components = [];
+ }
+
+ ///
+ /// Constructs a with the provided
+ /// s.
+ ///
+ /// The components to add to this
+ public ModalComponentBuilder(params IEnumerable components) : this()
{
- var builder = new ComponentBuilder();
- for (int i = 0; i != components.Count; i++)
+ foreach (var component in components)
{
- var component = components.ElementAt(i);
- builder.AddComponent(component, i);
+ Add(component);
}
- return builder;
}
- internal void AddComponent(IMessageComponent component, int row)
+ ///
+ /// Constructs a with the provided
+ /// s.
+ ///
+ /// The components to add to this
+ public ModalComponentBuilder(params IEnumerable components) : this()
{
- switch (component)
+ foreach (var component in components)
{
- case TextInputComponent text:
- WithTextInput(text.Label, text.CustomId, text.Style, text.Placeholder, text.MinLength, text.MaxLength, row);
- break;
- case ActionRowComponent actionRow:
- foreach (var cmp in actionRow.Components)
- AddComponent(cmp, row);
- break;
+ Add(component);
}
}
+ private static void ValidateComponentBuilder(IMessageComponentBuilder builder)
+ {
+ if (builder is not LabelBuilder and not ActionRowBuilder and not TextDisplayBuilder)
+ throw new InvalidOperationException(
+ $"Only top-level modal components (labels, action rows or text displays) are allowed, not {builder.GetType().Name}."
+ );
+ }
+
///
- /// Adds a to the at the specific row.
- /// If the row cannot accept the component then it will add it to a row that can.
+ /// Creates a new builder from the provided list of components.
///
- /// The input's custom id.
- /// The input's label.
- /// The input's placeholder text.
- /// The input's minimum length.
- /// The input's maximum length.
- /// The input's style.
- /// The current builder.
- public ModalComponentBuilder WithTextInput(string label, string customId, TextInputStyle style = TextInputStyle.Short,
- string placeholder = null, int? minLength = null, int? maxLength = null, int row = 0, bool? required = null,
- string value = null)
- => WithTextInput(new(label, customId, style, placeholder, minLength, maxLength, required, value), row);
+ /// The components to create the builder from.
+ /// The newly created builder.
+ public static ModalComponentBuilder FromComponents(params IEnumerable components)
+ {
+ var builder = new ModalComponentBuilder();
+
+ foreach (var component in components)
+ builder.Add(component);
+
+ return builder;
+ }
+
+ [Obsolete("Modal components no longer have rows", error: true)]
+ internal ModalComponentBuilder AddComponent(IMessageComponent component, int row)
+ => Add(component);
///
- /// Adds a to the at the specific row.
- /// If the row cannot accept the component then it will add it to a row that can.
+ /// Adds a component to this .
///
- /// The to add.
- /// The row to add the text input.
- /// There are no more rows to add a text input to.
- /// must be less than .
- /// The current builder.
- public ModalComponentBuilder WithTextInput(TextInputBuilder text, int row = 0)
+ /// The component to add.
+ /// The current .
+ public ModalComponentBuilder Add(IMessageComponent component)
+ => Add(component.ToBuilder());
+
+ ///
+ /// Adds a component to this .
+ ///
+ /// The component to add.
+ /// The current .
+ public ModalComponentBuilder Add(IMessageComponentBuilder component)
{
- Preconditions.LessThan(row, MaxActionRowCount, nameof(row));
+ ValidateComponentBuilder(component);
- if (_actionRows == null)
- {
- _actionRows = new List
- {
- new ActionRowBuilder().AddComponent(text)
- };
- }
- else
+ _components.Add(component);
+ return this;
+ }
+
+ ///
+ /// Sets the components in this builder to the provided
+ ///
+ /// The components to set this builder to.
+ /// The current .
+ public ModalComponentBuilder With(params IEnumerable components)
+ {
+ _components.Clear();
+
+ foreach (var component in components)
+ Add(component);
+
+ return this;
+ }
+
+ ///
+ /// Adds a to the current .
+ ///
+ /// The to add.
+ /// The current .
+ public ModalComponentBuilder WithLabel(LabelBuilder label)
+ => Add(label);
+
+ ///
+ /// Constructs and adds a to the current .
+ ///
+ /// The label of the .
+ /// The component of the .
+ /// The description of the .
+ /// The id of the .
+ /// The current .
+ public ModalComponentBuilder WithLabel(
+ string label,
+ IMessageComponentBuilder component,
+ string description = null,
+ int? id = null
+ ) => WithLabel(new(
+ label,
+ component,
+ description,
+ id
+ ));
+
+ ///
+ /// Constructs and adds a containing a to the
+ /// current .
+ ///
+ /// The label around the .
+ /// The custom id of the .
+ /// The options of the .
+ /// The placeholder of the .
+ /// The min values of the .
+ /// The max values of the .
+ /// Whether the is disabled.
+ /// The type of the .
+ /// The channel types of the .
+ /// The id of the .
+ /// The description around the .
+ ///
+ /// The id of the wrapping the .
+ ///
+ /// The current .
+ public ModalComponentBuilder WithSelectMenu(
+ string label,
+ string customId,
+ List options = null,
+ string placeholder = null,
+ int minValues = 1,
+ int maxValues = 1,
+ bool disabled = false,
+ ComponentType type = ComponentType.SelectMenu,
+ ChannelType[] channelTypes = null,
+ int? id = null,
+ string description = null,
+ int? labelId = null
+ ) => WithSelectMenu(
+ label,
+ new SelectMenuBuilder()
+ .WithId(id)
+ .WithCustomId(customId)
+ .WithOptions(options)
+ .WithPlaceholder(placeholder)
+ .WithMaxValues(maxValues)
+ .WithMinValues(minValues)
+ .WithDisabled(disabled)
+ .WithType(type)
+ .WithChannelTypes(channelTypes),
+ description,
+ labelId
+ );
+
+ ///
+ /// Constructs and adds a with the provided to
+ /// the current .
+ ///
+ /// The label around the .
+ /// The menu to add.
+ /// The description around the .
+ ///
+ /// The id of the wrapping the .
+ ///
+ /// The current .
+ public ModalComponentBuilder WithSelectMenu(
+ string label,
+ SelectMenuBuilder menu,
+ string description = null,
+ int? labelId = null
+ )
+ {
+ if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count)
+ throw new InvalidOperationException("Please make sure that there is no duplicates values.");
+
+ return WithLabel(
+ label,
+ menu,
+ description,
+ labelId
+ );
+ }
+
+ ///
+ /// Constructs and adds a with the provided
+ /// to the current .
+ ///
+ /// The label around the .
+ /// The file upload to add.
+ /// The description around the .
+ ///
+ /// The id of the wrapping the .
+ ///
+ /// The current .
+ public ModalComponentBuilder WithFileUpload(
+ string label,
+ FileUploadComponentBuilder fileUpload,
+ string description = null,
+ int? labelId = null
+ ) => WithLabel(label, fileUpload, description, labelId);
+
+ ///
+ /// Constructs and adds a with a
+ /// to the current .
+ ///
+ /// The label around the .
+ /// The custom id of the .
+ /// The min values of the .
+ /// The max values of the .
+ /// Whether the is required.
+ /// The id of the .
+ /// The description around the .
+ ///
+ /// The id of the wrapping the .
+ ///
+ /// The current .
+ public ModalComponentBuilder WithFileUpload(
+ string label,
+ string customId,
+ int? minValues = null,
+ int? maxValues = null,
+ bool isRequired = true,
+ int? id = null,
+ string description = null,
+ int? labelId = null
+ ) => WithLabel(
+ label,
+ new FileUploadComponentBuilder(
+ customId,
+ minValues,
+ maxValues,
+ isRequired,
+ id
+ ),
+ description,
+ labelId
+ );
+
+ ///
+ /// Adds a to the current .
+ ///
+ /// The to add.
+ /// The current .
+ public ModalComponentBuilder WithTextDisplay(TextDisplayBuilder textDisplay)
+ => Add(textDisplay);
+
+ ///
+ /// Constructs and adds a to the current .
+ ///
+ /// The content of the .
+ /// The id of the .
+ /// The current .
+ public ModalComponentBuilder WithTextDisplay(string content, int? id = null)
+ => WithTextDisplay(new TextDisplayBuilder(content, id));
+
+ ///
+ /// Constructs and adds a with the provided to
+ /// the current .
+ ///
+ /// The label around the .
+ /// The text input to add.
+ /// The description around the .
+ ///
+ /// The id of the wrapping the .
+ ///
+ /// The current .
+ public ModalComponentBuilder WithTextInput(
+ string label,
+ TextInputBuilder textInput,
+ string description = null,
+ int? labelId = null
+ ) => WithLabel(label, textInput, description, labelId);
+
+ ///
+ /// Constructs and adds a with the provided to
+ /// the current .
+ ///
+ /// The text input to add.
+ /// The current .
+ [Obsolete("text components must be wrapped in a label", error: false)]
+ public ModalComponentBuilder WithTextInput(TextInputBuilder text)
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ if (text.Label is null)
{
- if (_actionRows.Count == row)
- _actionRows.Add(new ActionRowBuilder().AddComponent(text));
- else
- {
- ActionRowBuilder actionRow;
- if (_actionRows.Count > row)
- actionRow = _actionRows.ElementAt(row);
- else
- {
- actionRow = new ActionRowBuilder();
- _actionRows.Add(actionRow);
- }
-
- if (actionRow.CanTakeComponent(text))
- actionRow.AddComponent(text);
- else if (row < MaxActionRowCount)
- WithTextInput(text, row + 1);
- else
- throw new InvalidOperationException($"There are no more rows to add {nameof(text)} to.");
- }
+ // TODO: better explain
+ throw new ArgumentNullException(
+ nameof(text),
+ "Label cannot be null"
+ );
}
- return this;
+ return WithLabel(
+ text.Label,
+ text
+ );
+
+#pragma warning restore CS0618 // Type or member is obsolete
}
+ ///
+ /// Constructs and adds a with the provided to
+ /// the current .
+ ///
+ /// The text input to add.
+ /// The row to add the text input to.
+ /// The current .
+ [Obsolete("Modal components no longer have rows", error: false)]
+ public ModalComponentBuilder WithTextInput(TextInputBuilder text, int row)
+ => WithTextInput(text);
+
+ ///
+ /// Constructs and adds a with a
+ /// to the current .
+ ///
+ /// The label around the .
+ /// The custom id of the .
+ /// The style of the .
+ /// The placeholder of the .
+ /// The min length of the .
+ /// The max length of the .
+ /// DEPRECATED: The row to place the on.
+ /// Whether the is required.
+ /// The value of the .
+ /// The id of the .
+ /// The description around the .
+ ///
+ /// The id of the wrapping the .
+ ///
+ /// The current .
+ public ModalComponentBuilder WithTextInput(
+ string label,
+ string customId,
+ TextInputStyle style = TextInputStyle.Short,
+ string placeholder = null,
+ int? minLength = null,
+ int? maxLength = null,
+ int row = 0,
+ bool? required = null,
+ string value = null,
+ int? id = null,
+ string description = null,
+ int? labelId = null
+ ) => WithLabel(
+ label,
+ new TextInputBuilder(
+ customId,
+ style,
+ placeholder,
+ minLength,
+ maxLength,
+ required,
+ value,
+ id
+ ),
+ description,
+ labelId
+ );
+
+ ///
+ void ICollection.Add(IMessageComponentBuilder item) => Add(item);
+
+ ///
+ public void Clear() => _components.Clear();
+
+ ///
+ public bool Contains(IMessageComponentBuilder item) => _components.Contains(item);
+
+ ///
+ public void CopyTo(IMessageComponentBuilder[] array, int arrayIndex) => _components.CopyTo(array, arrayIndex);
+
+ ///
+ public bool Remove(IMessageComponentBuilder item) => _components.Remove(item);
+
+ ///
+ public int IndexOf(IMessageComponentBuilder item) => _components.IndexOf(item);
+
+ ///
+ public void Insert(int index, IMessageComponentBuilder item)
+ {
+ ValidateComponentBuilder(item);
+
+ _components.Insert(index, item);
+ }
+
+ ///
+ public void RemoveAt(int index) => _components.RemoveAt(index);
+
+ ///
+ public IEnumerator GetEnumerator() => _components.GetEnumerator();
+
///
/// Get a representing the builder.
///
/// A representing the builder.
public ModalComponent Build()
- => new(ActionRows?.Select(x => x.Build()).ToList());
+ => new(_components.Select(x => x.Build()).ToList());
+
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_components).GetEnumerator();
+ bool ICollection.IsReadOnly => false;
}
}
diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalComponent.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalComponent.cs
index ecc90720fa..0857649247 100644
--- a/src/Discord.Net.Core/Entities/Interactions/Modals/ModalComponent.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/Modals/ModalComponent.cs
@@ -10,9 +10,9 @@ public class ModalComponent
///
/// Gets the components to be used in a modal.
///
- public IReadOnlyCollection Components { get; }
+ public IReadOnlyCollection Components { get; }
- internal ModalComponent(List components)
+ internal ModalComponent(List components)
{
Components = components;
}
diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
index 85f53af3f5..c244640b45 100644
--- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
+++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs
@@ -10,6 +10,7 @@ public static class IDiscordInteractionExtentions
///
/// Type of the implementation.
/// The interaction to respond to.
+ /// The custom id of the modal.
/// Delegate that can be used to modify the modal.
/// The request options for this request.
/// A task that represents the asynchronous operation of responding to the interaction.
@@ -31,10 +32,11 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction
///
/// Type of the implementation.
/// The interaction to respond to.
+ /// The custom id of the modal.
/// Interaction service instance that should be used to build s.
/// The request options for this request.
/// Delegate that can be used to modify the modal.
- ///
+ /// A task that represents the asynchronous operation of responding to the interaction.
public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, InteractionService interactionService,
RequestOptions options = null, Action modifyModal = null)
where T : class, IModal
@@ -50,10 +52,11 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction
///
/// Type of the implementation.
/// The interaction to respond to.
+ /// The custom id of the modal.
/// The instance to get field values from.
/// The request options for this request.
/// Delegate that can be used to modify the modal.
- ///
+ /// A task that represents the asynchronous operation of responding to the interaction.
public static Task RespondWithModalAsync(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null,
Action modifyModal = null)
where T : class, IModal
@@ -81,8 +84,7 @@ public static Task RespondWithModalAsync(this IDiscordInteraction interaction
throw new InvalidOperationException($"{input.GetType().FullName} isn't a valid component info class");
}
- if (modifyModal is not null)
- modifyModal(builder);
+ modifyModal?.Invoke(builder);
return interaction.RespondWithModalAsync(builder.Build(), options);
}
diff --git a/src/Discord.Net.Rest/API/Common/FileUploadComponent.cs b/src/Discord.Net.Rest/API/Common/FileUploadComponent.cs
new file mode 100644
index 0000000000..ed2f71fcc9
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/FileUploadComponent.cs
@@ -0,0 +1,43 @@
+using Newtonsoft.Json;
+
+namespace Discord.API;
+
+internal class FileUploadComponent : IInteractableComponent
+{
+ [JsonProperty("type")]
+ public ComponentType Type { get; set; }
+
+ [JsonProperty("id")]
+ public Optional Id { get; set; }
+
+ [JsonProperty("custom_id")]
+ public string CustomId { get; set; }
+
+ [JsonProperty("min_values")]
+ public Optional MinValues { get; set; }
+
+ [JsonProperty("max_values")]
+ public Optional MaxValues { get; set; }
+
+ [JsonProperty("required")]
+ public Optional IsRequired { get; set; }
+
+ [JsonProperty("values")]
+ public Optional Values { get; set; }
+
+ public FileUploadComponent() {}
+
+ public FileUploadComponent(Discord.FileUploadComponent component)
+ {
+ Type = component.Type;
+ Id = component.Id ?? Optional.Unspecified;
+ CustomId = component.CustomId;
+ MinValues = component.MinValues ?? Optional.Unspecified;
+ MaxValues = component.MaxValues ?? Optional.Unspecified;
+ IsRequired = component.IsRequired;
+ }
+
+ [JsonIgnore]
+ int? IMessageComponent.Id => Id.ToNullable();
+ IMessageComponentBuilder IMessageComponent.ToBuilder() => null;
+}
diff --git a/src/Discord.Net.Rest/API/Common/LabelComponent.cs b/src/Discord.Net.Rest/API/Common/LabelComponent.cs
new file mode 100644
index 0000000000..f27875e23a
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/LabelComponent.cs
@@ -0,0 +1,38 @@
+using Discord.Rest;
+using Newtonsoft.Json;
+
+namespace Discord.API;
+
+internal class LabelComponent : IMessageComponent
+{
+ [JsonProperty("type")]
+ public ComponentType Type { get; set; }
+
+ [JsonProperty("id")]
+ public Optional Id { get; }
+
+ [JsonProperty("label")]
+ public string Label { get; set; }
+
+ [JsonProperty("description")]
+ public string Description { get; set; }
+
+ [JsonProperty("component")]
+ public IMessageComponent Component { get; set; }
+
+ public LabelComponent() {}
+
+ public LabelComponent(Discord.LabelComponent label)
+ {
+ Type = label.Type;
+ Id = label.Id ?? Optional.Unspecified;
+ Label = label.Label;
+ Description = label.Description;
+ Component = label.Component.ToModel();
+ }
+
+ public IMessageComponentBuilder ToBuilder() => null;
+
+ [JsonIgnore]
+ int? IMessageComponent.Id => Id.ToNullable();
+}
diff --git a/src/Discord.Net.Rest/API/Common/ModalInteractionData.cs b/src/Discord.Net.Rest/API/Common/ModalInteractionData.cs
index 182fa53b22..2f816ee1b2 100644
--- a/src/Discord.Net.Rest/API/Common/ModalInteractionData.cs
+++ b/src/Discord.Net.Rest/API/Common/ModalInteractionData.cs
@@ -8,6 +8,9 @@ internal class ModalInteractionData : IDiscordInteractionData
public string CustomId { get; set; }
[JsonProperty("components")]
- public API.ActionRowComponent[] Components { get; set; }
+ public IMessageComponent[] Components { get; set; }
+
+ [JsonProperty("resolved")]
+ public Optional Resolved { get; set; }
}
}
diff --git a/src/Discord.Net.Rest/API/Common/ModalInteractionDataResolved.cs b/src/Discord.Net.Rest/API/Common/ModalInteractionDataResolved.cs
new file mode 100644
index 0000000000..67194e700f
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/ModalInteractionDataResolved.cs
@@ -0,0 +1,22 @@
+using Newtonsoft.Json;
+using System.Collections.Generic;
+
+namespace Discord.API;
+
+internal class ModalInteractionDataResolved
+{
+ [JsonProperty("users")]
+ public Optional> Users { get; set; }
+
+ [JsonProperty("members")]
+ public Optional> Members { get; set; }
+
+ [JsonProperty("roles")]
+ public Optional> Roles { get; set; }
+
+ [JsonProperty("channels")]
+ public Optional> Channels { get; set; }
+
+ [JsonProperty("attachments")]
+ public Optional> Attachments { get; set; }
+}
diff --git a/src/Discord.Net.Rest/API/Common/TextInputComponent.cs b/src/Discord.Net.Rest/API/Common/TextInputComponent.cs
index d76dcd50f6..3e96f26d05 100644
--- a/src/Discord.Net.Rest/API/Common/TextInputComponent.cs
+++ b/src/Discord.Net.Rest/API/Common/TextInputComponent.cs
@@ -16,8 +16,9 @@ internal class TextInputComponent : IInteractableComponent
[JsonProperty("custom_id")]
public string CustomId { get; set; }
+ // deprecated
[JsonProperty("label")]
- public string Label { get; set; }
+ public Optional Label { get; set; }
[JsonProperty("placeholder")]
public Optional Placeholder { get; set; }
diff --git a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs
index ef16c0f55c..b481a4eb21 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/CommandBase/RestCommandBase.cs
@@ -372,7 +372,7 @@ public override string RespondWithModal(Modal modal, RequestOptions options = nu
{
CustomId = modal.CustomId,
Title = modal.Title,
- Components = modal.Component.Components.Select(x => new Discord.API.ActionRowComponent(x)).ToArray()
+ Components = modal.Component.Components.Select(x => x.ToModel()).ToArray()
}
};
diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs
index 8748ef4a37..52dfad62a4 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs
@@ -508,7 +508,7 @@ public override string RespondWithModal(Modal modal, RequestOptions options = nu
{
CustomId = modal.CustomId,
Title = modal.Title,
- Components = modal.Component.Components.Select(x => new Discord.API.ActionRowComponent(x)).ToArray()
+ Components = modal.Component.Components.Select(x => x.ToModel()).ToArray()
}
};
diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs
index f72eceecb4..17d1d388b8 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs
@@ -99,7 +99,7 @@ internal RestMessageComponentData(IInteractableComponent component, BaseDiscordC
Type = component.Type;
if (component is API.TextInputComponent textInput)
- Value = textInput.Value.Value;
+ Value = textInput.Value.GetValueOrDefault();
if (component is API.SelectMenuComponent select)
{
@@ -129,6 +129,11 @@ internal RestMessageComponentData(IInteractableComponent component, BaseDiscordC
: null;
}
}
+
+ if (component is API.FileUploadComponent fileUpload)
+ {
+ Values = fileUpload.Values.GetValueOrDefault(null);
+ }
}
}
}
diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs
index 1831d4b51a..69fc6ac255 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.ModalInteractionData;
@@ -17,15 +18,84 @@ public class RestModalData : IModalInteractionData
///
public IReadOnlyCollection Components { get; }
+ ///
+ public IReadOnlyCollection Channels { get; }
+
+ ///
+ public IReadOnlyCollection Users { get; }
+
+ ///
+ public IReadOnlyCollection Roles { get; }
+
+ ///
+ public IReadOnlyCollection Members { get; }
+
+ ///
+ public IReadOnlyCollection Attachments { get; }
+
IReadOnlyCollection IModalInteractionData.Components => Components;
+ ///
+ IReadOnlyCollection IModalInteractionData.Channels => Channels;
+
+ ///
+ IReadOnlyCollection IModalInteractionData.Users => Users;
+
+ ///
+ IReadOnlyCollection IModalInteractionData.Roles => Roles;
+
+ ///
+ IReadOnlyCollection IModalInteractionData.Members => Members;
+
+ ///
+ IReadOnlyCollection IModalInteractionData.Attachments => Attachments;
+
internal RestModalData(Model model, BaseDiscordClient discord, IGuild guild)
{
CustomId = model.CustomId;
Components = model.Components
- .SelectMany(x => x.Components.OfType())
+ .SelectMany(c => c switch
+ {
+ Discord.API.ActionRowComponent row => row.Components, // Preserve the previous behavior
+ Discord.API.LabelComponent label => [label.Component],
+ _ => [c]
+ })
+ .OfType()
.Select(x => new RestMessageComponentData(x, discord, guild))
.ToArray();
+
+ if (model.Resolved.IsSpecified)
+ {
+ Users = model.Resolved.Value.Users.IsSpecified
+ ? model.Resolved.Value.Users.Value.Select(user => RestUser.Create(discord, user.Value)).ToImmutableArray()
+ : [];
+
+ Members = model.Resolved.Value.Members.IsSpecified
+ ? model.Resolved.Value.Members.Value.Select(member =>
+ {
+ member.Value.User = model.Resolved.Value.Users.Value.First(u => u.Key == member.Key).Value;
+
+ return RestGuildUser.Create(discord, guild, member.Value);
+ }).ToImmutableArray()
+ : [];
+
+ Channels = model.Resolved.Value.Channels.IsSpecified
+ ? model.Resolved.Value.Channels.Value.Select(channel =>
+ {
+ if (channel.Value.Type is ChannelType.DM)
+ return RestDMChannel.Create(discord, channel.Value);
+ return RestChannel.Create(discord, channel.Value);
+ }).ToImmutableArray()
+ : [];
+
+ Roles = model.Resolved.Value.Roles.IsSpecified
+ ? model.Resolved.Value.Roles.Value.Select(role => RestRole.Create(discord, guild, role.Value)).ToImmutableArray()
+ : [];
+
+ Attachments = model.Resolved.Value.Attachments.IsSpecified
+ ? model.Resolved.Value.Attachments.Value.Select(attachment => Attachment.Create(attachment.Value, discord)).ToImmutableArray()
+ : [];
+ }
}
}
}
diff --git a/src/Discord.Net.Rest/Extensions/MessageComponentExtension.cs b/src/Discord.Net.Rest/Extensions/MessageComponentExtension.cs
index 96893f7e4b..a65533b0ed 100644
--- a/src/Discord.Net.Rest/Extensions/MessageComponentExtension.cs
+++ b/src/Discord.Net.Rest/Extensions/MessageComponentExtension.cs
@@ -41,6 +41,12 @@ internal static IMessageComponent ToModel(this IMessageComponent component)
case ContainerComponent container:
return new API.ContainerComponent(container);
+
+ case LabelComponent label:
+ return new API.LabelComponent(label);
+
+ case FileUploadComponent fileUpload:
+ return new API.FileUploadComponent(fileUpload);
}
return null;
@@ -110,7 +116,7 @@ internal static IMessageComponent ToEntity(this IMessageComponent component)
{
var parsed = (API.TextInputComponent)component;
return new TextInputComponent(parsed.CustomId,
- parsed.Label,
+ parsed.Label.GetValueOrDefault(),
parsed.Placeholder.GetValueOrDefault(null),
parsed.MinLength.ToNullable(),
parsed.MaxLength.ToNullable(),
@@ -173,6 +179,22 @@ internal static IMessageComponent ToEntity(this IMessageComponent component)
parsed.Id.ToNullable());
}
+ case ComponentType.Label:
+ {
+ var parsed = (API.LabelComponent)component;
+ return new LabelComponent(parsed.Id.ToNullable(), parsed.Label, parsed.Description, parsed.Component.ToEntity());
+ }
+
+ case ComponentType.FileUpload:
+ {
+ var parsed = (API.FileUploadComponent)component;
+ return new FileUploadComponent(parsed.Id.ToNullable(),
+ parsed.CustomId,
+ parsed.MaxValues.ToNullable(),
+ parsed.MaxValues.ToNullable(),
+ parsed.IsRequired.GetValueOrDefault(false));
+ }
+
default:
return null;
}
diff --git a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs
index f72ce4d11a..22d34d41fb 100644
--- a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs
+++ b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs
@@ -62,6 +62,12 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
case ComponentType.Container:
messageComponent = new API.ContainerComponent();
break;
+ case ComponentType.Label:
+ messageComponent = new API.LabelComponent();
+ break;
+ case ComponentType.FileUpload:
+ messageComponent = new API.FileUploadComponent();
+ break;
default:
throw new JsonSerializationException($"Unknown component type value '{typeProperty}' while deserializing message component");
}
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs
index 984c898454..957accca24 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs
@@ -504,7 +504,7 @@ public override async Task RespondWithModalAsync(Modal modal, RequestOptions opt
{
CustomId = modal.CustomId,
Title = modal.Title,
- Components = modal.Component.Components.Select(x => new Discord.API.ActionRowComponent(x)).ToArray()
+ Components = modal.Component.Components.Select(x => x.ToModel()).ToArray()
}
};
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs
index 718cc3b130..5dff8dd70a 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs
@@ -1,6 +1,4 @@
using Discord.Rest;
-using Discord.Utils;
-using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -95,9 +93,8 @@ internal SocketMessageComponentData(IInteractableComponent component, DiscordSoc
CustomId = component.CustomId;
Type = component.Type;
- Value = component.Type == ComponentType.TextInput
- ? ((TextInputComponent)component).Value
- : null;
+ if (component is API.TextInputComponent textInput)
+ Value = textInput.Value.GetValueOrDefault();
if (component is API.SelectMenuComponent select)
{
@@ -132,6 +129,11 @@ internal SocketMessageComponentData(IInteractableComponent component, DiscordSoc
: null;
}
}
+
+ if (component is API.FileUploadComponent fileUpload)
+ {
+ Values = fileUpload.Values.GetValueOrDefault(null);
+ }
}
}
}
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs
index a778766677..d0f7444a81 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs
@@ -1,5 +1,6 @@
using Discord.Rest;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.ModalInteractionData;
@@ -20,13 +21,83 @@ public class SocketModalData : IModalInteractionData
///
public IReadOnlyCollection Components { get; }
+ ///
+ public IReadOnlyCollection Channels { get; }
+
+ ///
+ /// Returns if user is cached, otherwise.
+ public IReadOnlyCollection Users { get; }
+
+ ///
+ public IReadOnlyCollection Roles { get; }
+
+ ///
+ public IReadOnlyCollection Members { get; }
+
+ ///
+ public IReadOnlyCollection Attachments { get; }
+
+ ///
+ IReadOnlyCollection IModalInteractionData.Channels => Channels;
+
+ ///
+ IReadOnlyCollection IModalInteractionData.Users => Users;
+
+ ///
+ IReadOnlyCollection IModalInteractionData.Roles => Roles;
+
+ ///
+ IReadOnlyCollection IModalInteractionData.Members => Members;
+
+ ///
+ IReadOnlyCollection IModalInteractionData.Attachments => Attachments;
+
internal SocketModalData(Model model, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser)
{
CustomId = model.CustomId;
Components = model.Components
- .SelectMany(x => x.Components.Select(y => y.ToEntity()).OfType())
+ .SelectMany(c => c switch
+ {
+ Discord.API.ActionRowComponent row => row.Components, // Preserve the previous behavior
+ Discord.API.LabelComponent label => [label.Component],
+ _ => [c]
+ })
+ .OfType()
.Select(x => new SocketMessageComponentData(x, discord, state, guild, dmUser))
.ToArray();
+
+ if (model.Resolved.IsSpecified)
+ {
+ Users = model.Resolved.Value.Users.IsSpecified
+ ? model.Resolved.Value.Users.Value.Select(user => (IUser)state.GetUser(user.Value.Id) ?? RestUser.Create(discord, user.Value)).ToImmutableArray()
+ : [];
+
+ Members = model.Resolved.Value.Members.IsSpecified
+ ? model.Resolved.Value.Members.Value.Select(member =>
+ {
+ member.Value.User = model.Resolved.Value.Users.Value.First(u => u.Key == member.Key).Value;
+ return SocketGuildUser.Create(guild, state, member.Value);
+ }).ToImmutableArray()
+ : [];
+
+ Channels = model.Resolved.Value.Channels.IsSpecified
+ ? model.Resolved.Value.Channels.Value.Select(
+ channel =>
+ {
+ if (channel.Value.Type is ChannelType.DM)
+ return SocketDMChannel.Create(discord, state, channel.Value.Id, dmUser);
+ return (SocketChannel)SocketGuildChannel.Create(guild, state, channel.Value);
+ }).ToImmutableArray()
+ : [];
+
+ Roles = model.Resolved.Value.Roles.IsSpecified
+ ? model.Resolved.Value.Roles.Value.Select(role => SocketRole.Create(guild, state, role.Value)).ToImmutableArray()
+ : [];
+
+ Attachments = model.Resolved.Value.Attachments.IsSpecified
+ ? model.Resolved.Value.Attachments.Value.Select(attachment => Attachment.Create(attachment.Value, discord)).ToImmutableArray()
+ : [];
+ }
}
IReadOnlyCollection IModalInteractionData.Components => Components;
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs
index 075e4e31fa..914df8ac33 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs
@@ -161,7 +161,7 @@ public override async Task RespondWithModalAsync(Modal modal, RequestOptions opt
{
CustomId = modal.CustomId,
Title = modal.Title,
- Components = modal.Component.Components.Select(x => new Discord.API.ActionRowComponent(x)).ToArray()
+ Components = modal.Component.Components.Select(x => x.ToModel()).ToArray()
}
};