[Feature] Modal refactoring & select menu support (#3172)

* add missing xmldoc + new component type

* add API models

* label builder

* most POG commit ever

* another pog commit (add file upload component)

* add a constructor & missing extension methods

* refactor modal builder

* Fix build errors

* Add xml docs and fluent methods to new components

* Fix naming

* Set default value of `FileUploadComponentBuilder.IsRequired` to `true`

* xmldoc fixes

* Support receiving modal interactions with new components & implement resolved data

* Add missing text display methods to modal builder

* Update src/Discord.Net.Rest/API/Common/FileUploadComponent.cs

Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com>

---------

Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com>
Co-authored-by: d4n <dan3436@hotmail.com>
This commit is contained in:
Mihail Gribkov
2025-11-09 13:57:57 +03:00
committed by GitHub
parent ca6c9bcff7
commit a4760144a5
34 changed files with 1636 additions and 176 deletions

View File

@@ -9,6 +9,7 @@ namespace Discord;
/// </summary>
public class ButtonBuilder : IInteractableComponentBuilder
{
/// <inheritdoc />
public ComponentType Type => ComponentType.Button;
/// <summary>

View File

@@ -10,7 +10,7 @@ namespace Discord;
public class ComponentBuilder
{
/// <summary>
/// The max length of a <see cref="ButtonComponent.CustomId"/>.
/// The max length of <see cref="ButtonComponent.CustomId"/> and <see cref="SelectMenuComponent.CustomId"/>.
/// </summary>
public const int MaxCustomIdLength = 100;

View File

@@ -469,6 +469,54 @@ public static class ComponentContainerExtensions
return container.WithActionRow(cont);
}
/// <summary>
/// Adds a <see cref="FileUploadComponentBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithFileUpload<BuilderT>(this BuilderT container, FileUploadComponentBuilder fileUpload)
where BuilderT : class, IInteractableComponentContainer
{
container.AddComponent(fileUpload);
return container;
}
/// <summary>
/// Adds a <see cref="FileUploadComponentBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithFileUpload<BuilderT>(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));
/// <summary>
/// Adds a <see cref="FileUploadComponentBuilder"/> to the container.
/// </summary>
/// <returns>
/// The current container.
/// </returns>
public static BuilderT WithFileUpload<BuilderT>(this BuilderT container,
Action<FileUploadComponentBuilder> options)
where BuilderT : class, IInteractableComponentContainer
{
var comp = new FileUploadComponentBuilder();
options(comp);
return container.WithFileUpload(comp);
}
/// <summary>
/// Finds the first <see cref="IMessageComponentBuilder"/> in the <see cref="IComponentContainer"/>
/// or any of its child <see cref="IComponentContainer"/>s with matching id.

View File

@@ -2,6 +2,9 @@ using System;
namespace Discord;
/// <summary>
/// Represents a class used to build <see cref="FileComponent"/>'s.
/// </summary>
public class FileComponentBuilder : IMessageComponentBuilder
{
/// <inheritdoc />

View File

@@ -0,0 +1,192 @@
using System;
namespace Discord;
/// <summary>
/// Represents a class used to build <see cref="FileUploadComponent"/>'s.
/// </summary>
public class FileUploadComponentBuilder : IInteractableComponentBuilder
{
/// <summary>
/// The maximum number of values for the <see cref="FileUploadComponentBuilder.MinValues"/> and <see cref="FileUploadComponentBuilder.MaxValues"/> properties.
/// </summary>
public const int MaxFileCount = 10;
/// <inheritdoc/>
public ComponentType Type => ComponentType.FileUpload;
/// <inheritdoc />
public int? Id { get; set; }
/// <summary>
/// Gets or sets the custom id of the current file upload.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length exceeds <see cref="ModalComponentBuilder.MaxCustomIdLength"/>.</exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length subceeds 1.</exception>
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;
}
}
/// <summary>
/// Gets or sets the minimum number of items that must be uploaded (defaults to 1).
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="MinValues"/> exceeds <see cref="MaxFileCount"/>.</exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="MinValues"/> length subceeds 0.</exception>
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;
}
}
/// <summary>
/// Gets or sets the maximum number of items that can be uploaded (defaults to 1).
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="MaxValues"/> exceeds <see cref="MaxFileCount"/>.</exception>
public int? MaxValues
{
get => _maxValues;
set
{
if (value is not null)
{
Preconditions.AtMost(value.Value, MaxFileCount, nameof(MaxValues));
}
_maxValues = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether the current file upload requires files to be uploaded before submitting the modal (defaults to <see langword="true"></see>).
/// </summary>
public bool IsRequired { get; set; } = true;
/// <summary>
/// Sets the custom id of the current file upload.
/// </summary>
/// <param name="customId">The id to use for the current file upload.</param>
/// <inheritdoc cref="CustomId"/>
/// <returns>The current builder.</returns>
public FileUploadComponentBuilder WithCustomId(string customId)
{
CustomId = customId;
return this;
}
/// <summary>
/// Sets the minimum number of items that must be uploaded (defaults to 1).
/// </summary>
/// <param name="minValues">Sets the minimum number of items that must be uploaded.</param>
/// <inheritdoc cref="MinValues"/>
/// <returns>
/// The current builder.
/// </returns>
public FileUploadComponentBuilder WithMinValues(int? minValues)
{
MinValues = minValues;
return this;
}
/// <summary>
/// Sets the maximum number of items that can be uploaded (defaults to 1).
/// </summary>
/// <param name="maxValues">The maximum number of items that can be uploaded.</param>
/// <inheritdoc cref="MaxValues"/>
/// <returns>
/// The current builder.
/// </returns>
public FileUploadComponentBuilder WithMaxValues(int? maxValues)
{
MaxValues = maxValues;
return this;
}
/// <summary>
/// Sets whether the current file upload requires files to be uploaded before submitting the modal.
/// </summary>
/// <param name="isRequired">Whether the current file upload requires files to be uploaded before submitting the modal.</param>
/// <returns>
/// The current builder.
/// </returns>
public FileUploadComponentBuilder WithRequired(bool isRequired)
{
IsRequired = isRequired;
return this;
}
private string _customId;
private int? _minValues;
private int? _maxValues;
/// <summary>
/// Initializes a new instance of the <see cref="FileUploadComponentBuilder"/>.
/// </summary>
public FileUploadComponentBuilder() {}
/// <summary>
/// Initializes a new instance of the <see cref="FileUploadComponentBuilder"/>.
/// </summary>
/// <param name="customId">The custom id of the current file upload.</param>
/// <param name="minValues">The minimum number of items that must be uploaded (defaults to 1).</param>
/// <param name="maxValues">the maximum number of items that can be uploaded (defaults to 1).</param>
/// <param name="isRequired">Whether the current file upload requires files to be uploaded before submitting the modal.</param>
/// <param name="id">The id for the component.</param>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="FileUploadComponentBuilder"/> class from an existing <see cref="FileUploadComponent"/>.
/// </summary>
/// <param name="fileUpload">The component.</param>
public FileUploadComponentBuilder(FileUploadComponent fileUpload)
{
CustomId = fileUpload.CustomId;
MinValues = fileUpload.MinValues;
MaxValues = fileUpload.MaxValues;
IsRequired = fileUpload.IsRequired;
Id = fileUpload.Id;
}
/// <inheritdoc cref="IMessageComponentBuilder.Build" />
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);
}
/// <inheritdoc/>
IMessageComponent IMessageComponentBuilder.Build() => Build();
}

