diff --git a/src/Discord.Net.Interactions/Attributes/ChannelTypesAttribute.cs b/src/Discord.Net.Interactions/Attributes/ChannelTypesAttribute.cs index 1f498e34..37015161 100644 --- a/src/Discord.Net.Interactions/Attributes/ChannelTypesAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/ChannelTypesAttribute.cs @@ -7,7 +7,7 @@ namespace Discord.Interactions /// /// Specify the target channel types for a option. /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class ChannelTypesAttribute : Attribute { /// diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs index 5e445f23..4cf1a6b5 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalChannelSelectAttribute.cs @@ -12,5 +12,9 @@ public class ModalChannelSelectAttribute : ModalSelectComponentAttribute /// Create a new . /// /// Custom ID of the channel select component. - public ModalChannelSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } + /// The minimum number of values that can be selected. + /// The maximum number of values that can be selected. + /// Optional identifier for the component. + public ModalChannelSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0) + : base(customId, minValues, maxValues, id) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs index 7b6ce331..b8d67d6d 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalComponentAttribute.cs @@ -13,5 +13,16 @@ public abstract class ModalComponentAttribute : Attribute /// public abstract ComponentType ComponentType { get; } - internal ModalComponentAttribute() { } + /// + /// Gets the optional identifier for component. + /// + /// + /// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API. + /// + public int Id { get; set; } + + internal ModalComponentAttribute(int id = 0) + { + Id = id; + } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs index 5271b64a..bb22c9e2 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalFileUploadAttribute.cs @@ -24,7 +24,9 @@ public class ModalFileUploadAttribute : ModalInputAttribute /// Custom ID of the file upload component. /// Minimum number of files that can be uploaded. /// Maximum number of files that can be uploaded. - public ModalFileUploadAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId) + /// The optional identifier for the component. + public ModalFileUploadAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0) + : base(customId, id) { MinValues = minValues; MaxValues = maxValues; diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs index 5829b363..5ba3a447 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalInputAttribute.cs @@ -17,7 +17,8 @@ namespace Discord.Interactions /// Create a new . /// /// The custom id of the input. - internal ModalInputAttribute(string customId) + /// Optional identifier for component. + internal ModalInputAttribute(string customId, int id) : base(id) { CustomId = customId; } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs index 57a0b1b9..f76a3345 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalMentionableSelectAttribute.cs @@ -14,5 +14,7 @@ public class ModalMentionableSelectAttribute : ModalSelectComponentAttribute /// Custom ID of the mentionable select component. /// Minimum number of values that can be selected. /// Maximum number of values that can be selected - public ModalMentionableSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } + /// The optional identifier for the component. + public ModalMentionableSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0) + : base(customId, minValues, maxValues, id) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs index 3808be32..ca9b7b47 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalRoleSelectAttribute.cs @@ -14,5 +14,7 @@ public class ModalRoleSelectAttribute : ModalSelectComponentAttribute /// Custom ID of the role select component. /// Minimum number of values that can be selected. /// Maximum number of values that can be selected. - public ModalRoleSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } + /// The optional identifier for the component. + public ModalRoleSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0) + : base(customId, minValues, maxValues, id) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs index ccbc5891..b7cb710f 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectComponentAttribute.cs @@ -20,7 +20,8 @@ public abstract class ModalSelectComponentAttribute : ModalInputAttribute /// public string Placeholder { get; set; } - internal ModalSelectComponentAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId) + internal ModalSelectComponentAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0) + : base(customId, id) { MinValues = minValues; MaxValues = maxValues; diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs index 5d70eb0c..3e3eca95 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalSelectMenuAttribute.cs @@ -14,5 +14,7 @@ public sealed class ModalSelectMenuAttribute : ModalSelectComponentAttribute /// Custom ID of the select menu component. /// Minimum number of values that can be selected. /// Maximum number of values that can be selected. - public ModalSelectMenuAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } + /// The optional identifier for the component. + public ModalSelectMenuAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0) + : base(customId, minValues, maxValues, id) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs index 300db32b..eecb71ec 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextDisplayAttribute.cs @@ -17,7 +17,9 @@ public class ModalTextDisplayAttribute : ModalComponentAttribute /// Create a new . /// /// Content of the text display. - public ModalTextDisplayAttribute(string content = null) + /// Optional identifier for component. + public ModalTextDisplayAttribute(string content = null, int id = 0) + : base(id) { Content = content; } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs index 4439e1d8..e9972b97 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalTextInputAttribute.cs @@ -11,27 +11,27 @@ namespace Discord.Interactions /// /// Gets the style of the text input. /// - public TextInputStyle Style { get; } + public TextInputStyle Style { get; set; } /// /// Gets the placeholder of the text input. /// - public string Placeholder { get; } + public string Placeholder { get; set; } /// /// Gets the minimum length of the text input. /// - public int MinLength { get; } + public int MinLength { get; set; } /// /// Gets the maximum length of the text input. /// - public int MaxLength { get; } + public int MaxLength { get; set; } /// /// Gets the initial value to be displayed by this input. /// - public string InitialValue { get; } + public string InitialValue { get; set; } /// /// Create a new . @@ -42,8 +42,9 @@ namespace Discord.Interactions /// The minimum length of the text input's content. /// The maximum length of the text input's content. /// The initial value to be displayed by this input. - public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null) - : base(customId) + /// The optional identifier for the component. + public ModalTextInputAttribute(string customId, TextInputStyle style = TextInputStyle.Short, string placeholder = null, int minLength = 1, int maxLength = 4000, string initValue = null, int id = 0) + : base(customId, id) { Style = style; Placeholder = placeholder; diff --git a/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs index cc3cf288..630e9b1b 100644 --- a/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Modals/ModalUserSelectAttribute.cs @@ -14,5 +14,7 @@ public class ModalUserSelectAttribute : ModalSelectComponentAttribute /// Custom ID of the user select component. /// Minimum number of values that can be selected. /// Maximum number of values that can be selected. - public ModalUserSelectAttribute(string customId, int minValues = 1, int maxValues = 1) : base(customId, minValues, maxValues) { } + /// The optional identifier for the component. + public ModalUserSelectAttribute(string customId, int minValues = 1, int maxValues = 1, int id = 0) + : base(customId, minValues, maxValues, id) { } } diff --git a/src/Discord.Net.Interactions/Attributes/Modals/SelectMenuOptionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Modals/SelectMenuOptionAttribute.cs new file mode 100644 index 00000000..c84e4c97 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/Modals/SelectMenuOptionAttribute.cs @@ -0,0 +1,31 @@ +using System; + +namespace Discord.Interactions; + +/// +/// Adds additional metadata to enum fields that are used for select-menus. +/// +/// +/// To manually add select menu options to modal components, use instead. +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] +public class SelectMenuOptionAttribute : Attribute +{ + /// + /// Gets or sets the desription of the option. + /// + public string Description { get; set; } + + /// + /// Gets or sets whether the option is selected by default. + /// + public bool IsDefault { get; set; } + + /// + /// Gets or sets the emote of the option. + /// + /// + /// Can be either an or an + /// + public string Emote { get; set; } +} diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs index 01642458..338e7c72 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/ChannelSelectComponentBuilder.cs @@ -8,8 +8,15 @@ namespace Discord.Interactions.Builders; /// public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder { + private readonly List _channelTypes = new(); + protected override ChannelSelectComponentBuilder Instance => this; + /// + /// Gets the presented channel types for this Channel Select. + /// + public IReadOnlyCollection ChannelTypes => _channelTypes.AsReadOnly(); + /// /// Initializes a new . /// @@ -42,6 +49,19 @@ public class ChannelSelectComponentBuilder : SnowflakeSelectComponentBuilder + /// Sets the value of . + /// + /// the new value of . + /// + /// The builder instance. + /// + public ChannelSelectComponentBuilder WithChannelTypes(params IEnumerable channelTypes) + { + _channelTypes.AddRange(channelTypes); + return this; + } + internal override ChannelSelectComponentInfo Build(ModalInfo modal) => new(this, modal); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs index 9d869e4c..e7f05261 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/IModalComponentBuilder.cs @@ -31,6 +31,14 @@ public interface IModalComponentBuilder /// object DefaultValue { get; } + /// + /// Gets the optional identifier for component. + /// + /// + /// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API. + /// + int Id { get; } + /// /// Gets a collection of the attributes of this component. /// @@ -62,4 +70,13 @@ public interface IModalComponentBuilder /// The builder instance. /// IModalComponentBuilder WithAttributes(params Attribute[] attributes); + + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + IModalComponentBuilder WithId(int id); } diff --git a/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs b/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs index f7c41b3f..70b7f706 100644 --- a/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Modals/Components/ModalComponentBuilder.cs @@ -26,6 +26,9 @@ public abstract class ModalComponentBuilder : IModalComponentBu /// public object DefaultValue { get; set; } + /// + public int Id { get; set; } + /// public IReadOnlyCollection Attributes => _attributes; @@ -87,6 +90,19 @@ public abstract class ModalComponentBuilder : IModalComponentBu return Instance; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public virtual TBuilder WithId(int id) + { + Id = id; + return Instance; + } + internal abstract TInfo Build(ModalInfo modal); /// @@ -97,4 +113,7 @@ public abstract class ModalComponentBuilder : IModalComponentBu /// IModalComponentBuilder IModalComponentBuilder.WithAttributes(params Attribute[] attributes) => WithAttributes(attributes); + + /// + IModalComponentBuilder IModalComponentBuilder.WithId(int id) => WithId(id); } diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index c76552ef..f3567f5e 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -654,6 +654,8 @@ namespace Discord.Interactions.Builders private static void BuildTextInputComponent(TextInputComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) { + EnsurePubliclySettable(propertyInfo); + var attributes = propertyInfo.GetCustomAttributes(); builder.Label = propertyInfo.Name; @@ -673,6 +675,7 @@ namespace Discord.Interactions.Builders builder.MaxLength = textInput.MaxLength; builder.MinLength = textInput.MinLength; builder.InitialValue = textInput.InitialValue; + builder.Id = textInput.Id; break; case RequiredInputAttribute requiredInput: builder.IsRequired = requiredInput.IsRequired; @@ -690,6 +693,8 @@ namespace Discord.Interactions.Builders private static void BuildSelectMenuComponent(SelectMenuComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) { + EnsurePubliclySettable(propertyInfo); + var attributes = propertyInfo.GetCustomAttributes(); builder.Label = propertyInfo.Name; @@ -707,6 +712,7 @@ namespace Discord.Interactions.Builders builder.MinValues = selectMenuInput.MinValues; builder.MaxValues = selectMenuInput.MaxValues; builder.Placeholder = selectMenuInput.Placeholder; + builder.Id = selectMenuInput.Id; break; case RequiredInputAttribute requiredInput: builder.IsRequired = requiredInput.IsRequired; @@ -742,6 +748,8 @@ namespace Discord.Interactions.Builders where TInfo : SnowflakeSelectComponentInfo where TBuilder : SnowflakeSelectComponentBuilder { + EnsurePubliclySettable(propertyInfo); + var attributes = propertyInfo.GetCustomAttributes(); builder.Label = propertyInfo.Name; @@ -759,6 +767,7 @@ namespace Discord.Interactions.Builders builder.MinValues = selectInput.MinValues; builder.MaxValues = selectInput.MaxValues; builder.Placeholder = selectInput.Placeholder; + builder.Id = selectInput.Id; break; case RequiredInputAttribute requiredInput: builder.IsRequired = requiredInput.IsRequired; @@ -767,6 +776,9 @@ namespace Discord.Interactions.Builders builder.Label = inputLabel.Label; builder.Description = inputLabel.Description; break; + case ChannelTypesAttribute channelTypes when builder is ChannelSelectComponentBuilder channelSelectBuilder: + channelSelectBuilder.WithChannelTypes(channelTypes.ChannelTypes); + break; default: builder.WithAttributes(attribute); break; @@ -776,6 +788,8 @@ namespace Discord.Interactions.Builders private static void BuildFileUploadComponent(FileUploadComponentBuilder builder, PropertyInfo propertyInfo, object defaultValue) { + EnsurePubliclySettable(propertyInfo); + var attributes = propertyInfo.GetCustomAttributes(); builder.Label = propertyInfo.Name; @@ -792,6 +806,7 @@ namespace Discord.Interactions.Builders builder.ComponentType = fileUploadInput.ComponentType; builder.MinValues = fileUploadInput.MinValues; builder.MaxValues = fileUploadInput.MaxValues; + builder.Id = fileUploadInput.Id; break; case RequiredInputAttribute requiredInput: builder.IsRequired = requiredInput.IsRequired; @@ -822,6 +837,7 @@ namespace Discord.Interactions.Builders case ModalTextDisplayAttribute textDisplay: builder.ComponentType = textDisplay.ComponentType; builder.Content = textDisplay.Content; + builder.Id = textDisplay.Id; break; default: builder.WithAttributes(attribute); @@ -882,9 +898,18 @@ namespace Discord.Interactions.Builders private static bool IsValidModalComponentDefinition(PropertyInfo propertyInfo) { - return propertyInfo.SetMethod?.IsPublic == true && - propertyInfo.SetMethod?.IsStatic == false && - propertyInfo.IsDefined(typeof(ModalComponentAttribute)); + return propertyInfo.IsDefined(typeof(ModalComponentAttribute)); + } + + private static bool IsPubliclySettable(PropertyInfo propertyInfo) + { + return propertyInfo.SetMethod is { IsPublic: true, IsStatic: false }; + } + + private static void EnsurePubliclySettable(PropertyInfo propertyInfo) + { + if(!IsPubliclySettable(propertyInfo)) + throw new InvalidOperationException($"The property {propertyInfo.Name} must be publicly settable."); } private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter) diff --git a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs index 56549da8..cb8babd8 100644 --- a/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs +++ b/src/Discord.Net.Interactions/Extensions/IDiscordInteractionExtensions.cs @@ -82,12 +82,10 @@ namespace Discord.Interactions case TextInputComponentInfo textComponent: { var inputBuilder = new TextInputBuilder(textComponent.CustomId, textComponent.Style, textComponent.Placeholder, textComponent.IsRequired ? textComponent.MinLength : null, - textComponent.MaxLength, textComponent.IsRequired); + textComponent.MaxLength, textComponent.IsRequired, id: textComponent.Id); - if (modalInstance != null) - { - await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, textComponent.Getter(modalInstance)); - } + var instanceValue = modalInstance is not null ? textComponent.Getter(modalInstance) : null; + await textComponent.TypeConverter.WriteAsync(inputBuilder, interaction, textComponent, instanceValue); var labelBuilder = new LabelBuilder(textComponent.Label, inputBuilder, textComponent.Description); builder.AddLabel(labelBuilder); @@ -95,12 +93,10 @@ namespace Discord.Interactions break; case SelectMenuComponentInfo selectMenuComponent: { - var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false, isRequired: selectMenuComponent.IsRequired); + var inputBuilder = new SelectMenuBuilder(selectMenuComponent.CustomId, selectMenuComponent.Options.Select(x => new SelectMenuOptionBuilder(x)).ToList(), selectMenuComponent.Placeholder, selectMenuComponent.MaxValues, selectMenuComponent.MinValues, false, isRequired: selectMenuComponent.IsRequired, id: selectMenuComponent.Id); - if (modalInstance != null) - { - await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, selectMenuComponent.Getter(modalInstance)); - } + var instanceValue = modalInstance is not null ? selectMenuComponent.Getter(modalInstance) : null; + await selectMenuComponent.TypeConverter.WriteAsync(inputBuilder, interaction, selectMenuComponent, instanceValue); var labelBuilder = new LabelBuilder(selectMenuComponent.Label, inputBuilder, selectMenuComponent.Description); builder.AddLabel(labelBuilder); @@ -108,12 +104,11 @@ namespace Discord.Interactions break; case SnowflakeSelectComponentInfo snowflakeSelectComponent: { - var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, null, snowflakeSelectComponent.DefaultValues.ToList(), null, snowflakeSelectComponent.IsRequired); + var channelTypes = snowflakeSelectComponent is ChannelSelectComponentInfo channelSelectComponent ? channelSelectComponent.ChannelTypes : null; + var inputBuilder = new SelectMenuBuilder(snowflakeSelectComponent.CustomId, null, snowflakeSelectComponent.Placeholder, snowflakeSelectComponent.MaxValues, snowflakeSelectComponent.MinValues, false, snowflakeSelectComponent.ComponentType, channelTypes?.ToList(), snowflakeSelectComponent.DefaultValues.ToList(), snowflakeSelectComponent.Id, snowflakeSelectComponent.IsRequired); - if (modalInstance != null) - { - await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, snowflakeSelectComponent.Getter(modalInstance)); - } + var instanceValue = modalInstance is not null ? snowflakeSelectComponent.Getter(modalInstance) : null; + await snowflakeSelectComponent.TypeConverter.WriteAsync(inputBuilder, interaction, snowflakeSelectComponent, instanceValue); var labelBuilder = new LabelBuilder(snowflakeSelectComponent.Label, inputBuilder, snowflakeSelectComponent.Description); builder.AddLabel(labelBuilder); @@ -121,23 +116,22 @@ namespace Discord.Interactions break; case FileUploadComponentInfo fileUploadComponent: { - var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired); + var inputBuilder = new FileUploadComponentBuilder(fileUploadComponent.CustomId, fileUploadComponent.MinValues, fileUploadComponent.MaxValues, fileUploadComponent.IsRequired, fileUploadComponent.Id); - if (modalInstance != null) - { - await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, fileUploadComponent.Getter(modalInstance)); - } + var instanceValue = modalInstance is not null ? fileUploadComponent.Getter(modalInstance) : null; + await fileUploadComponent.TypeConverter.WriteAsync(inputBuilder, interaction, fileUploadComponent, instanceValue); var labelBuilder = new LabelBuilder(fileUploadComponent.Label, inputBuilder, fileUploadComponent.Description); builder.AddLabel(labelBuilder); } break; case TextDisplayComponentInfo textDisplayComponent: - { - var instanceValue = modalInstance is not null ? textDisplayComponent.Getter(modalInstance).ToString() : null; - var content = instanceValue ?? (textDisplayComponent.DefaultValue as string) ?? textDisplayComponent.Content; - var componentBuilder = new TextDisplayBuilder(content); - builder.AddTextDisplay(componentBuilder); + { + var instanceValue = modalInstance is not null ? textDisplayComponent.Getter(modalInstance).ToString() : null; + var content = instanceValue ?? (textDisplayComponent.DefaultValue as string) ?? textDisplayComponent.Content; + + var componentBuilder = new TextDisplayBuilder(content, textDisplayComponent.Id); + builder.AddTextDisplay(componentBuilder); } break; default: diff --git a/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs index 6161a3c8..0a218a67 100644 --- a/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/Components/ChannelSelectComponentInfo.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + namespace Discord.Interactions; /// @@ -5,5 +8,14 @@ namespace Discord.Interactions; /// public class ChannelSelectComponentInfo : SnowflakeSelectComponentInfo { - internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal) : base(builder, modal) { } + /// + /// Gets the presented channel types for this Channel Select. + /// + public IReadOnlyCollection ChannelTypes { get; } + + internal ChannelSelectComponentInfo(Builders.ChannelSelectComponentBuilder builder, ModalInfo modal) + : base(builder, modal) + { + ChannelTypes = builder.ChannelTypes.ToImmutableArray(); + } } diff --git a/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs b/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs index b9245589..782821c9 100644 --- a/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs +++ b/src/Discord.Net.Interactions/Info/Components/ModalComponentInfo.cs @@ -39,6 +39,14 @@ public abstract class ModalComponentInfo /// public object DefaultValue { get; } + /// + /// Gets the optional identifier for component. + /// + /// + /// Sending components with an id of 0 is allowed but will be treated as empty and replaced by the API. + /// + public int Id { get; } + /// /// Gets a collection of the attributes of this command. /// @@ -51,6 +59,7 @@ public abstract class ModalComponentInfo Type = builder.Type; PropertyInfo = builder.PropertyInfo; DefaultValue = builder.DefaultValue; + Id = builder.Id; Attributes = builder.Attributes.ToImmutableArray(); _getter = new(() => ReflectionUtils.CreateLambdaPropertyGetter(Modal.Type, PropertyInfo)); diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index d8f3bffc..398143f4 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -1219,7 +1219,7 @@ namespace Discord.Interactions { var type = typeof(T); - if (_modalInfos.ContainsKey(type)) + if (ModalUtils.Contains(type)) throw new InvalidOperationException($"Modal type {type.FullName} already exists."); return ModalUtils.GetOrAdd(type, this); diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs index 3fc14592..eb267837 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultArrayModalComponentConverter.cs @@ -1,5 +1,7 @@ +using Discord.Interactions.Utilities; using Discord.Utils; using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -12,6 +14,7 @@ internal sealed class DefaultArrayModalComponentConverter : ModalComponentTyp private readonly Type _underlyingType; private readonly TypeReader _typeReader; private readonly ImmutableArray _channelTypes; + private readonly ImmutableArray _enumOptions; public DefaultArrayModalComponentConverter(InteractionService interactionService) { @@ -56,13 +59,14 @@ internal sealed class DefaultArrayModalComponentConverter : ModalComponentTyp => [ChannelType.Forum], _ => [] }; + + _enumOptions = _underlyingType!.IsEnum ? [..EnumUtils.BuildSelectMenuOptions(_underlyingType)] : ImmutableArray.Empty; } public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) { var objs = new List(); - if (_typeReader is not null && option.Values.Count > 0) foreach (var value in option.Values) { @@ -77,7 +81,7 @@ internal sealed class DefaultArrayModalComponentConverter : ModalComponentTyp { if (!TryGetModalInteractionData(context, out var modalData)) { - return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{typeof(IModalInteractionData).Name} cannot be accessed from the provided {typeof(IInteractionContext).Name} type."); + return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{nameof(IModalInteractionData)} cannot be accessed from the provided {nameof(IInteractionContext)} type."); } var resolvedSnowflakes = new Dictionary(); @@ -134,49 +138,44 @@ internal sealed class DefaultArrayModalComponentConverter : ModalComponentTyp if (builder is not SelectMenuBuilder selectMenu || !component.ComponentType.IsSelectType()) throw new InvalidOperationException($"Component type of the input {component.CustomId} of modal {component.Modal.Type.FullName} must be a select type."); - switch (value) + if (!_enumOptions.IsEmpty) { - case IUser user: - selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromUser(user)); - break; - case IRole role: - selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromRole(role)); - break; - case IChannel channel: - selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromChannel(channel)); - break; - case IMentionable mentionable: - selectMenu.WithDefaultValues(mentionable switch + var visibleOptions = _enumOptions.Where(x => !x.Predicate?.Invoke(interaction) ?? true); + + var enumValues = value is IEnumerable valueArr ? valueArr.Cast().ToArray() : null; + + foreach (var option in visibleOptions) + { + var optionBuilder = new SelectMenuOptionBuilder(option.MenuOption); + + if (enumValues is not null) + optionBuilder.IsDefault = enumValues.Contains(option.Value); + + selectMenu.AddOption(optionBuilder); + } + + return Task.CompletedTask; + } + + selectMenu.DefaultValues = value switch + { + IEnumerable defaultUsers => defaultUsers.Select(SelectMenuDefaultValue.FromUser).ToList(), + IEnumerable defaultRoles => defaultRoles.Select(SelectMenuDefaultValue.FromRole).ToList(), + IEnumerable defaultChannels => + defaultChannels.Select(SelectMenuDefaultValue.FromChannel).ToList(), + IEnumerable defaultMentionables => defaultMentionables + .Select(x => { - IUser user => SelectMenuDefaultValue.FromUser(user), - IRole role => SelectMenuDefaultValue.FromRole(role), - IChannel channel => SelectMenuDefaultValue.FromChannel(channel), - _ => throw new InvalidOperationException($"Mentionable select cannot be populated using an entity with type: {mentionable.GetType().FullName}") - }); - break; - case IEnumerable defaultUsers: - selectMenu.DefaultValues = defaultUsers.Select(SelectMenuDefaultValue.FromUser).ToList(); - break; - case IEnumerable defaultRoles: - selectMenu.DefaultValues = defaultRoles.Select(SelectMenuDefaultValue.FromRole).ToList(); - break; - case IEnumerable defaultChannels: - selectMenu.DefaultValues = defaultChannels.Select(SelectMenuDefaultValue.FromChannel).ToList(); - break; - case IEnumerable defaultMentionables: - selectMenu.DefaultValues = defaultMentionables.Where(x => x is IUser or IRole or IChannel) - .Select(x => + return x switch { - return x switch - { - IUser user => SelectMenuDefaultValue.FromUser(user), - IRole role => SelectMenuDefaultValue.FromRole(role), - IChannel channel => SelectMenuDefaultValue.FromChannel(channel), - _ => throw new InvalidOperationException($"Mentionable select cannot be populated using an entity with type: {x.GetType().FullName}") - }; - }) - .ToList(); - break; + IUser user => SelectMenuDefaultValue.FromUser(user), + IRole role => SelectMenuDefaultValue.FromRole(role), + _ => throw new InvalidOperationException( + $"Mentionable select cannot be populated using an entity with type: {x.GetType().FullName}") + }; + }) + .ToList(), + _ => selectMenu.DefaultValues }; if (component.ComponentType == ComponentType.ChannelSelect && _channelTypes.Length > 0) diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultSnowflakeModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultSnowflakeModalComponentConverter.cs index 5223bd85..636e5702 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultSnowflakeModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/DefaultSnowflakeModalComponentConverter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; @@ -16,7 +17,7 @@ internal abstract class DefaultSnowflakeModalComponentConverter : ModalCompon if (!TryGetModalInteractionData(context, out modalData)) { - preemptiveResult = TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{typeof(IModalInteractionData).Name} cannot be accessed from the provided {typeof(IInteractionContext).Name} type."); + preemptiveResult = TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"{nameof(IModalInteractionData)} cannot be accessed from the provided {nameof(IInteractionContext)} type."); return true; } @@ -51,52 +52,47 @@ internal abstract class DefaultSnowflakeModalComponentConverter : ModalCompon internal class DefaultAttachmentModalComponentConverter : DefaultSnowflakeModalComponentConverter where T : class, IAttachment { - public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) { if (TryGetPreemptiveResult(context, option, ComponentType.FileUpload, out var result, out var modalData, out var id)) { - return result; + return Task.FromResult(result); } var resolvedEntity = modalData.Attachments.FirstOrDefault(x => x.Id == id); if (resolvedEntity is null) { - return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."); + return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.")); } - return TypeConverterResult.FromSuccess(resolvedEntity); + return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity)); } } internal class DefaultUserModalComponentConverter : DefaultSnowflakeModalComponentConverter where T : class, IUser { - public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) { if (TryGetPreemptiveResult(context, option, ComponentType.UserSelect, out var result, out var modalData, out var id)) { - return result; + return Task.FromResult(result); } var resolvedEntity = modalData.Members.UnionBy(modalData.Users, x => x.Id).FirstOrDefault(x => x.Id == id); if (resolvedEntity is null) { - return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."); + return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.")); } - return TypeConverterResult.FromSuccess(resolvedEntity); + return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity)); } public override Task WriteAsync(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value) { - if (value is null) - { - return Task.CompletedTask; - } - - if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.UserSelect) + if (builder is not SelectMenuBuilder { Type: ComponentType.UserSelect } selectMenu) { throw new InvalidOperationException($"{typeof(DefaultUserModalComponentConverter).Name} can only be used with User Select components."); } @@ -106,9 +102,14 @@ internal class DefaultUserModalComponentConverter : DefaultSnowflakeModalComp throw new InvalidOperationException($"Multi-select User Select cannot be used with a single {typeof(T).Name} entity."); } + if(value is null) + { + return Task.CompletedTask; + } + if (value is not IUser user) { - throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default User Select values. Expected {typeof(IUser).Name}"); + throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default User Select values. Expected {nameof(IUser)}"); } selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromUser(user)); @@ -120,31 +121,26 @@ internal class DefaultUserModalComponentConverter : DefaultSnowflakeModalComp internal class DefaultRoleModalComponentConverter : DefaultSnowflakeModalComponentConverter where T : class, IRole { - public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) { if (TryGetPreemptiveResult(context, option, ComponentType.RoleSelect, out var result, out var modalData, out var id)) { - return result; + return Task.FromResult(result); } var resolvedEntity = modalData.Roles.FirstOrDefault(x => x.Id == id); if (resolvedEntity is null) { - return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."); + return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.")); } - return TypeConverterResult.FromSuccess(resolvedEntity); + return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity)); } public override Task WriteAsync(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value) { - if(value is null) - { - return Task.CompletedTask; - } - - if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.RoleSelect) + if (builder is not SelectMenuBuilder { Type: ComponentType.RoleSelect } selectMenu) { throw new InvalidOperationException($"{typeof(DefaultRoleModalComponentConverter).Name} can only be used with Role Select components."); } @@ -154,9 +150,14 @@ internal class DefaultRoleModalComponentConverter : DefaultSnowflakeModalComp throw new InvalidOperationException($"Multi-select Role Select cannot be used with a single {typeof(T).Name} entity."); } + if(value is null) + { + return Task.CompletedTask; + } + if (value is not IRole role) { - throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Role Select values. Expected {typeof(IRole).Name}"); + throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Role Select values. Expected {nameof(IRole)}"); } selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromRole(role)); @@ -168,43 +169,77 @@ internal class DefaultRoleModalComponentConverter : DefaultSnowflakeModalComp internal class DefaultChannelModalComponentConverter : DefaultSnowflakeModalComponentConverter where T : class, IChannel { - public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + private readonly ImmutableArray _channelTypes; + + public DefaultChannelModalComponentConverter() + { + var type = typeof(T); + + _channelTypes = true switch + { + _ when typeof(IStageChannel).IsAssignableFrom(type) + => [ChannelType.Stage], + _ when typeof(IVoiceChannel).IsAssignableFrom(type) + => [ChannelType.Voice], + _ when typeof(IDMChannel).IsAssignableFrom(type) + => [ChannelType.DM], + _ when typeof(IGroupChannel).IsAssignableFrom(type) + => [ChannelType.Group], + _ when typeof(ICategoryChannel).IsAssignableFrom(type) + => [ChannelType.Category], + _ when typeof(INewsChannel).IsAssignableFrom(type) + => [ChannelType.News], + _ when typeof(IThreadChannel).IsAssignableFrom(type) + => [ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.NewsThread], + _ when typeof(ITextChannel).IsAssignableFrom(type) + => [ChannelType.Text], + _ when typeof(IMediaChannel).IsAssignableFrom(type) + => [ChannelType.Media], + _ when typeof(IForumChannel).IsAssignableFrom(type) + => [ChannelType.Forum], + _ => [] + }; + } + + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) { if (TryGetPreemptiveResult(context, option, ComponentType.ChannelSelect, out var result, out var modalData, out var id)) { - return result; + return Task.FromResult(result); } var resolvedEntity = modalData.Channels.FirstOrDefault(x => x.Id == id); if (resolvedEntity is null) { - return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."); + return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.")); } - return TypeConverterResult.FromSuccess(resolvedEntity); + return Task.FromResult(TypeConverterResult.FromSuccess(resolvedEntity)); } public override Task WriteAsync(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value) { - if (value is null) - { - return Task.CompletedTask; - } - - if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.ChannelSelect) + if (builder is not SelectMenuBuilder { Type: ComponentType.ChannelSelect } selectMenu) { throw new InvalidOperationException($"{typeof(DefaultChannelModalComponentConverter).Name} can only be used with Channel Select components."); } + selectMenu.WithChannelTypes(_channelTypes.ToList()); + if(selectMenu.MaxValues > 1) { throw new InvalidOperationException($"Multi-select Channel Select cannot be used with a single {typeof(T).Name} entity."); } + if (value is null) + { + return Task.CompletedTask; + } + if(value is not IChannel channel) { - throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Channel Select values. Expected {typeof(IChannel).Name}"); + throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Channel Select values. Expected {nameof(IChannel)}"); } selectMenu.WithDefaultValues(SelectMenuDefaultValue.FromChannel(channel)); @@ -216,11 +251,11 @@ internal class DefaultChannelModalComponentConverter : DefaultSnowflakeModalC internal class DefaultMentionableModalComponentConverter : DefaultSnowflakeModalComponentConverter where T : class, IMentionable { - public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) + public override Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) { if (TryGetPreemptiveResult(context, option, ComponentType.MentionableSelect, out var result, out var modalData, out var id)) { - return result; + return Task.FromResult(result); } var resolvedMentionables = new Dictionary(); @@ -236,20 +271,15 @@ internal class DefaultMentionableModalComponentConverter : DefaultSnowflakeMo if (resolvedMentionables.Count == 0 || !resolvedMentionables.TryGetValue(id, out var entity)) { - return TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved."); + return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ParseFailed, $"Snowflake entity reference for the {option.Type} cannot be resolved.")); } - return TypeConverterResult.FromSuccess(entity); + return Task.FromResult(TypeConverterResult.FromSuccess(entity)); } public override Task WriteAsync(TBuilder builder, IDiscordInteraction interaction, InputComponentInfo component, object value) { - if (value is null) - { - return Task.CompletedTask; - } - - if (builder is not SelectMenuBuilder selectMenu || selectMenu.Type is not ComponentType.MentionableSelect) + if (builder is not SelectMenuBuilder { Type: ComponentType.MentionableSelect } selectMenu) { throw new InvalidOperationException($"{typeof(DefaultMentionableModalComponentConverter).Name} can only be used with Mentionable Select components."); } @@ -259,11 +289,16 @@ internal class DefaultMentionableModalComponentConverter : DefaultSnowflakeMo throw new InvalidOperationException($"Multi-select Mentionable Select cannot be used with a single {typeof(T).Name} entity."); } + if (value is null) + { + return Task.CompletedTask; + } + var defaultValue = value switch { IRole role => SelectMenuDefaultValue.FromRole(role), IUser user => SelectMenuDefaultValue.FromUser(user), - _ => throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Mentionable Select values. Expected {typeof(IUser).Name} or {typeof(IRole).Name}") + _ => throw new InvalidOperationException($"{typeof(T).Name} cannot be used to assign default Mentionable Select values. Expected {nameof(IUser)} or {nameof(IRole)}") }; selectMenu.WithDefaultValues(defaultValue); diff --git a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs index 75a1c1fd..cd218293 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ModalComponents/EnumModalComponentConverter.cs @@ -1,3 +1,4 @@ +using Discord.Interactions.Utilities; using System; using System.Collections.Immutable; using System.Linq; @@ -9,41 +10,18 @@ namespace Discord.Interactions; internal sealed class EnumModalComponentConverter : ModalComponentTypeConverter where T : struct, Enum { - private record Option(SelectMenuOptionBuilder OptionBuilder, Predicate Predicate, T Value); - private readonly bool _isFlags; - private readonly ImmutableArray