View File

@@ -0,0 +1,169 @@
using System;
using System.Collections.Immutable;
namespace Discord;
/// <summary>
/// Represents a class used to build <see cref="LabelComponent"/>'s.
/// </summary>
public class LabelBuilder : IMessageComponentBuilder
{
/// <inheritdoc cref="IComponentContainer.SupportedComponentTypes"/>
public ImmutableArray<ComponentType> SupportedComponentTypes { get; } =
[
ComponentType.SelectMenu,
ComponentType.TextInput,
ComponentType.UserSelect,
ComponentType.RoleSelect,
ComponentType.MentionableSelect,
ComponentType.ChannelSelect,
ComponentType.FileUpload
];
/// <summary>
/// The maximum length of the label.
/// </summary>
public const int MaxLabelLength = 45;
/// <summary>
/// The maximum length of the description.
/// </summary>
public const int MaxDescriptionLength = 100;
/// <inheritdoc />
public ComponentType Type => ComponentType.Label;
/// <inheritdoc />
public int? Id { get; set; }
/// <summary>
/// Gets or sets the label text.
/// </summary>
public string Label
{
get => _label;
set
{
if (value is not null)
{
Preconditions.AtMost(value.Length, MaxLabelLength, nameof(Label));
}
_label = value;
}
}
/// <summary>
/// Gets or sets the description text for the label.
/// </summary>
public string Description
{
get => _description;
set
{
if (value is not null)
{
Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description));
}
_description = value;
}
}
/// <summary>
/// Gets or sets the component within the label.
/// </summary>
public IMessageComponentBuilder Component { get; set; }
/// <summary>
/// Sets the label text.
/// </summary>
/// <param name="label">The label text.</param>
/// <returns>
/// The current builder.
/// </returns>
public LabelBuilder WithLabel(string label)
{
Label = label;
return this;
}
/// <summary>
/// Sets the description text for the label.
/// </summary>
/// <param name="description">The description text for the label.</param>
/// <returns>
/// The current builder.
/// </returns>
public LabelBuilder WithDescription(string description)
{
Description = description;
return this;
}
/// <summary>
/// Sets the component within the label.
/// </summary>
/// <param name="component">The component within the label.</param>
/// <returns>
/// The current builder.
/// </returns>
public LabelBuilder WithComponent(IMessageComponentBuilder component)
{
Component = component;
return this;
}
private string _label;
private string _description;
/// <summary>
/// Initializes a new <see cref="LabelBuilder"/>.
/// </summary>
public LabelBuilder() { }
/// <summary>
/// Initializes a new <see cref="LabelBuilder"/> with the specified content.
/// </summary>
/// <param name="label">The label text.</param>
/// <param name="component">The component within the label.</param>
/// <param name="description">The description text for the label.</param>
/// <param name="id">The id for the component.</param>
public LabelBuilder(string label, IMessageComponentBuilder component, string description = null, int? id = null)
{
Id = id;
Label = label;
Component = component;
Description = description;
}
/// <summary>
/// Initializes a new <see cref="LabelBuilder"/> from existing component.
/// </summary>
public LabelBuilder(LabelComponent label)
{
Label = label.Label;
Description = label.Description;
Id = label.Id;
Component = label.Component.ToBuilder();
}
/// <inheritdoc cref="IMessageComponentBuilder.Build" />
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());
}
/// <inheritdoc />
IMessageComponent IMessageComponentBuilder.Build() => Build();
}

View File

@@ -18,7 +18,7 @@ public class MediaGalleryBuilder : IMessageComponentBuilder
/// <inheritdoc/>
public int? Id { get; set; }
private List<MediaGalleryItemProperties> _items = new();
private List<MediaGalleryItemProperties> _items = [];
/// <summary>
/// Initializes a new instance of the <see cref="MediaGalleryBuilder"/>.

View File

@@ -2,6 +2,9 @@ using System;
namespace Discord;
/// <summary>
/// Represents a class used to build <see cref="TextDisplayComponent"/>'s.
/// </summary>
public class TextDisplayBuilder : IMessageComponentBuilder
{
/// <summary>

View File

@@ -14,12 +14,16 @@ public class TextInputBuilder : IInteractableComponentBuilder
/// The max length of a <see cref="TextInputComponent.Placeholder"/>.
/// </summary>
public const int MaxPlaceholderLength = 100;
/// <summary>
/// The max value for <see cref="TextInputBuilder.MaxLength"/> and <see cref="TextInputBuilder.MinLength"/>, and the max length for <see cref="TextInputBuilder.Value"/>.
/// </summary>
public const int LargestMaxLength = 4000;
/// <summary>
/// Gets or sets the custom id of the current text input.
/// </summary>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length exceeds <see cref="ComponentBuilder.MaxCustomIdLength"/></exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length exceeds <see cref="ModalComponentBuilder.MaxCustomIdLength"/>.</exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="CustomId"/> length subceeds 1.</exception>
public string CustomId
{
@@ -29,7 +33,7 @@ public class TextInputBuilder : IInteractableComponentBuilder
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 class TextInputBuilder : IInteractableComponentBuilder
/// <summary>
/// Gets or sets the label of the current text input.
/// </summary>
[Obsolete("Label is no longer supported", error: false)]
public string Label { get; set; }
/// <summary>
@@ -55,7 +60,8 @@ public class TextInputBuilder : IInteractableComponentBuilder
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}\"");
}
/// <summary>
@@ -72,7 +78,8 @@ public class TextInputBuilder : IInteractableComponentBuilder
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 class TextInputBuilder : IInteractableComponentBuilder
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 class TextInputBuilder : IInteractableComponentBuilder
/// </summary>
public bool? Required { get; set; }
/// <inheritdoc/>
public int? Id { get; set; }
/// <summary>
@@ -123,9 +133,11 @@ public class TextInputBuilder : IInteractableComponentBuilder
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 class TextInputBuilder : IInteractableComponentBuilder
/// <summary>
/// Creates a new instance of a <see cref="TextInputBuilder"/>.
/// </summary>
/// <param name="label">The text input's label.</param>
/// <param name="style">The text input's style.</param>
/// <param name="customId">The text input's custom id.</param>
/// <param name="placeholder">The text input's placeholder.</param>
/// <param name="minLength">The text input's minimum length.</param>
/// <param name="maxLength">The text input's maximum length.</param>
/// <param name="required">The text input's required value.</param>
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)
/// <param name="value">The text input's default value.</param>
/// <param name="id">The id for the component.</param>
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;
@@ -161,12 +181,37 @@ public class TextInputBuilder : IInteractableComponentBuilder
Id = id;
}
/// <summary>
/// Creates a new instance of a <see cref="TextInputBuilder"/>.
/// </summary>
/// <param name="label">The text input's label.</param>
/// <param name="style">The text input's style.</param>
/// <param name="customId">The text input's custom id.</param>
/// <param name="placeholder">The text input's placeholder.</param>
/// <param name="minLength">The text input's minimum length.</param>
/// <param name="maxLength">The text input's maximum length.</param>
/// <param name="required">The text input's required value.</param>
[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;
}
/// <summary>
/// Creates a new instance of a <see cref="TextInputBuilder"/>.
/// </summary>
public TextInputBuilder()
{
}
/// <summary>
@@ -174,7 +219,9 @@ public class TextInputBuilder : IInteractableComponentBuilder
/// </summary>
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 class TextInputBuilder : IInteractableComponentBuilder
/// </summary>
/// <param name="label">The value to set.</param>
/// <returns>The current builder. </returns>
[Obsolete("Label is no longer supported", error: false)]
public TextInputBuilder WithLabel(string label)
{
Label = label;
@@ -273,16 +321,19 @@ public class TextInputBuilder : IInteractableComponentBuilder
return this;
}
/// <inheritdoc cref="IMessageComponentBuilder.Build" />
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));
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));
#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();

View File

@@ -45,18 +45,49 @@ namespace Discord
/// </summary>
ChannelSelect = 8,
/// <summary>
/// A container to display text alongside an accessory component.
/// </summary>
Section = 9,
/// <summary>
/// A component displaying Markdown text.
/// </summary>
TextDisplay = 10,
/// <summary>
/// A small image that can be used as an accessory.
/// </summary>
Thumbnail = 11,
/// <summary>
/// A component displaying images and other media.
/// </summary>
MediaGallery = 12,
/// <summary>
/// A component displaying an attached file.
/// </summary>
File = 13,
/// <summary>
/// A component to add vertical padding between other components.
/// </summary>
Separator = 14,
/// <summary>
/// A container that visually groups a set of components.
/// </summary>
Container = 17,
/// <summary>
/// A layout component that wraps modal components (text input, select menu or file upload) with a label and description.
/// </summary>
Label = 18,
/// <summary>
/// A component that allows users to upload files in modals.
/// </summary>
FileUpload = 19
}
}

View File

@@ -0,0 +1,51 @@
namespace Discord;
/// <summary>
/// Represents a component that allows users to upload files in modals.
/// </summary>
public class FileUploadComponent : IInteractableComponent
{
/// <inheritdoc/>
public ComponentType Type => ComponentType.FileUpload;
/// <summary>
/// Gets the ID of this component.
/// </summary>
public int? Id { get; }
/// <summary>
/// Gets the custom ID of this component.
/// </summary>
public string CustomId { get; }
/// <summary>
/// Gets the minimum number of files a user must upload.
/// </summary>
public int? MinValues { get; }
/// <summary>
/// Gets the maximum number of files a user can upload.
/// </summary>
public int? MaxValues { get; }
/// <summary>
/// Gets whether this component requires a file upload to be submitted.
/// </summary>
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;
}
/// <inheritdoc cref="IMessageComponent.ToBuilder"/>
public FileUploadComponentBuilder ToBuilder()
=> new(this);
/// <inheritdoc/>
IMessageComponentBuilder IMessageComponent.ToBuilder() => ToBuilder();
}

View File

@@ -11,7 +11,7 @@ public interface IMessageComponent
ComponentType Type { get; }
/// <summary>
///
/// Gets the id for the component.
/// </summary>
int? Id { get; }

View File

@@ -0,0 +1,40 @@
namespace Discord;
/// <summary>
/// Represents a layout component that wraps modal components (text input, select menu or file upload) with a label and description.
/// </summary>
public class LabelComponent : IMessageComponent
{
/// <inheritdoc />
public ComponentType Type => ComponentType.Label;
/// <inheritdoc />
public int? Id { get; }
/// <summary>
/// Gets the label text.
/// </summary>
public string Label { get; }
/// <summary>
/// Gets the description text for the label.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the component within the label.
/// </summary>
public IMessageComponent Component { get; }
internal LabelComponent(int? id, string label, string description, IMessageComponent component)
{
Id = id;
Label = label;
Description = description;
Component = component;
}
/// <inheritdoc />
public IMessageComponentBuilder ToBuilder()
=> new LabelBuilder(this);
}

View File

@@ -49,15 +49,23 @@ public class TextInputComponent : IInteractableComponent
/// </summary>
public string Value { get; }
/// <summary>
/// Converts a <see cref="TextInputComponent"/> to a <see cref="TextInputBuilder"/>.
/// </summary>
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;

View File

@@ -16,5 +16,30 @@ namespace Discord
/// Gets the <see cref="Modal"/> components submitted by the user.
/// </summary>
IReadOnlyCollection<IComponentInteractionData> Components { get; }
/// <summary>
/// Gets the channels(s) of a <see cref="ComponentType.ChannelSelect"/> component within the modal.
/// </summary>
IReadOnlyCollection<IChannel> Channels { get; }
/// <summary>
/// Gets the user(s) of a <see cref="ComponentType.UserSelect"/> or <see cref="ComponentType.MentionableSelect"/> component within the modal.
/// </summary>
IReadOnlyCollection<IUser> Users { get; }
/// <summary>
/// Gets the roles(s) of a <see cref="ComponentType.RoleSelect"/> or <see cref="ComponentType.MentionableSelect"/> component within the modal.
/// </summary>
IReadOnlyCollection<IRole> Roles { get; }
/// <summary>
/// Gets the guild member(s) of a <see cref="ComponentType.UserSelect"/> or <see cref="ComponentType.MentionableSelect"/> component within the modal.
/// </summary>
IReadOnlyCollection<IGuildUser> Members { get; }
/// <summary>
/// Gets the attachment(s) of a <see cref="ComponentType.FileUpload"/> component within the modal.
/// </summary>
IReadOnlyCollection<IAttachment> Attachments { get; }
}
}

View File

@@ -10,7 +10,9 @@ namespace Discord
/// </summary>
public string Title { get; set; }
/// <inheritdoc/>
/// <summary>
/// Gets the custom id of the modal.
/// </summary>
public string CustomId { get; set; }
/// <summary>

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -11,7 +12,13 @@ namespace Discord
{
private string _customId;
public ModalBuilder() { }
/// <summary>
/// Creates a new and empty <see cref="ModalBuilder"/>.
/// </summary>
public ModalBuilder()
{
Components = new();
}
/// <summary>
/// Creates a new instance of the <see cref="ModalBuilder"/>.
@@ -19,7 +26,6 @@ namespace Discord
/// <param name="title">The modal's title.</param>
/// <param name="customId">The modal's customId.</param>
/// <param name="components">The modal's components.</param>
/// <exception cref="ArgumentException">Only TextInputComponents are allowed.</exception>
public ModalBuilder(string title, string customId, ModalComponentBuilder components = null)
{
Title = title;
@@ -27,6 +33,28 @@ namespace Discord
Components = components ?? new();
}
/// <summary>
/// Creates a new instance of the <see cref="ModalBuilder"/>.
/// </summary>
/// <param name="title">The modal's title.</param>
/// <param name="customId">The modal's customId.</param>
/// <param name="components">The modal's components.</param>
public ModalBuilder(string title, string customId, params IEnumerable<IMessageComponentBuilder> components)
: this(title, customId, new ModalComponentBuilder(components))
{
}
/// <summary>
/// Creates a new instance of the <see cref="ModalBuilder"/>.
/// </summary>
/// <param name="title">The modal's title.</param>
/// <param name="customId">The modal's customId.</param>
/// <param name="components">The modal's components.</param>
public ModalBuilder(string title, string customId, params IEnumerable<IMessageComponent> components)
: this(title, customId, new ModalComponentBuilder(components))
{
}
/// <summary>
/// Gets or sets the title of the current modal.
/// </summary>
@@ -43,7 +71,7 @@ namespace Discord
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 @@ namespace Discord
/// <summary>
/// Gets or sets the components of the current modal.
/// </summary>
public ModalComponentBuilder Components { get; set; } = new();
public ModalComponentBuilder Components { get; set; }
/// <summary>
/// Sets the title of the current modal.
@@ -83,52 +111,227 @@ namespace Discord
/// <param name="component">The component to add.</param>
/// <param name="row">The row to add the text input.</param>
/// <returns>The current builder.</returns>
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;
}
/// <summary>
/// Adds a <see cref="TextInputBuilder"/> to the current builder.
/// </summary>
/// <param name="customId">The input's custom id.</param>
/// <param name="label">The input's label.</param>
/// <param name="placeholder">The input's placeholder text.</param>
/// <param name="minLength">The input's minimum length.</param>
/// <param name="maxLength">The input's maximum length.</param>
/// <param name="style">The input's style.</param>
/// <returns>The current builder.</returns>
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));
/// <inheritdoc
/// cref="ModalComponentBuilder.WithTextInput(string, string, TextInputStyle, string, int?, int?, int, bool?, string, int?, string, int?)"
/// />
/// <returns>The current <see cref="ModalBuilder"/>.</returns>
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;
}
/// <inheritdoc cref="ModalComponentBuilder.WithLabel(LabelBuilder)"/>
/// <returns>The current <see cref="ModalBuilder"/>.</returns>
public ModalBuilder AddLabel(LabelBuilder label)
{
Components.WithLabel(label);
return this;
}
/// <inheritdoc cref="ModalComponentBuilder.WithLabel(string, IMessageComponentBuilder, string, int?)"/>
/// <returns>The current <see cref="ModalBuilder"/>.</returns>
public ModalBuilder AddLabel(
string label,
IMessageComponentBuilder component,
string description = null,
int? id = null
)
{
Components.WithLabel(label, component, description, id);
return this;
}
/// <inheritdoc cref="ModalComponentBuilder.WithSelectMenu(string, string, List{SelectMenuOptionBuilder}, string, int, int, bool, ComponentType, ChannelType[], int?, string, int?)"/>
/// <returns>The current <see cref="ModalBuilder"/>.</returns>
public ModalBuilder AddSelectMenu(
string label,
string customId,
List<SelectMenuOptionBuilder> 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;
}
/// <inheritdoc cref="ModalComponentBuilder.WithSelectMenu(string, SelectMenuBuilder, string, int?)"/>
/// <returns>The current <see cref="ModalBuilder"/>.</returns>
public ModalBuilder AddSelectMenu(
string label,
SelectMenuBuilder menu,
string description = null,
int? labelId = null
)
{
Components.WithSelectMenu(label, menu, description, labelId);
return this;
}
/// <inheritdoc cref="ModalComponentBuilder.WithFileUpload(string, FileUploadComponentBuilder, string, int?)"/>
/// <returns>The current <see cref="ModalBuilder"/>.</returns>
public ModalBuilder AddFileUpload(
string label,
FileUploadComponentBuilder fileUpload,
string description = null,
int? labelId = null
)
{
Components.WithFileUpload(label, fileUpload, description, labelId);
return this;
}
/// <inheritdoc cref="ModalComponentBuilder.WithFileUpload(string, string, int?, int?, bool, int?, string, int?)"/>
/// <returns>The current <see cref="ModalBuilder"/>.</returns>
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;
}
/// <inheritdoc cref="ModalComponentBuilder.WithTextDisplay(TextDisplayBuilder)"/>
/// <returns>The current <see cref="ModalBuilder"/>.</returns>
public ModalBuilder AddTextDisplay(TextDisplayBuilder textDisplay)
{
Components.WithTextDisplay(textDisplay);
return this;
}
/// <inheritdoc cref="ModalComponentBuilder.WithTextDisplay(string, int?)"/>
/// <returns>The current <see cref="ModalBuilder"/>.</returns>
public ModalBuilder AddTextDisplay(string content, int? id = null)
{
Components.WithTextDisplay(content, id);
return this;
}
/// <summary>
/// Adds multiple components to the current builder.
/// </summary>
/// <param name="components">The components to add.</param>
/// <returns>The current builder</returns>
[Obsolete("Modal components no longer have rows", error: false)]
public ModalBuilder AddComponents(List<IMessageComponent> components, int row)
{
components.ForEach(x => Components.AddComponent(x, row));
return this;
}
/// <summary>
/// Adds multiple components to the current builder.
/// </summary>
/// <param name="components">The components to add.</param>
/// <returns>The current builder</returns>
public ModalBuilder AddComponents(params IEnumerable<IMessageComponentBuilder> components)
{
Components.With(components);
return this;
}
/// <summary>
/// Gets a <see cref="IInteractableComponentBuilder"/> by the specified <paramref name="customId"/>.
/// </summary>
/// <param name="customId">
/// The <see cref="IInteractableComponentBuilder.CustomId"/> of the component to get.
/// </param>
/// <returns>
/// The component that was found, <see langword="null"/> otherwise.
/// </returns>
public IInteractableComponentBuilder GetComponent(string customId) =>
GetComponent<IInteractableComponentBuilder>(customId);
/// <summary>
/// Gets a <typeparamref name="TMessageComponentBuilder"/> by the specified <paramref name="customId"/>.
/// </summary>
/// <typeparam name="TMessageComponentBuilder">The type of the component to get.</typeparam>
/// <param name="customId">The <see cref="IInteractableComponentBuilder.CustomId"/> of the component to get.</param>
/// <param name="customId">
/// The <see cref="IInteractableComponentBuilder.CustomId"/> of the component to get.
/// </param>
/// <returns>
/// The component of type <typeparamref name="TMessageComponentBuilder"/> that was found, <see langword="null"/> otherwise.
/// The component of type <typeparamref name="TMessageComponentBuilder"/> that was found,
/// <see langword="null"/> otherwise.
/// </returns>
public TMessageComponentBuilder GetComponent<TMessageComponentBuilder>(string customId)
where TMessageComponentBuilder : class, IInteractableComponentBuilder
{
Preconditions.NotNull(customId, nameof(customId));
return Components.ActionRows?.SelectMany(r => r.Components.OfType<TMessageComponentBuilder>())
.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<TMessageComponentBuilder>();
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<IInteractableComponentBuilder> ExtractComponent(IMessageComponentBuilder builder)
=> builder switch
{
LabelBuilder { Component: IInteractableComponentBuilder target } => [target],
ActionRowBuilder { Components: { } components }
=> components.OfType<IInteractableComponentBuilder>(),
_ => []
};
}
/// <summary>
@@ -144,25 +347,21 @@ namespace Discord
{
Preconditions.NotNull(customId, nameof(customId));
var component = GetComponent<TextInputBuilder>(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<TextInputBuilder>(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
};
/*
* 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.
*/
updateTextInput(builder);
row.Components.Remove(component);
row.AddComponent(builder);
updateTextInput(component);
return this;
}
@@ -188,7 +387,35 @@ namespace Discord
{
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 @@ namespace Discord
/// <returns>The current builder.</returns>
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 @@ namespace Discord
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 @@ namespace Discord
/// <summary>
/// Represents a builder for creating a <see cref="ModalComponent"/>.
/// </summary>
public class ModalComponentBuilder
public class ModalComponentBuilder : IList<IMessageComponentBuilder>
{
/// <summary>
/// The max length of a <see cref="IInteractableComponent.CustomId"/>.
@@ -236,126 +465,448 @@ namespace Discord
/// <summary>
/// The max amount of rows a <see cref="ModalComponent"/> can have.
/// </summary>
[Obsolete("Modal components no longer support action rows", error: true)]
public const int MaxActionRowCount = 5;
/// <summary>
/// Gets or sets the Action Rows for this Component Builder.
/// Gets the number of components in the builder.
/// </summary>
/// <exception cref="ArgumentNullException" accessor="set"><see cref="ActionRows"/> cannot be null.</exception>
/// <exception cref="ArgumentException" accessor="set"><see cref="ActionRows"/> count exceeds <see cref="MaxActionRowCount"/>.</exception>
public List<ActionRowBuilder> ActionRows
public int Count => _components.Count;
/// <summary>
/// Gets or sets the component at the specified index.
/// </summary>
/// <param name="index">The index of the component to get or set</param>
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<ActionRowBuilder> _actionRows;
private readonly List<IMessageComponentBuilder> _components;
/// <summary>
/// Constructs an empty <see cref="ModalComponentBuilder"/>.
/// </summary>
public ModalComponentBuilder()
{
_components = [];
}
/// <summary>
/// Constructs a <see cref="ModalComponentBuilder"/> with the provided
/// <see cref="IMessageComponentBuilder"/>s.
/// </summary>
/// <param name="components">The components to add to this <see cref="ModalComponentBuilder"/></param>
public ModalComponentBuilder(params IEnumerable<IMessageComponentBuilder> components) : this()
{
foreach (var component in components)
{
Add(component);
}
}
/// <summary>
/// Constructs a <see cref="ModalComponentBuilder"/> with the provided
/// <see cref="IMessageComponent"/>s.
/// </summary>
/// <param name="components">The components to add to this <see cref="ModalComponentBuilder"/></param>
public ModalComponentBuilder(params IEnumerable<IMessageComponent> components) : this()
{
foreach (var component in components)
{
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}."
);
}
/// <summary>
/// Creates a new builder from the provided list of components.
/// </summary>
/// <param name="components">The components to create the builder from.</param>
/// <returns>The newly created builder.</returns>
public static ComponentBuilder FromComponents(IReadOnlyCollection<IMessageComponent> components)
public static ModalComponentBuilder FromComponents(params IEnumerable<IMessageComponent> components)
{
var builder = new ComponentBuilder();
for (int i = 0; i != components.Count; i++)
{
var component = components.ElementAt(i);
builder.AddComponent(component, i);
}
var builder = new ModalComponentBuilder();
foreach (var component in components)
builder.Add(component);
return builder;
}
internal void AddComponent(IMessageComponent component, int row)
[Obsolete("Modal components no longer have rows", error: true)]
internal ModalComponentBuilder AddComponent(IMessageComponent component, int row)
=> Add(component);
/// <summary>
/// Adds a component to this <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="component">The component to add.</param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
public ModalComponentBuilder Add(IMessageComponent component)
=> Add(component.ToBuilder());
/// <summary>
/// Adds a component to this <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="component">The component to add.</param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
public ModalComponentBuilder Add(IMessageComponentBuilder component)
{
switch (component)
{
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;
}
ValidateComponentBuilder(component);
_components.Add(component);
return this;
}
/// <summary>
/// Adds a <see cref="TextInputBuilder"/> to the <see cref="ComponentBuilder"/> at the specific row.
/// If the row cannot accept the component then it will add it to a row that can.
/// Sets the components in this builder to the provided <paramref name="components"/>
/// </summary>
/// <param name="customId">The input's custom id.</param>
/// <param name="label">The input's label.</param>
/// <param name="placeholder">The input's placeholder text.</param>
/// <param name="minLength">The input's minimum length.</param>
/// <param name="maxLength">The input's maximum length.</param>
/// <param name="style">The input's style.</param>
/// <returns>The current builder.</returns>
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);
/// <param name="components">The components to set this builder to.</param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
public ModalComponentBuilder With(params IEnumerable<IMessageComponentBuilder> components)
{
_components.Clear();
/// <summary>
/// Adds a <see cref="TextInputBuilder"/> to the <see cref="ModalComponentBuilder"/> at the specific row.
/// If the row cannot accept the component then it will add it to a row that can.
/// </summary>
/// <param name="text">The <see cref="TextInputBuilder"/> to add.</param>
/// <param name="row">The row to add the text input.</param>
/// <exception cref="InvalidOperationException">There are no more rows to add a text input to.</exception>
/// <exception cref="ArgumentException"><paramref name="row"/> must be less than <see cref="MaxActionRowCount"/>.</exception>
/// <returns>The current builder.</returns>
public ModalComponentBuilder WithTextInput(TextInputBuilder text, int row = 0)
{
Preconditions.LessThan(row, MaxActionRowCount, nameof(row));
if (_actionRows == null)
{
_actionRows = new List<ActionRowBuilder>
{
new ActionRowBuilder().AddComponent(text)
};
}
else
{
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.");
}
}
foreach (var component in components)
Add(component);
return this;
}
/// <summary>
/// Adds a <see cref="LabelBuilder"/> to the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="label">The <see cref="LabelBuilder"/> to add.</param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
public ModalComponentBuilder WithLabel(LabelBuilder label)
=> Add(label);
/// <summary>
/// Constructs and adds a <see cref="LabelBuilder"/> to the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="label">The label of the <see cref="LabelBuilder"/>.</param>
/// <param name="component">The component of the <see cref="LabelBuilder"/>.</param>
/// <param name="description">The description of the <see cref="LabelBuilder"/>.</param>
/// <param name="id">The id of the <see cref="LabelBuilder"/>.</param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
public ModalComponentBuilder WithLabel(
string label,
IMessageComponentBuilder component,
string description = null,
int? id = null
) => WithLabel(new(
label,
component,
description,
id
));
/// <summary>
/// Constructs and adds a <see cref="LabelBuilder"/> containing a <see cref="SelectMenuBuilder"/> to the
/// current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="label">The label around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="customId">The custom id of the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="options">The options of the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="placeholder">The placeholder of the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="minValues">The min values of the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="maxValues">The max values of the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="disabled">Whether the <see cref="SelectMenuBuilder"/> is disabled.</param>
/// <param name="type">The type of the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="channelTypes">The channel types of the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="id">The id of the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="description">The description around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="labelId">
/// The id of the <see cref="LabelBuilder"/> wrapping the <see cref="SelectMenuBuilder"/>.
/// </param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
public ModalComponentBuilder WithSelectMenu(
string label,
string customId,
List<SelectMenuOptionBuilder> 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
);
/// <summary>
/// Constructs and adds a <see cref="LabelBuilder"/> with the provided <see cref="SelectMenuBuilder"/> to
/// the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="label">The label around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="menu">The menu to add.</param>
/// <param name="description">The description around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="labelId">
/// The id of the <see cref="LabelBuilder"/> wrapping the <see cref="SelectMenuBuilder"/>.
/// </param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
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
);
}
/// <summary>
/// Constructs and adds a <see cref="LabelBuilder"/> with the provided
/// <see cref="FileUploadComponentBuilder"/> to the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="label">The label around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="fileUpload">The file upload to add.</param>
/// <param name="description">The description around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="labelId">
/// The id of the <see cref="LabelBuilder"/> wrapping the <see cref="SelectMenuBuilder"/>.
/// </param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
public ModalComponentBuilder WithFileUpload(
string label,
FileUploadComponentBuilder fileUpload,
string description = null,
int? labelId = null
) => WithLabel(label, fileUpload, description, labelId);
/// <summary>
/// Constructs and adds a <see cref="LabelBuilder"/> with a <see cref="FileUploadComponentBuilder"/>
/// to the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="label">The label around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="customId">The custom id of the <see cref="FileUploadComponentBuilder"/>.</param>
/// <param name="minValues">The min values of the <see cref="FileUploadComponentBuilder"/>.</param>
/// <param name="maxValues">The max values of the <see cref="FileUploadComponentBuilder"/>.</param>
/// <param name="isRequired">Whether the <see cref="FileUploadComponentBuilder"/> is required.</param>
/// <param name="id">The id of the <see cref="FileUploadComponentBuilder"/>.</param>
/// <param name="description">The description around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="labelId">
/// The id of the <see cref="LabelBuilder"/> wrapping the <see cref="SelectMenuBuilder"/>.
/// </param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
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
);
/// <summary>
/// Adds a <see cref="TextDisplayBuilder"/> to the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="textDisplay">The <see cref="TextDisplayBuilder"/> to add.</param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
public ModalComponentBuilder WithTextDisplay(TextDisplayBuilder textDisplay)
=> Add(textDisplay);
/// <summary>
/// Constructs and adds a <see cref="TextDisplayBuilder"/> to the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="content">The content of the <see cref="TextDisplayBuilder"/>.</param>
/// <param name="id">The id of the <see cref="TextDisplayBuilder"/>.</param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
public ModalComponentBuilder WithTextDisplay(string content, int? id = null)
=> WithTextDisplay(new TextDisplayBuilder(content, id));
/// <summary>
/// Constructs and adds a <see cref="LabelBuilder"/> with the provided <see cref="TextInputBuilder"/> to
/// the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="label">The label around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="textInput">The text input to add.</param>
/// <param name="description">The description around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="labelId">
/// The id of the <see cref="LabelBuilder"/> wrapping the <see cref="SelectMenuBuilder"/>.
/// </param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
public ModalComponentBuilder WithTextInput(
string label,
TextInputBuilder textInput,
string description = null,
int? labelId = null
) => WithLabel(label, textInput, description, labelId);
/// <summary>
/// Constructs and adds a <see cref="LabelBuilder"/> with the provided <see cref="TextInputBuilder"/> to
/// the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="text">The text input to add.</param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
[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)
{
// TODO: better explain
throw new ArgumentNullException(
nameof(text),
"Label cannot be null"
);
}
return WithLabel(
text.Label,
text
);
#pragma warning restore CS0618 // Type or member is obsolete
}
/// <summary>
/// Constructs and adds a <see cref="LabelBuilder"/> with the provided <see cref="TextInputBuilder"/> to
/// the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="text">The text input to add.</param>
/// <param name="row">The row to add the text input to.</param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
[Obsolete("Modal components no longer have rows", error: false)]
public ModalComponentBuilder WithTextInput(TextInputBuilder text, int row)
=> WithTextInput(text);
/// <summary>
/// Constructs and adds a <see cref="LabelBuilder"/> with a <see cref="TextInputBuilder"/>
/// to the current <see cref="ModalComponentBuilder"/>.
/// </summary>
/// <param name="label">The label around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="customId">The custom id of the <see cref="TextInputBuilder"/>.</param>
/// <param name="style">The style of the <see cref="TextInputBuilder"/>.</param>
/// <param name="placeholder">The placeholder of the <see cref="TextInputBuilder"/>.</param>
/// <param name="minLength">The min length of the <see cref="TextInputBuilder"/>.</param>
/// <param name="maxLength">The max length of the <see cref="TextInputBuilder"/>.</param>
/// <param name="row"><b>DEPRECATED:</b> The row to place the <see cref="TextInputBuilder"/> on.</param>
/// <param name="required">Whether the <see cref="TextInputBuilder"/> is required.</param>
/// <param name="value">The value of the <see cref="TextInputBuilder"/>.</param>
/// <param name="id">The id of the <see cref="TextInputBuilder"/>.</param>
/// <param name="description">The description around the <see cref="SelectMenuBuilder"/>.</param>
/// <param name="labelId">
/// The id of the <see cref="LabelBuilder"/> wrapping the <see cref="SelectMenuBuilder"/>.
/// </param>
/// <returns>The current <see cref="ModalComponentBuilder"/>.</returns>
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
);
/// <inheritdoc />
void ICollection<IMessageComponentBuilder>.Add(IMessageComponentBuilder item) => Add(item);
/// <inheritdoc />
public void Clear() => _components.Clear();
/// <inheritdoc />
public bool Contains(IMessageComponentBuilder item) => _components.Contains(item);
/// <inheritdoc />
public void CopyTo(IMessageComponentBuilder[] array, int arrayIndex) => _components.CopyTo(array, arrayIndex);
/// <inheritdoc />
public bool Remove(IMessageComponentBuilder item) => _components.Remove(item);
/// <inheritdoc />
public int IndexOf(IMessageComponentBuilder item) => _components.IndexOf(item);
/// <inheritdoc />
public void Insert(int index, IMessageComponentBuilder item)
{
ValidateComponentBuilder(item);
_components.Insert(index, item);
}
/// <inheritdoc />
public void RemoveAt(int index) => _components.RemoveAt(index);
/// <inheritdoc />
public IEnumerator<IMessageComponentBuilder> GetEnumerator() => _components.GetEnumerator();
/// <summary>
/// Get a <see cref="ModalComponent"/> representing the builder.
/// </summary>
/// <returns>A <see cref="ModalComponent"/> representing the builder.</returns>
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<IMessageComponentBuilder>.IsReadOnly => false;
}
}

View File

@@ -10,9 +10,9 @@ namespace Discord
/// <summary>
/// Gets the components to be used in a modal.
/// </summary>
public IReadOnlyCollection<ActionRowComponent> Components { get; }
public IReadOnlyCollection<IMessageComponent> Components { get; }
internal ModalComponent(List<ActionRowComponent> components)
internal ModalComponent(List<IMessageComponent> components)
{
Components = components;
}

View File

@@ -10,6 +10,7 @@ namespace Discord.Interactions
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="interaction">The interaction to respond to.</param>
/// <param name="customId">The custom id of the modal.</param>
/// <param name="modifyModal">Delegate that can be used to modify the modal.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <returns>A task that represents the asynchronous operation of responding to the interaction.</returns>
@@ -31,10 +32,11 @@ namespace Discord.Interactions
/// </remarks>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="interaction">The interaction to respond to.</param>
/// <param name="customId">The custom id of the modal.</param>
/// <param name="interactionService">Interaction service instance that should be used to build <see cref="ModalInfo"/>s.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <param name="modifyModal">Delegate that can be used to modify the modal.</param>
/// <returns></returns>
/// <returns>A task that represents the asynchronous operation of responding to the interaction.</returns>
public static Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, InteractionService interactionService,
RequestOptions options = null, Action<ModalBuilder> modifyModal = null)
where T : class, IModal
@@ -50,10 +52,11 @@ namespace Discord.Interactions
/// </summary>
/// <typeparam name="T">Type of the <see cref="IModal"/> implementation.</typeparam>
/// <param name="interaction">The interaction to respond to.</param>
/// <param name="customId">The custom id of the modal.</param>
/// <param name="modal">The <see cref="IModal"/> instance to get field values from.</param>
/// <param name="options">The request options for this <see langword="async"/> request.</param>
/// <param name="modifyModal">Delegate that can be used to modify the modal.</param>
/// <returns></returns>
/// <returns>A task that represents the asynchronous operation of responding to the interaction.</returns>
public static Task RespondWithModalAsync<T>(this IDiscordInteraction interaction, string customId, T modal, RequestOptions options = null,
Action<ModalBuilder> modifyModal = null)
where T : class, IModal
@@ -81,8 +84,7 @@ namespace Discord.Interactions
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);
}

View File

@@ -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<int> Id { get; set; }
[JsonProperty("custom_id")]
public string CustomId { get; set; }
[JsonProperty("min_values")]
public Optional<int> MinValues { get; set; }
[JsonProperty("max_values")]
public Optional<int> MaxValues { get; set; }
[JsonProperty("required")]
public Optional<bool> IsRequired { get; set; }
[JsonProperty("values")]
public Optional<string[]> Values { get; set; }
public FileUploadComponent() {}
public FileUploadComponent(Discord.FileUploadComponent component)
{
Type = component.Type;
Id = component.Id ?? Optional<int>.Unspecified;
CustomId = component.CustomId;
MinValues = component.MinValues ?? Optional<int>.Unspecified;
MaxValues = component.MaxValues ?? Optional<int>.Unspecified;
IsRequired = component.IsRequired;
}
[JsonIgnore]
int? IMessageComponent.Id => Id.ToNullable();
IMessageComponentBuilder IMessageComponent.ToBuilder() => null;
}

View File

@@ -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<int> 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<int>.Unspecified;
Label = label.Label;
Description = label.Description;
Component = label.Component.ToModel();
}
public IMessageComponentBuilder ToBuilder() => null;
[JsonIgnore]
int? IMessageComponent.Id => Id.ToNullable();
}

View File

@@ -8,6 +8,9 @@ namespace Discord.API
public string CustomId { get; set; }
[JsonProperty("components")]
public API.ActionRowComponent[] Components { get; set; }
public IMessageComponent[] Components { get; set; }
[JsonProperty("resolved")]
public Optional<ModalInteractionDataResolved> Resolved { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Discord.API;
internal class ModalInteractionDataResolved
{
[JsonProperty("users")]
public Optional<Dictionary<string, User>> Users { get; set; }
[JsonProperty("members")]
public Optional<Dictionary<string, GuildMember>> Members { get; set; }
[JsonProperty("roles")]
public Optional<Dictionary<string, Role>> Roles { get; set; }
[JsonProperty("channels")]
public Optional<Dictionary<string, Channel>> Channels { get; set; }
[JsonProperty("attachments")]
public Optional<Dictionary<string, Attachment>> Attachments { get; set; }
}

View File

@@ -16,8 +16,9 @@ namespace Discord.API
[JsonProperty("custom_id")]
public string CustomId { get; set; }
// deprecated
[JsonProperty("label")]
public string Label { get; set; }
public Optional<string> Label { get; set; }
[JsonProperty("placeholder")]
public Optional<string> Placeholder { get; set; }

View File

@@ -372,7 +372,7 @@ namespace Discord.Rest
{
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()
}
};

View File

@@ -508,7 +508,7 @@ namespace Discord.Rest
{
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()
}
};

View File

@@ -99,7 +99,7 @@ namespace Discord.Rest
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 @@ namespace Discord.Rest
: null;
}
}
if (component is API.FileUploadComponent fileUpload)
{
Values = fileUpload.Values.GetValueOrDefault(null);
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Model = Discord.API.ModalInteractionData;
@@ -17,15 +18,84 @@ namespace Discord.Rest
/// </summary>
public IReadOnlyCollection<RestMessageComponentData> Components { get; }
/// <inheritdoc cref="IModalInteractionData.Channels"/>
public IReadOnlyCollection<RestChannel> Channels { get; }
/// <inheritdoc cref="IModalInteractionData.Users"/>
public IReadOnlyCollection<RestUser> Users { get; }
/// <inheritdoc cref="IModalInteractionData.Roles"/>
public IReadOnlyCollection<RestRole> Roles { get; }
/// <inheritdoc cref="IModalInteractionData.Members"/>
public IReadOnlyCollection<RestGuildUser> Members { get; }
/// <inheritdoc cref="IModalInteractionData.Attachments"/>
public IReadOnlyCollection<IAttachment> Attachments { get; }
IReadOnlyCollection<IComponentInteractionData> IModalInteractionData.Components => Components;
/// <inheritdoc/>
IReadOnlyCollection<IChannel> IModalInteractionData.Channels => Channels;
/// <inheritdoc/>
IReadOnlyCollection<IUser> IModalInteractionData.Users => Users;
/// <inheritdoc/>
IReadOnlyCollection<IRole> IModalInteractionData.Roles => Roles;
/// <inheritdoc/>
IReadOnlyCollection<IGuildUser> IModalInteractionData.Members => Members;
/// <inheritdoc/>
IReadOnlyCollection<IAttachment> IModalInteractionData.Attachments => Attachments;
internal RestModalData(Model model, BaseDiscordClient discord, IGuild guild)
{
CustomId = model.CustomId;
Components = model.Components
.SelectMany(x => x.Components.OfType<IInteractableComponent>())
.SelectMany(c => c switch
{
Discord.API.ActionRowComponent row => row.Components, // Preserve the previous behavior
Discord.API.LabelComponent label => [label.Component],
_ => [c]
})
.OfType<IInteractableComponent>()
.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()
: [];
}
}
}
}

View File

@@ -41,6 +41,12 @@ internal static class MessageComponentExtension
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 class MessageComponentExtension
{
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 class MessageComponentExtension
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;
}

View File

@@ -62,6 +62,12 @@ namespace Discord.Net.Converters
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");
}

View File

@@ -504,7 +504,7 @@ namespace Discord.WebSocket
{
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()
}
};

View File

@@ -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 @@ namespace Discord.WebSocket
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 @@ namespace Discord.WebSocket
: null;
}
}
if (component is API.FileUploadComponent fileUpload)
{
Values = fileUpload.Values.GetValueOrDefault(null);
}
}
}
}

View File

@@ -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 @@ namespace Discord.WebSocket
/// </summary>
public IReadOnlyCollection<SocketMessageComponentData> Components { get; }
/// <inheritdoc cref="IModalInteractionData.Channels"/>
public IReadOnlyCollection<SocketChannel> Channels { get; }
/// <inheritdoc cref="IModalInteractionData.Users"/>
/// <remarks>Returns <see cref="SocketUser"/> if user is cached, <see cref="RestUser"/> otherwise.</remarks>
public IReadOnlyCollection<IUser> Users { get; }
/// <inheritdoc cref="IModalInteractionData.Roles"/>
public IReadOnlyCollection<SocketRole> Roles { get; }
/// <inheritdoc cref="IModalInteractionData.Members"/>
public IReadOnlyCollection<SocketGuildUser> Members { get; }
/// <inheritdoc cref="IModalInteractionData.Attachments"/>
public IReadOnlyCollection<IAttachment> Attachments { get; }
/// <inheritdoc />
IReadOnlyCollection<IChannel> IModalInteractionData.Channels => Channels;
/// <inheritdoc />
IReadOnlyCollection<IUser> IModalInteractionData.Users => Users;
/// <inheritdoc />
IReadOnlyCollection<IRole> IModalInteractionData.Roles => Roles;
/// <inheritdoc />
IReadOnlyCollection<IGuildUser> IModalInteractionData.Members => Members;
/// <inheritdoc />
IReadOnlyCollection<IAttachment> 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<IInteractableComponent>())
.SelectMany(c => c switch
{
Discord.API.ActionRowComponent row => row.Components, // Preserve the previous behavior
Discord.API.LabelComponent label => [label.Component],
_ => [c]
})
.OfType<IInteractableComponent>()
.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<IComponentInteractionData> IModalInteractionData.Components => Components;

View File

@@ -161,7 +161,7 @@ namespace Discord.WebSocket
{
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()
}
};