From 170dc99ef8be9f302c19098005c8d5d13939c078 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:37:30 +0000 Subject: [PATCH 1/6] Implement new modal components: RadioGroup, CheckboxGroup, Checkbox This commit introduces support for the new Discord components: Radio Group, Checkbox Group, and Checkbox. Key changes: - Added `ComponentType` enum values for `RadioGroup`, `CheckboxGroup`, and `Checkbox`. - Created JSON models for local and modal (transient) representations of these components. - Implemented `LocalComponent` classes (`LocalRadioGroupComponent`, `LocalCheckboxGroupComponent`, `LocalCheckboxComponent`) for constructing these components. - Implemented `IModalComponent` interfaces and `TransientModalComponent` classes for handling received components in modal submissions. - Updated `LocalComponent.ToModel()` to serialize the new components properly. - Updated `BindValues` command execution step to support parameter binding for these new component types in modal commands. Co-authored-by: Quahu <24848652+Quahu@users.noreply.github.com> --- .../Components/ExecutionSteps/BindValues.cs | 14 + .../Modal/IModalCheckboxComponent.cs | 6 + .../Modal/IModalCheckboxGroupComponent.cs | 8 + .../Modal/IModalRadioGroupComponent.cs | 6 + .../Interaction/Components/LocalComponent.cs | 556 ++++++++++-------- .../Components/V2/LocalCheckboxComponent.cs | 31 + .../V2/LocalCheckboxGroupComponent.cs | 39 ++ .../Components/V2/LocalCheckboxGroupOption.cs | 49 ++ .../Components/V2/LocalRadioGroupComponent.cs | 33 ++ .../Components/V2/LocalRadioGroupOption.cs | 49 ++ .../Modal/TransientModalCheckboxComponent.cs | 13 + .../TransientModalCheckboxGroupComponent.cs | 14 + .../Modal/TransientModalComponent.cs | 3 + .../TransientModalRadioGroupComponent.cs | 13 + .../Interactions/Components/ComponentType.cs | 84 +-- .../Modal/ModalCheckboxComponentJsonModel.cs | 17 + .../ModalCheckboxGroupComponentJsonModel.cs | 17 + .../ModalRadioGroupComponentJsonModel.cs | 18 + .../V2/CheckboxComponentJsonModel.cs | 24 + .../V2/CheckboxGroupComponentJsonModel.cs | 30 + .../V2/CheckboxGroupOptionJsonModel.cs | 22 + .../V2/RadioGroupComponentJsonModel.cs | 24 + .../V2/RadioGroupOptionJsonModel.cs | 22 + 23 files changed, 797 insertions(+), 295 deletions(-) create mode 100644 src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalCheckboxComponent.cs create mode 100644 src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalCheckboxGroupComponent.cs create mode 100644 src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalRadioGroupComponent.cs create mode 100644 src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxComponent.cs create mode 100644 src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupComponent.cs create mode 100644 src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupOption.cs create mode 100644 src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupComponent.cs create mode 100644 src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupOption.cs create mode 100644 src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalCheckboxComponent.cs create mode 100644 src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalCheckboxGroupComponent.cs create mode 100644 src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalRadioGroupComponent.cs create mode 100644 src/Disqord.Core/Models/Interactions/Components/Modal/ModalCheckboxComponentJsonModel.cs create mode 100644 src/Disqord.Core/Models/Interactions/Components/Modal/ModalCheckboxGroupComponentJsonModel.cs create mode 100644 src/Disqord.Core/Models/Interactions/Components/Modal/ModalRadioGroupComponentJsonModel.cs create mode 100644 src/Disqord.Core/Models/Interactions/Components/V2/CheckboxComponentJsonModel.cs create mode 100644 src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupComponentJsonModel.cs create mode 100644 src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupOptionJsonModel.cs create mode 100644 src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupComponentJsonModel.cs create mode 100644 src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupOptionJsonModel.cs diff --git a/src/Disqord.Bot/Commands/Implementation/Components/ExecutionSteps/BindValues.cs b/src/Disqord.Bot/Commands/Implementation/Components/ExecutionSteps/BindValues.cs index 45653376..87b2ece3 100644 --- a/src/Disqord.Bot/Commands/Implementation/Components/ExecutionSteps/BindValues.cs +++ b/src/Disqord.Bot/Commands/Implementation/Components/ExecutionSteps/BindValues.cs @@ -190,6 +190,20 @@ protected virtual MultiString GetRawArgumentFromModalComponent(IModalComponent m { return ToMultiString(fileUploadComponent.AttachmentIds.Select(static id => id.ToString())); } + case IModalRadioGroupComponent radioGroupComponent: + { + return radioGroupComponent.Value != null + ? new MultiString(radioGroupComponent.Value) + : default; + } + case IModalCheckboxGroupComponent checkboxGroupComponent: + { + return ToMultiString(checkboxGroupComponent.Values); + } + case IModalCheckboxComponent checkboxComponent: + { + return new MultiString(checkboxComponent.Value.ToString()); + } default: { ThrowNotImplementedException($"{nameof(BindValues)}.{nameof(GetRawArgumentFromModalComponent)}() does not support the modal component of type: {modalComponent.Type} (ID: {modalComponent.Id})."); diff --git a/src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalCheckboxComponent.cs b/src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalCheckboxComponent.cs new file mode 100644 index 00000000..23ab967c --- /dev/null +++ b/src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalCheckboxComponent.cs @@ -0,0 +1,6 @@ +namespace Disqord; + +public interface IModalCheckboxComponent : IModalComponent, ICustomIdentifiableEntity +{ + bool Value { get; } +} diff --git a/src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalCheckboxGroupComponent.cs b/src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalCheckboxGroupComponent.cs new file mode 100644 index 00000000..5f299961 --- /dev/null +++ b/src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalCheckboxGroupComponent.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Disqord; + +public interface IModalCheckboxGroupComponent : IModalComponent, ICustomIdentifiableEntity +{ + IReadOnlyList Values { get; } +} diff --git a/src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalRadioGroupComponent.cs b/src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalRadioGroupComponent.cs new file mode 100644 index 00000000..8944e0b0 --- /dev/null +++ b/src/Disqord.Core/Entities/Core/Interactions/Components/Modal/IModalRadioGroupComponent.cs @@ -0,0 +1,6 @@ +namespace Disqord; + +public interface IModalRadioGroupComponent : IModalComponent, ICustomIdentifiableEntity +{ + string? Value { get; } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.cs index bff3ec61..ff03558b 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.cs @@ -1,256 +1,300 @@ -using System; -using System.Linq; -using Disqord.Models; -using Qommon; - -namespace Disqord; - -public abstract partial class LocalComponent : ILocalConstruct, IJsonConvertible -{ - /// - /// Gets or sets the ID of this component. - /// If not set, Discord will set it based on an incrementing value. - /// - public Optional Id { get; set; } - - protected LocalComponent() - { } - - protected LocalComponent(LocalComponent other) - { - Id = other.Id; - } - - /// - public abstract LocalComponent Clone(); - - /// - public virtual BaseComponentJsonModel ToModel() - { - if (!IsComponentV2()) - { - return CreateComponentJsonModel(); - } - - BaseComponentJsonModel model; - switch (this) - { - case LocalSectionComponent section: - { - model = new SectionComponentJsonModel - { - Components = Optional.ConvertOrDefault(section.Components, static components => components.Select(static component => component.ToModel()).ToArray()) ?? [], - Accessory = Optional.ConvertOrDefault(section.Accessory, static accessory => accessory.ToModel())! - }; - - break; - } - case LocalTextDisplayComponent textDisplay: - { - OptionalGuard.HasValue(textDisplay.Content); - - model = new TextDisplayComponentJsonModel - { - Content = textDisplay.Content.Value - }; - - break; - } - case LocalThumbnailComponent thumbnail: - { - OptionalGuard.HasValue(thumbnail.Media); - - model = new ThumbnailComponentJsonModel - { - Media = thumbnail.Media.Value.ToModel(), - Description = thumbnail.Description, - Spoiler = thumbnail.IsSpoiler - }; - - break; - } - case LocalMediaGalleryComponent mediaGallery: - { - model = new MediaGalleryComponentJsonModel - { - Items = Optional.ConvertOrDefault(mediaGallery.Items, static items => items.Select(static item => item.ToModel()).ToArray()) ?? [] - }; - - break; - } - case LocalFileComponent file: - { - OptionalGuard.HasValue(file.File); - - model = new FileComponentJsonModel - { - File = file.File.Value.ToModel(), - Spoiler = file.IsSpoiler - }; - - break; - } - case LocalSeparatorComponent separator: - { - model = new SeparatorComponentJsonModel - { - Divider = separator.IsDivider, - Spacing = separator.SpacingSize - }; - - break; - } - case LocalContainerComponent container: - { - model = new ContainerComponentJsonModel - { - Components = Optional.ConvertOrDefault(container.Components, static components => components.Select(static component => component.ToModel()).ToArray()) ?? [], - AccentColor = Optional.Convert(container.AccentColor, static color => color?.RawValue), - Spoiler = container.IsSpoiler - }; - - break; - } - case LocalLabelComponent label: - { - OptionalGuard.HasValue(label.Label); - OptionalGuard.HasValue(label.Component); - - model = new LabelComponentJsonModel - { - Label = label.Label.Value, - Description = label.Description, - Component = label.Component.Value.ToModel() - }; - - break; - } - case LocalFileUploadComponent fileUpload: - { - OptionalGuard.HasValue(fileUpload.CustomId); - - model = new FileUploadComponentJsonModel - { - CustomId = fileUpload.CustomId.Value, - MinValues = fileUpload.MinimumUploadedFiles, - MaxValues = fileUpload.MaximumUploadedFiles, - Required = fileUpload.IsRequired, - }; - - break; - } - default: - { - throw new InvalidOperationException("Unknown local component type."); - } - } - - model.Id = Id; - - return model; - } - - private ComponentJsonModel CreateComponentJsonModel() - { - // TODO: maybe split this via inheritance - var model = new ComponentJsonModel(); - - if (this is ILocalCustomIdentifiableEntity customIdentifiableEntity) - model.CustomId = customIdentifiableEntity.CustomId; - - if (this is LocalRowComponent rowComponent) - { - model.Type = ComponentType.Row; - model.Components = Optional.Convert(rowComponent.Components, components => components.Select(component => component.ToModel()).ToArray()); - } - else if (this is LocalButtonComponentBase buttonComponentBase) - { - model.Type = ComponentType.Button; - model.Label = buttonComponentBase.Label; - model.Emoji = Optional.Convert(buttonComponentBase.Emoji, emoji => emoji.ToModel()); - model.Disabled = buttonComponentBase.IsDisabled; - - if (buttonComponentBase is LocalButtonComponent buttonComponent) - { - model.Style = Optional.Convert(buttonComponent.Style, style => (byte) style); - } - else if (buttonComponentBase is LocalLinkButtonComponent linkButtonComponent) - { - model.Style = (byte) ButtonComponentStyle.Link; - model.Url = linkButtonComponent.Url; - } - else - { - throw new InvalidOperationException("Unknown local button component type."); - } - } - else if (this is LocalSelectionComponent selectionComponent) - { - model.Type = (ComponentType) selectionComponent.Type; - model.ChannelTypes = Optional.Convert(selectionComponent.ChannelTypes, channelTypes => channelTypes?.ToArray())!; - model.Placeholder = selectionComponent.Placeholder; - model.DefaultValues = Optional.Convert(selectionComponent.DefaultValues, defaultValues => defaultValues.Select(defaultValue => defaultValue.ToModel()).ToArray()); - model.MinValues = selectionComponent.MinimumSelectedOptions; - model.MaxValues = selectionComponent.MaximumSelectedOptions; - model.Disabled = selectionComponent.IsDisabled; - model.Options = Optional.Convert(selectionComponent.Options, options => options.Select(option => option.ToModel()).ToArray()); - model.Required = selectionComponent.IsRequired; - } - else if (this is LocalTextInputComponent textInputComponent) - { - model.Type = ComponentType.TextInput; - model.Style = Optional.Convert(textInputComponent.Style, style => (byte) style); - model.CustomId = textInputComponent.CustomId; - -#pragma warning disable CS0618 // Type or member is obsolete - model.Label = textInputComponent.Label; -#pragma warning restore CS0618 // Type or member is obsolete - - model.MinLength = textInputComponent.MinimumInputLength; - model.MaxLength = textInputComponent.MaximumInputLength; - model.Required = textInputComponent.IsRequired; - model.Value = textInputComponent.PrefilledValue; - model.Placeholder = textInputComponent.Placeholder; - } - else - { - throw new InvalidOperationException("Unknown local component type."); - } - - return model; - } - - /// - /// Converts the specified component to a . - /// - /// The component to convert. - /// - /// The output . - /// - public static LocalComponent CreateFrom(IComponent component) - { - return component switch - { - IRowComponent rowComponent => LocalRowComponent.CreateFrom(rowComponent), - IButtonComponent buttonComponent => LocalButtonComponentBase.CreateFrom(buttonComponent), - ISelectionComponent selectionComponent => LocalSelectionComponent.CreateFrom(selectionComponent), - ITextInputComponent textInputComponent => LocalTextInputComponent.CreateFrom(textInputComponent), - ISectionComponent sectionComponent => LocalSectionComponent.CreateFrom(sectionComponent), - ITextDisplayComponent textDisplayComponent => LocalTextDisplayComponent.CreateFrom(textDisplayComponent), - IThumbnailComponent thumbnailComponent => LocalThumbnailComponent.CreateFrom(thumbnailComponent), - IMediaGalleryComponent mediaGalleryComponent => LocalMediaGalleryComponent.CreateFrom(mediaGalleryComponent), - IFileComponent => Throw.ArgumentException( - "Cannot convert file components to local file components as they do not support arbitrary external urls." - + "You must use the `attachment://` reference system instead."), - ISeparatorComponent separatorComponent => LocalSeparatorComponent.CreateFrom(separatorComponent), - IContainerComponent containerComponent => LocalContainerComponent.CreateFrom(containerComponent), - ILabelComponent labelComponent => LocalLabelComponent.CreateFrom(labelComponent), - _ => Throw.ArgumentException("Unsupported component type.", nameof(component)) - }; - } - - internal bool IsComponentV2() - { - return this is not (LocalRowComponent or LocalButtonComponentBase or LocalSelectionComponent or LocalTextInputComponent); - } -} +using System; +using System.Linq; +using Disqord.Models; +using Qommon; + +namespace Disqord; + +public abstract partial class LocalComponent : ILocalConstruct, IJsonConvertible +{ + /// + /// Gets or sets the ID of this component. + /// If not set, Discord will set it based on an incrementing value. + /// + public Optional Id { get; set; } + + protected LocalComponent() + { } + + protected LocalComponent(LocalComponent other) + { + Id = other.Id; + } + + /// + public abstract LocalComponent Clone(); + + /// + public virtual BaseComponentJsonModel ToModel() + { + if (!IsComponentV2()) + { + return CreateComponentJsonModel(); + } + + BaseComponentJsonModel model; + switch (this) + { + case LocalSectionComponent section: + { + model = new SectionComponentJsonModel + { + Components = Optional.ConvertOrDefault(section.Components, static components => components.Select(static component => component.ToModel()).ToArray()) ?? [], + Accessory = Optional.ConvertOrDefault(section.Accessory, static accessory => accessory.ToModel())! + }; + + break; + } + case LocalTextDisplayComponent textDisplay: + { + OptionalGuard.HasValue(textDisplay.Content); + + model = new TextDisplayComponentJsonModel + { + Content = textDisplay.Content.Value + }; + + break; + } + case LocalThumbnailComponent thumbnail: + { + OptionalGuard.HasValue(thumbnail.Media); + + model = new ThumbnailComponentJsonModel + { + Media = thumbnail.Media.Value.ToModel(), + Description = thumbnail.Description, + Spoiler = thumbnail.IsSpoiler + }; + + break; + } + case LocalMediaGalleryComponent mediaGallery: + { + model = new MediaGalleryComponentJsonModel + { + Items = Optional.ConvertOrDefault(mediaGallery.Items, static items => items.Select(static item => item.ToModel()).ToArray()) ?? [] + }; + + break; + } + case LocalFileComponent file: + { + OptionalGuard.HasValue(file.File); + + model = new FileComponentJsonModel + { + File = file.File.Value.ToModel(), + Spoiler = file.IsSpoiler + }; + + break; + } + case LocalSeparatorComponent separator: + { + model = new SeparatorComponentJsonModel + { + Divider = separator.IsDivider, + Spacing = separator.SpacingSize + }; + + break; + } + case LocalContainerComponent container: + { + model = new ContainerComponentJsonModel + { + Components = Optional.ConvertOrDefault(container.Components, static components => components.Select(static component => component.ToModel()).ToArray()) ?? [], + AccentColor = Optional.Convert(container.AccentColor, static color => color?.RawValue), + Spoiler = container.IsSpoiler + }; + + break; + } + case LocalLabelComponent label: + { + OptionalGuard.HasValue(label.Label); + OptionalGuard.HasValue(label.Component); + + model = new LabelComponentJsonModel + { + Label = label.Label.Value, + Description = label.Description, + Component = label.Component.Value.ToModel() + }; + + break; + } + case LocalFileUploadComponent fileUpload: + { + OptionalGuard.HasValue(fileUpload.CustomId); + + model = new FileUploadComponentJsonModel + { + CustomId = fileUpload.CustomId.Value, + MinValues = fileUpload.MinimumUploadedFiles, + MaxValues = fileUpload.MaximumUploadedFiles, + Required = fileUpload.IsRequired, + }; + + break; + } + case LocalRadioGroupComponent radioGroup: + { + OptionalGuard.HasValue(radioGroup.CustomId); + + model = new RadioGroupComponentJsonModel + { + CustomId = radioGroup.CustomId.Value, + Options = radioGroup.Options.Select(static option => option.ToModel()).ToArray(), + Required = radioGroup.IsRequired, + Disabled = radioGroup.IsDisabled + }; + + break; + } + case LocalCheckboxGroupComponent checkboxGroup: + { + OptionalGuard.HasValue(checkboxGroup.CustomId); + + model = new CheckboxGroupComponentJsonModel + { + CustomId = checkboxGroup.CustomId.Value, + Options = checkboxGroup.Options.Select(static option => option.ToModel()).ToArray(), + MinValues = checkboxGroup.MinimumSelectedOptions, + MaxValues = checkboxGroup.MaximumSelectedOptions, + Required = checkboxGroup.IsRequired, + Disabled = checkboxGroup.IsDisabled + }; + + break; + } + case LocalCheckboxComponent checkbox: + { + OptionalGuard.HasValue(checkbox.CustomId); + + model = new CheckboxComponentJsonModel + { + CustomId = checkbox.CustomId.Value, + Default = checkbox.IsDefault, + Label = checkbox.Label, + Disabled = checkbox.IsDisabled + }; + + break; + } + default: + { + throw new InvalidOperationException("Unknown local component type."); + } + } + + model.Id = Id; + + return model; + } + + private ComponentJsonModel CreateComponentJsonModel() + { + // TODO: maybe split this via inheritance + var model = new ComponentJsonModel(); + + if (this is ILocalCustomIdentifiableEntity customIdentifiableEntity) + model.CustomId = customIdentifiableEntity.CustomId; + + if (this is LocalRowComponent rowComponent) + { + model.Type = ComponentType.Row; + model.Components = Optional.Convert(rowComponent.Components, components => components.Select(component => component.ToModel()).ToArray()); + } + else if (this is LocalButtonComponentBase buttonComponentBase) + { + model.Type = ComponentType.Button; + model.Label = buttonComponentBase.Label; + model.Emoji = Optional.Convert(buttonComponentBase.Emoji, emoji => emoji.ToModel()); + model.Disabled = buttonComponentBase.IsDisabled; + + if (buttonComponentBase is LocalButtonComponent buttonComponent) + { + model.Style = Optional.Convert(buttonComponent.Style, style => (byte) style); + } + else if (buttonComponentBase is LocalLinkButtonComponent linkButtonComponent) + { + model.Style = (byte) ButtonComponentStyle.Link; + model.Url = linkButtonComponent.Url; + } + else + { + throw new InvalidOperationException("Unknown local button component type."); + } + } + else if (this is LocalSelectionComponent selectionComponent) + { + model.Type = (ComponentType) selectionComponent.Type; + model.ChannelTypes = Optional.Convert(selectionComponent.ChannelTypes, channelTypes => channelTypes?.ToArray())!; + model.Placeholder = selectionComponent.Placeholder; + model.DefaultValues = Optional.Convert(selectionComponent.DefaultValues, defaultValues => defaultValues.Select(defaultValue => defaultValue.ToModel()).ToArray()); + model.MinValues = selectionComponent.MinimumSelectedOptions; + model.MaxValues = selectionComponent.MaximumSelectedOptions; + model.Disabled = selectionComponent.IsDisabled; + model.Options = Optional.Convert(selectionComponent.Options, options => options.Select(option => option.ToModel()).ToArray()); + model.Required = selectionComponent.IsRequired; + } + else if (this is LocalTextInputComponent textInputComponent) + { + model.Type = ComponentType.TextInput; + model.Style = Optional.Convert(textInputComponent.Style, style => (byte) style); + model.CustomId = textInputComponent.CustomId; + +#pragma warning disable CS0618 // Type or member is obsolete + model.Label = textInputComponent.Label; +#pragma warning restore CS0618 // Type or member is obsolete + + model.MinLength = textInputComponent.MinimumInputLength; + model.MaxLength = textInputComponent.MaximumInputLength; + model.Required = textInputComponent.IsRequired; + model.Value = textInputComponent.PrefilledValue; + model.Placeholder = textInputComponent.Placeholder; + } + else + { + throw new InvalidOperationException("Unknown local component type."); + } + + return model; + } + + /// + /// Converts the specified component to a . + /// + /// The component to convert. + /// + /// The output . + /// + public static LocalComponent CreateFrom(IComponent component) + { + return component switch + { + IRowComponent rowComponent => LocalRowComponent.CreateFrom(rowComponent), + IButtonComponent buttonComponent => LocalButtonComponentBase.CreateFrom(buttonComponent), + ISelectionComponent selectionComponent => LocalSelectionComponent.CreateFrom(selectionComponent), + ITextInputComponent textInputComponent => LocalTextInputComponent.CreateFrom(textInputComponent), + ISectionComponent sectionComponent => LocalSectionComponent.CreateFrom(sectionComponent), + ITextDisplayComponent textDisplayComponent => LocalTextDisplayComponent.CreateFrom(textDisplayComponent), + IThumbnailComponent thumbnailComponent => LocalThumbnailComponent.CreateFrom(thumbnailComponent), + IMediaGalleryComponent mediaGalleryComponent => LocalMediaGalleryComponent.CreateFrom(mediaGalleryComponent), + IFileComponent => Throw.ArgumentException( + "Cannot convert file components to local file components as they do not support arbitrary external urls." + + "You must use the `attachment://` reference system instead."), + ISeparatorComponent separatorComponent => LocalSeparatorComponent.CreateFrom(separatorComponent), + IContainerComponent containerComponent => LocalContainerComponent.CreateFrom(containerComponent), + ILabelComponent labelComponent => LocalLabelComponent.CreateFrom(labelComponent), + _ => Throw.ArgumentException("Unsupported component type.", nameof(component)) + }; + } + + internal bool IsComponentV2() + { + return this is not (LocalRowComponent or LocalButtonComponentBase or LocalSelectionComponent or LocalTextInputComponent); + } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxComponent.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxComponent.cs new file mode 100644 index 00000000..4bc14bf7 --- /dev/null +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxComponent.cs @@ -0,0 +1,31 @@ +using Disqord.Models; +using Qommon; + +namespace Disqord; + +public class LocalCheckboxComponent : LocalComponent, ILocalCustomIdentifiableEntity, ILocalConstruct +{ + public Optional CustomId { get; set; } + + public Optional IsDefault { get; set; } + + public new Optional Label { get; set; } + + public Optional IsDisabled { get; set; } + + public LocalCheckboxComponent() + { } + + protected LocalCheckboxComponent(LocalCheckboxComponent other) : base(other) + { + CustomId = other.CustomId; + IsDefault = other.IsDefault; + Label = other.Label; + IsDisabled = other.IsDisabled; + } + + public override LocalCheckboxComponent Clone() + { + return new(this); + } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupComponent.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupComponent.cs new file mode 100644 index 00000000..36519ba1 --- /dev/null +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupComponent.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Disqord.Models; +using Qommon; + +namespace Disqord; + +public class LocalCheckboxGroupComponent : LocalComponent, ILocalCustomIdentifiableEntity, ILocalConstruct +{ + public Optional CustomId { get; set; } + + public IList Options { get; set; } = new List(); + + public Optional MinimumSelectedOptions { get; set; } + + public Optional MaximumSelectedOptions { get; set; } + + public Optional IsRequired { get; set; } + + public Optional IsDisabled { get; set; } + + public LocalCheckboxGroupComponent() + { } + + protected LocalCheckboxGroupComponent(LocalCheckboxGroupComponent other) : base(other) + { + CustomId = other.CustomId; + Options = other.Options.Select(x => x.Clone()).ToList(); + MinimumSelectedOptions = other.MinimumSelectedOptions; + MaximumSelectedOptions = other.MaximumSelectedOptions; + IsRequired = other.IsRequired; + IsDisabled = other.IsDisabled; + } + + public override LocalCheckboxGroupComponent Clone() + { + return new(this); + } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupOption.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupOption.cs new file mode 100644 index 00000000..b3a0eed6 --- /dev/null +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupOption.cs @@ -0,0 +1,49 @@ +using Disqord.Models; +using Qommon; + +namespace Disqord; + +public class LocalCheckboxGroupOption : ILocalConstruct, IJsonConvertible +{ + public string Label { get; set; } + + public string Value { get; set; } + + public Optional Description { get; set; } + + public Optional Emoji { get; set; } + + public Optional IsDefault { get; set; } + + public LocalCheckboxGroupOption(string label, string value) + { + Label = label; + Value = value; + } + + protected LocalCheckboxGroupOption(LocalCheckboxGroupOption other) + { + Label = other.Label; + Value = other.Value; + Description = other.Description; + Emoji = other.Emoji; + IsDefault = other.IsDefault; + } + + public LocalCheckboxGroupOption Clone() + { + return new(this); + } + + public CheckboxGroupOptionJsonModel ToModel() + { + return new CheckboxGroupOptionJsonModel + { + Label = Label, + Value = Value, + Description = Description, + Emoji = Optional.Convert(Emoji, static emoji => emoji.ToModel()), + Default = IsDefault + }; + } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupComponent.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupComponent.cs new file mode 100644 index 00000000..89fb317f --- /dev/null +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupComponent.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using Disqord.Models; +using Qommon; + +namespace Disqord; + +public class LocalRadioGroupComponent : LocalComponent, ILocalCustomIdentifiableEntity, ILocalConstruct +{ + public Optional CustomId { get; set; } + + public IList Options { get; set; } = new List(); + + public Optional IsRequired { get; set; } + + public Optional IsDisabled { get; set; } + + public LocalRadioGroupComponent() + { } + + protected LocalRadioGroupComponent(LocalRadioGroupComponent other) : base(other) + { + CustomId = other.CustomId; + Options = other.Options.Select(x => x.Clone()).ToList(); + IsRequired = other.IsRequired; + IsDisabled = other.IsDisabled; + } + + public override LocalRadioGroupComponent Clone() + { + return new(this); + } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupOption.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupOption.cs new file mode 100644 index 00000000..99bdf8fd --- /dev/null +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupOption.cs @@ -0,0 +1,49 @@ +using Disqord.Models; +using Qommon; + +namespace Disqord; + +public class LocalRadioGroupOption : ILocalConstruct, IJsonConvertible +{ + public string Label { get; set; } + + public string Value { get; set; } + + public Optional Description { get; set; } + + public Optional Emoji { get; set; } + + public Optional IsDefault { get; set; } + + public LocalRadioGroupOption(string label, string value) + { + Label = label; + Value = value; + } + + protected LocalRadioGroupOption(LocalRadioGroupOption other) + { + Label = other.Label; + Value = other.Value; + Description = other.Description; + Emoji = other.Emoji; + IsDefault = other.IsDefault; + } + + public LocalRadioGroupOption Clone() + { + return new(this); + } + + public RadioGroupOptionJsonModel ToModel() + { + return new RadioGroupOptionJsonModel + { + Label = Label, + Value = Value, + Description = Description, + Emoji = Optional.Convert(Emoji, static emoji => emoji.ToModel()), + Default = IsDefault + }; + } +} diff --git a/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalCheckboxComponent.cs b/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalCheckboxComponent.cs new file mode 100644 index 00000000..48128075 --- /dev/null +++ b/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalCheckboxComponent.cs @@ -0,0 +1,13 @@ +using Disqord.Models; + +namespace Disqord; + +public class TransientModalCheckboxComponent : TransientModalComponent, IModalCheckboxComponent +{ + public string CustomId => Model.CustomId; + + public bool Value => Model.Value; + + public TransientModalCheckboxComponent(ModalCheckboxComponentJsonModel model) : base(model) + { } +} diff --git a/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalCheckboxGroupComponent.cs b/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalCheckboxGroupComponent.cs new file mode 100644 index 00000000..691a0318 --- /dev/null +++ b/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalCheckboxGroupComponent.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Disqord.Models; + +namespace Disqord; + +public class TransientModalCheckboxGroupComponent : TransientModalComponent, IModalCheckboxGroupComponent +{ + public string CustomId => Model.CustomId; + + public IReadOnlyList Values => Model.Values; + + public TransientModalCheckboxGroupComponent(ModalCheckboxGroupComponentJsonModel model) : base(model) + { } +} diff --git a/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalComponent.cs b/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalComponent.cs index 80add4fe..90b88827 100644 --- a/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalComponent.cs +++ b/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalComponent.cs @@ -15,6 +15,9 @@ public static IModalComponent Create(ModalBaseComponentJsonModel model) ComponentType.TextDisplay => new TransientModalTextDisplayComponent(Guard.IsOfType(model)), ComponentType.Label => new TransientModalLabelComponent(Guard.IsOfType(model)), ComponentType.FileUpload => new TransientModalFileUploadComponent(Guard.IsOfType(model)), + ComponentType.RadioGroup => new TransientModalRadioGroupComponent(Guard.IsOfType(model)), + ComponentType.CheckboxGroup => new TransientModalCheckboxGroupComponent(Guard.IsOfType(model)), + ComponentType.Checkbox => new TransientModalCheckboxComponent(Guard.IsOfType(model)), _ => new TransientModalComponent(model) }; } diff --git a/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalRadioGroupComponent.cs b/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalRadioGroupComponent.cs new file mode 100644 index 00000000..c7a82105 --- /dev/null +++ b/src/Disqord.Core/Entities/Transient/Interactions/Components/Modal/TransientModalRadioGroupComponent.cs @@ -0,0 +1,13 @@ +using Disqord.Models; + +namespace Disqord; + +public class TransientModalRadioGroupComponent : TransientModalComponent, IModalRadioGroupComponent +{ + public string CustomId => Model.CustomId; + + public string? Value => Model.Value.HasValue ? Model.Value.Value : null; + + public TransientModalRadioGroupComponent(ModalRadioGroupComponentJsonModel model) : base(model) + { } +} diff --git a/src/Disqord.Core/Enums/Interactions/Components/ComponentType.cs b/src/Disqord.Core/Enums/Interactions/Components/ComponentType.cs index f8d8e09b..0a82ee99 100644 --- a/src/Disqord.Core/Enums/Interactions/Components/ComponentType.cs +++ b/src/Disqord.Core/Enums/Interactions/Components/ComponentType.cs @@ -1,39 +1,45 @@ -namespace Disqord; - -public enum ComponentType : byte -{ - Row = 1, - - Button = 2, - - StringSelection = 3, - - TextInput = 4, - - UserSelection = 5, - - RoleSelection = 6, - - MentionableSelection = 7, - - ChannelSelection = 8, - - // Components V2 - Section = 9, - - TextDisplay = 10, - - Thumbnail = 11, - - MediaGallery = 12, - - File = 13, - - Separator = 14, - - Container = 17, - - Label = 18, - - FileUpload = 19, -} +namespace Disqord; + +public enum ComponentType : byte +{ + Row = 1, + + Button = 2, + + StringSelection = 3, + + TextInput = 4, + + UserSelection = 5, + + RoleSelection = 6, + + MentionableSelection = 7, + + ChannelSelection = 8, + + // Components V2 + Section = 9, + + TextDisplay = 10, + + Thumbnail = 11, + + MediaGallery = 12, + + File = 13, + + Separator = 14, + + Container = 17, + + Label = 18, + + FileUpload = 19, + + RadioGroup = 21, + + CheckboxGroup = 22, + + Checkbox = 23, +} diff --git a/src/Disqord.Core/Models/Interactions/Components/Modal/ModalCheckboxComponentJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/Modal/ModalCheckboxComponentJsonModel.cs new file mode 100644 index 00000000..0dcdbaa6 --- /dev/null +++ b/src/Disqord.Core/Models/Interactions/Components/Modal/ModalCheckboxComponentJsonModel.cs @@ -0,0 +1,17 @@ +using Disqord.Serialization.Json; + +namespace Disqord.Models; + +public class ModalCheckboxComponentJsonModel : ModalBaseComponentJsonModel +{ + [JsonProperty("custom_id")] + public string CustomId = null!; + + [JsonProperty("value")] + public bool Value; + + public ModalCheckboxComponentJsonModel() + { + Type = ComponentType.Checkbox; + } +} diff --git a/src/Disqord.Core/Models/Interactions/Components/Modal/ModalCheckboxGroupComponentJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/Modal/ModalCheckboxGroupComponentJsonModel.cs new file mode 100644 index 00000000..79feffe1 --- /dev/null +++ b/src/Disqord.Core/Models/Interactions/Components/Modal/ModalCheckboxGroupComponentJsonModel.cs @@ -0,0 +1,17 @@ +using Disqord.Serialization.Json; + +namespace Disqord.Models; + +public class ModalCheckboxGroupComponentJsonModel : ModalBaseComponentJsonModel +{ + [JsonProperty("custom_id")] + public string CustomId = null!; + + [JsonProperty("values")] + public string[] Values = null!; + + public ModalCheckboxGroupComponentJsonModel() + { + Type = ComponentType.CheckboxGroup; + } +} diff --git a/src/Disqord.Core/Models/Interactions/Components/Modal/ModalRadioGroupComponentJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/Modal/ModalRadioGroupComponentJsonModel.cs new file mode 100644 index 00000000..8a241e05 --- /dev/null +++ b/src/Disqord.Core/Models/Interactions/Components/Modal/ModalRadioGroupComponentJsonModel.cs @@ -0,0 +1,18 @@ +using Disqord.Serialization.Json; +using Qommon; + +namespace Disqord.Models; + +public class ModalRadioGroupComponentJsonModel : ModalBaseComponentJsonModel +{ + [JsonProperty("custom_id")] + public string CustomId = null!; + + [JsonProperty("value")] + public Optional Value; + + public ModalRadioGroupComponentJsonModel() + { + Type = ComponentType.RadioGroup; + } +} diff --git a/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxComponentJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxComponentJsonModel.cs new file mode 100644 index 00000000..43e66c65 --- /dev/null +++ b/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxComponentJsonModel.cs @@ -0,0 +1,24 @@ +using Disqord.Serialization.Json; +using Qommon; + +namespace Disqord.Models; + +public class CheckboxComponentJsonModel : BaseComponentJsonModel +{ + [JsonProperty("custom_id")] + public string CustomId = null!; + + [JsonProperty("default")] + public Optional Default; + + [JsonProperty("label")] + public Optional Label; + + [JsonProperty("disabled")] + public Optional Disabled; + + public CheckboxComponentJsonModel() + { + Type = ComponentType.Checkbox; + } +} diff --git a/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupComponentJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupComponentJsonModel.cs new file mode 100644 index 00000000..812f9a19 --- /dev/null +++ b/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupComponentJsonModel.cs @@ -0,0 +1,30 @@ +using Disqord.Serialization.Json; +using Qommon; + +namespace Disqord.Models; + +public class CheckboxGroupComponentJsonModel : BaseComponentJsonModel +{ + [JsonProperty("custom_id")] + public string CustomId = null!; + + [JsonProperty("options")] + public CheckboxGroupOptionJsonModel[] Options = null!; + + [JsonProperty("min_values")] + public Optional MinValues; + + [JsonProperty("max_values")] + public Optional MaxValues; + + [JsonProperty("required")] + public Optional Required; + + [JsonProperty("disabled")] + public Optional Disabled; + + public CheckboxGroupComponentJsonModel() + { + Type = ComponentType.CheckboxGroup; + } +} diff --git a/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupOptionJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupOptionJsonModel.cs new file mode 100644 index 00000000..bfd4f3fd --- /dev/null +++ b/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupOptionJsonModel.cs @@ -0,0 +1,22 @@ +using Disqord.Serialization.Json; +using Qommon; + +namespace Disqord.Models; + +public class CheckboxGroupOptionJsonModel : JsonModel +{ + [JsonProperty("label")] + public string Label = null!; + + [JsonProperty("value")] + public string Value = null!; + + [JsonProperty("description")] + public Optional Description; + + [JsonProperty("emoji")] + public Optional Emoji; + + [JsonProperty("default")] + public Optional Default; +} diff --git a/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupComponentJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupComponentJsonModel.cs new file mode 100644 index 00000000..504849ba --- /dev/null +++ b/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupComponentJsonModel.cs @@ -0,0 +1,24 @@ +using Disqord.Serialization.Json; +using Qommon; + +namespace Disqord.Models; + +public class RadioGroupComponentJsonModel : BaseComponentJsonModel +{ + [JsonProperty("custom_id")] + public string CustomId = null!; + + [JsonProperty("options")] + public RadioGroupOptionJsonModel[] Options = null!; + + [JsonProperty("required")] + public Optional Required; + + [JsonProperty("disabled")] + public Optional Disabled; + + public RadioGroupComponentJsonModel() + { + Type = ComponentType.RadioGroup; + } +} diff --git a/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupOptionJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupOptionJsonModel.cs new file mode 100644 index 00000000..a18bf726 --- /dev/null +++ b/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupOptionJsonModel.cs @@ -0,0 +1,22 @@ +using Disqord.Serialization.Json; +using Qommon; + +namespace Disqord.Models; + +public class RadioGroupOptionJsonModel : JsonModel +{ + [JsonProperty("label")] + public string Label = null!; + + [JsonProperty("value")] + public string Value = null!; + + [JsonProperty("description")] + public Optional Description; + + [JsonProperty("emoji")] + public Optional Emoji; + + [JsonProperty("default")] + public Optional Default; +} From 9e1cc3be0fdc94795ef173b2dbcc3705b10d6129 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 01:08:23 +0000 Subject: [PATCH 2/6] Implement new modal components: RadioGroup, CheckboxGroup, Checkbox This commit introduces support for the new Discord components: Radio Group, Checkbox Group, and Checkbox. Key changes: - Added `ComponentType` enum values for `RadioGroup`, `CheckboxGroup`, and `Checkbox`. - Created JSON models for local and modal (transient) representations of these components. - Implemented `LocalComponent` classes (`LocalRadioGroupComponent`, `LocalCheckboxGroupComponent`, `LocalCheckboxComponent`) for constructing these components. - Implemented `IModalComponent` interfaces and `TransientModalComponent` classes for handling received components in modal submissions. - Updated `LocalComponent.ToModel()` to serialize the new components properly. - Updated `BindValues` command execution step to support parameter binding for these new component types in modal commands. - Added fluent builder extension methods for the new local components. Co-authored-by: Quahu <24848652+Quahu@users.noreply.github.com> --- .../LocalCheckboxComponentExtensions.cs | 32 ++++++++++ .../LocalCheckboxGroupComponentExtensions.cs | 63 +++++++++++++++++++ .../LocalRadioGroupComponentExtensions.cs | 49 +++++++++++++++ .../Interaction/Components/LocalComponent.cs | 4 +- .../V2/LocalCheckboxGroupComponent.cs | 5 +- .../Components/V2/LocalRadioGroupComponent.cs | 5 +- .../V2/CheckboxGroupComponentJsonModel.cs | 2 +- .../V2/RadioGroupComponentJsonModel.cs | 2 +- 8 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxComponentExtensions.cs create mode 100644 src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs create mode 100644 src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxComponentExtensions.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxComponentExtensions.cs new file mode 100644 index 00000000..f8d9ef97 --- /dev/null +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxComponentExtensions.cs @@ -0,0 +1,32 @@ +namespace Disqord; + +public static class LocalCheckboxComponentExtensions +{ + public static TCheckboxComponent WithCustomId(this TCheckboxComponent checkboxComponent, string customId) + where TCheckboxComponent : LocalCheckboxComponent + { + checkboxComponent.CustomId = customId; + return checkboxComponent; + } + + public static TCheckboxComponent WithLabel(this TCheckboxComponent checkboxComponent, string label) + where TCheckboxComponent : LocalCheckboxComponent + { + checkboxComponent.Label = label; + return checkboxComponent; + } + + public static TCheckboxComponent WithIsDefault(this TCheckboxComponent checkboxComponent, bool isDefault = true) + where TCheckboxComponent : LocalCheckboxComponent + { + checkboxComponent.IsDefault = isDefault; + return checkboxComponent; + } + + public static TCheckboxComponent WithIsDisabled(this TCheckboxComponent checkboxComponent, bool isDisabled = true) + where TCheckboxComponent : LocalCheckboxComponent + { + checkboxComponent.IsDisabled = isDisabled; + return checkboxComponent; + } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs new file mode 100644 index 00000000..539fb217 --- /dev/null +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using Qommon; + +namespace Disqord; + +public static class LocalCheckboxGroupComponentExtensions +{ + public static TCheckboxGroupComponent WithCustomId(this TCheckboxGroupComponent checkboxGroupComponent, string customId) + where TCheckboxGroupComponent : LocalCheckboxGroupComponent + { + checkboxGroupComponent.CustomId = customId; + return checkboxGroupComponent; + } + + public static TCheckboxGroupComponent WithOptions(this TCheckboxGroupComponent checkboxGroupComponent, IEnumerable options) + where TCheckboxGroupComponent : LocalCheckboxGroupComponent + { + if (checkboxGroupComponent.Options.HasValue) + { + checkboxGroupComponent.Options.Value.Clear(); + foreach (var option in options) + checkboxGroupComponent.Options.Value.Add(option); + } + else + { + checkboxGroupComponent.Options = new List(options); + } + + return checkboxGroupComponent; + } + + public static TCheckboxGroupComponent WithOptions(this TCheckboxGroupComponent checkboxGroupComponent, params LocalCheckboxGroupOption[] options) + where TCheckboxGroupComponent : LocalCheckboxGroupComponent + => checkboxGroupComponent.WithOptions((IEnumerable) options); + + public static TCheckboxGroupComponent WithMinimumSelectedOptions(this TCheckboxGroupComponent checkboxGroupComponent, int minimumSelectedOptions) + where TCheckboxGroupComponent : LocalCheckboxGroupComponent + { + checkboxGroupComponent.MinimumSelectedOptions = minimumSelectedOptions; + return checkboxGroupComponent; + } + + public static TCheckboxGroupComponent WithMaximumSelectedOptions(this TCheckboxGroupComponent checkboxGroupComponent, int maximumSelectedOptions) + where TCheckboxGroupComponent : LocalCheckboxGroupComponent + { + checkboxGroupComponent.MaximumSelectedOptions = maximumSelectedOptions; + return checkboxGroupComponent; + } + + public static TCheckboxGroupComponent WithIsRequired(this TCheckboxGroupComponent checkboxGroupComponent, bool isRequired = true) + where TCheckboxGroupComponent : LocalCheckboxGroupComponent + { + checkboxGroupComponent.IsRequired = isRequired; + return checkboxGroupComponent; + } + + public static TCheckboxGroupComponent WithIsDisabled(this TCheckboxGroupComponent checkboxGroupComponent, bool isDisabled = true) + where TCheckboxGroupComponent : LocalCheckboxGroupComponent + { + checkboxGroupComponent.IsDisabled = isDisabled; + return checkboxGroupComponent; + } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs new file mode 100644 index 00000000..f2795adb --- /dev/null +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Qommon; + +namespace Disqord; + +public static class LocalRadioGroupComponentExtensions +{ + public static TRadioGroupComponent WithCustomId(this TRadioGroupComponent radioGroupComponent, string customId) + where TRadioGroupComponent : LocalRadioGroupComponent + { + radioGroupComponent.CustomId = customId; + return radioGroupComponent; + } + + public static TRadioGroupComponent WithOptions(this TRadioGroupComponent radioGroupComponent, IEnumerable options) + where TRadioGroupComponent : LocalRadioGroupComponent + { + if (radioGroupComponent.Options.HasValue) + { + radioGroupComponent.Options.Value.Clear(); + foreach (var option in options) + radioGroupComponent.Options.Value.Add(option); + } + else + { + radioGroupComponent.Options = new List(options); + } + + return radioGroupComponent; + } + + public static TRadioGroupComponent WithOptions(this TRadioGroupComponent radioGroupComponent, params LocalRadioGroupOption[] options) + where TRadioGroupComponent : LocalRadioGroupComponent + => radioGroupComponent.WithOptions((IEnumerable) options); + + public static TRadioGroupComponent WithIsRequired(this TRadioGroupComponent radioGroupComponent, bool isRequired = true) + where TRadioGroupComponent : LocalRadioGroupComponent + { + radioGroupComponent.IsRequired = isRequired; + return radioGroupComponent; + } + + public static TRadioGroupComponent WithIsDisabled(this TRadioGroupComponent radioGroupComponent, bool isDisabled = true) + where TRadioGroupComponent : LocalRadioGroupComponent + { + radioGroupComponent.IsDisabled = isDisabled; + return radioGroupComponent; + } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.cs index ff03558b..c601c9ba 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.cs @@ -146,7 +146,7 @@ public virtual BaseComponentJsonModel ToModel() model = new RadioGroupComponentJsonModel { CustomId = radioGroup.CustomId.Value, - Options = radioGroup.Options.Select(static option => option.ToModel()).ToArray(), + Options = Optional.Convert(radioGroup.Options, static options => options.Select(static option => option.ToModel()).ToArray()), Required = radioGroup.IsRequired, Disabled = radioGroup.IsDisabled }; @@ -160,7 +160,7 @@ public virtual BaseComponentJsonModel ToModel() model = new CheckboxGroupComponentJsonModel { CustomId = checkboxGroup.CustomId.Value, - Options = checkboxGroup.Options.Select(static option => option.ToModel()).ToArray(), + Options = Optional.Convert(checkboxGroup.Options, static options => options.Select(static option => option.ToModel()).ToArray()), MinValues = checkboxGroup.MinimumSelectedOptions, MaxValues = checkboxGroup.MaximumSelectedOptions, Required = checkboxGroup.IsRequired, diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupComponent.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupComponent.cs index 36519ba1..15a5fadf 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupComponent.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupComponent.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Disqord.Models; using Qommon; @@ -9,7 +8,7 @@ public class LocalCheckboxGroupComponent : LocalComponent, ILocalCustomIdentifia { public Optional CustomId { get; set; } - public IList Options { get; set; } = new List(); + public Optional> Options { get; set; } public Optional MinimumSelectedOptions { get; set; } @@ -25,7 +24,7 @@ public LocalCheckboxGroupComponent() protected LocalCheckboxGroupComponent(LocalCheckboxGroupComponent other) : base(other) { CustomId = other.CustomId; - Options = other.Options.Select(x => x.Clone()).ToList(); + Options = other.Options.DeepClone(); MinimumSelectedOptions = other.MinimumSelectedOptions; MaximumSelectedOptions = other.MaximumSelectedOptions; IsRequired = other.IsRequired; diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupComponent.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupComponent.cs index 89fb317f..a97ab792 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupComponent.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupComponent.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Disqord.Models; using Qommon; @@ -9,7 +8,7 @@ public class LocalRadioGroupComponent : LocalComponent, ILocalCustomIdentifiable { public Optional CustomId { get; set; } - public IList Options { get; set; } = new List(); + public Optional> Options { get; set; } public Optional IsRequired { get; set; } @@ -21,7 +20,7 @@ public LocalRadioGroupComponent() protected LocalRadioGroupComponent(LocalRadioGroupComponent other) : base(other) { CustomId = other.CustomId; - Options = other.Options.Select(x => x.Clone()).ToList(); + Options = other.Options.DeepClone(); IsRequired = other.IsRequired; IsDisabled = other.IsDisabled; } diff --git a/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupComponentJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupComponentJsonModel.cs index 812f9a19..e508732d 100644 --- a/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupComponentJsonModel.cs +++ b/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupComponentJsonModel.cs @@ -9,7 +9,7 @@ public class CheckboxGroupComponentJsonModel : BaseComponentJsonModel public string CustomId = null!; [JsonProperty("options")] - public CheckboxGroupOptionJsonModel[] Options = null!; + public Optional Options; [JsonProperty("min_values")] public Optional MinValues; diff --git a/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupComponentJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupComponentJsonModel.cs index 504849ba..585ec1bb 100644 --- a/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupComponentJsonModel.cs +++ b/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupComponentJsonModel.cs @@ -9,7 +9,7 @@ public class RadioGroupComponentJsonModel : BaseComponentJsonModel public string CustomId = null!; [JsonProperty("options")] - public RadioGroupOptionJsonModel[] Options = null!; + public Optional Options; [JsonProperty("required")] public Optional Required; From 074401de77643b6ef1eb913d0ceed3dff2a30c8b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 01:22:07 +0000 Subject: [PATCH 3/6] Implement new modal components: RadioGroup, CheckboxGroup, Checkbox This commit introduces support for the new Discord components: Radio Group, Checkbox Group, and Checkbox. Key changes: - Added `ComponentType` enum values for `RadioGroup`, `CheckboxGroup`, and `Checkbox`. - Created JSON models for local and modal (transient) representations of these components. - Implemented `LocalComponent` classes (`LocalRadioGroupComponent`, `LocalCheckboxGroupComponent`, `LocalCheckboxComponent`) for constructing these components. - Implemented `IModalComponent` interfaces and `TransientModalComponent` classes for handling received components in modal submissions. - Updated `LocalComponent.ToModel()` to serialize the new components properly. - Updated `BindValues` command execution step to support parameter binding for these new component types in modal commands. - Added fluent builder extension methods for the new local components. - Ensured consistency with existing component patterns, including usage of `Optional` and generic extension methods. Co-authored-by: Quahu <24848652+Quahu@users.noreply.github.com> --- .../Extensions/LocalCheckboxComponentExtensions.cs | 7 ------- .../Extensions/LocalCheckboxGroupComponentExtensions.cs | 7 ------- .../Extensions/LocalRadioGroupComponentExtensions.cs | 7 ------- 3 files changed, 21 deletions(-) diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxComponentExtensions.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxComponentExtensions.cs index f8d9ef97..4ba3deb8 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxComponentExtensions.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxComponentExtensions.cs @@ -2,13 +2,6 @@ namespace Disqord; public static class LocalCheckboxComponentExtensions { - public static TCheckboxComponent WithCustomId(this TCheckboxComponent checkboxComponent, string customId) - where TCheckboxComponent : LocalCheckboxComponent - { - checkboxComponent.CustomId = customId; - return checkboxComponent; - } - public static TCheckboxComponent WithLabel(this TCheckboxComponent checkboxComponent, string label) where TCheckboxComponent : LocalCheckboxComponent { diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs index 539fb217..8b51b3e3 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs @@ -5,13 +5,6 @@ namespace Disqord; public static class LocalCheckboxGroupComponentExtensions { - public static TCheckboxGroupComponent WithCustomId(this TCheckboxGroupComponent checkboxGroupComponent, string customId) - where TCheckboxGroupComponent : LocalCheckboxGroupComponent - { - checkboxGroupComponent.CustomId = customId; - return checkboxGroupComponent; - } - public static TCheckboxGroupComponent WithOptions(this TCheckboxGroupComponent checkboxGroupComponent, IEnumerable options) where TCheckboxGroupComponent : LocalCheckboxGroupComponent { diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs index f2795adb..c254aacb 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs @@ -5,13 +5,6 @@ namespace Disqord; public static class LocalRadioGroupComponentExtensions { - public static TRadioGroupComponent WithCustomId(this TRadioGroupComponent radioGroupComponent, string customId) - where TRadioGroupComponent : LocalRadioGroupComponent - { - radioGroupComponent.CustomId = customId; - return radioGroupComponent; - } - public static TRadioGroupComponent WithOptions(this TRadioGroupComponent radioGroupComponent, IEnumerable options) where TRadioGroupComponent : LocalRadioGroupComponent { From 83d0e4bbbd083be6580d7f05d57c47f714a27be1 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 01:51:36 +0000 Subject: [PATCH 4/6] Implement new modal components: RadioGroup, CheckboxGroup, Checkbox This commit introduces full support for the new Discord modal components: Radio Group, Checkbox Group, and Checkbox. Key changes: - Added `ComponentType` enum values for `RadioGroup`, `CheckboxGroup`, and `Checkbox`. - Created JSON models for local and modal (transient) representations of these components. - Implemented `LocalComponent` classes (`LocalRadioGroupComponent`, `LocalCheckboxGroupComponent`, `LocalCheckboxComponent`) and their option classes. - Implemented `IModalComponent` interfaces and `TransientModalComponent` classes. - Updated `LocalComponent.ToModel()` to serialize the new components properly, using `OptionalGuard` for validation. - Updated `ModalComponentConverter` to handle polymorphic deserialization of the new component types. - Updated `BindValues` command execution step to support parameter binding for these new component types in modal commands. - Added fluent builder extension methods and static factory methods (`LocalComponent.RadioGroup`, etc.). - Ensured correct use of `Optional>` for option lists and removed unsupported `Emoji` properties from options. Co-authored-by: Quahu <24848652+Quahu@users.noreply.github.com> --- .../LocalCheckboxGroupOptionExtensions.cs | 30 +++++++++++++++++++ .../LocalRadioGroupOptionExtensions.cs | 30 +++++++++++++++++++ .../Components/LocalComponent.Construction.cs | 21 +++++++++++++ .../Components/V2/LocalCheckboxGroupOption.cs | 18 ++++++----- .../Components/V2/LocalRadioGroupOption.cs | 18 ++++++----- .../V2/CheckboxGroupOptionJsonModel.cs | 3 -- .../V2/RadioGroupOptionJsonModel.cs | 3 -- .../Converters/ModalComponentConverter.cs | 3 ++ 8 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupOptionExtensions.cs create mode 100644 src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupOptionExtensions.cs diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupOptionExtensions.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupOptionExtensions.cs new file mode 100644 index 00000000..0b7acae2 --- /dev/null +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupOptionExtensions.cs @@ -0,0 +1,30 @@ +using Qommon; + +namespace Disqord; + +public static class LocalCheckboxGroupOptionExtensions +{ + public static LocalCheckboxGroupOption WithLabel(this LocalCheckboxGroupOption option, string label) + { + option.Label = label; + return option; + } + + public static LocalCheckboxGroupOption WithValue(this LocalCheckboxGroupOption option, string value) + { + option.Value = value; + return option; + } + + public static LocalCheckboxGroupOption WithDescription(this LocalCheckboxGroupOption option, string description) + { + option.Description = description; + return option; + } + + public static LocalCheckboxGroupOption WithIsDefault(this LocalCheckboxGroupOption option, bool isDefault = true) + { + option.IsDefault = isDefault; + return option; + } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupOptionExtensions.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupOptionExtensions.cs new file mode 100644 index 00000000..c4a5e988 --- /dev/null +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupOptionExtensions.cs @@ -0,0 +1,30 @@ +using Qommon; + +namespace Disqord; + +public static class LocalRadioGroupOptionExtensions +{ + public static LocalRadioGroupOption WithLabel(this LocalRadioGroupOption option, string label) + { + option.Label = label; + return option; + } + + public static LocalRadioGroupOption WithValue(this LocalRadioGroupOption option, string value) + { + option.Value = value; + return option; + } + + public static LocalRadioGroupOption WithDescription(this LocalRadioGroupOption option, string description) + { + option.Description = description; + return option; + } + + public static LocalRadioGroupOption WithIsDefault(this LocalRadioGroupOption option, bool isDefault = true) + { + option.IsDefault = isDefault; + return option; + } +} diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs index 583defad..c1f3ad4f 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs @@ -160,4 +160,25 @@ public static LocalFileUploadComponent FileUpload(string customId) return new LocalFileUploadComponent() .WithCustomId(customId); } + + public static LocalRadioGroupComponent RadioGroup(string customId, params IEnumerable options) + { + return new LocalRadioGroupComponent() + .WithCustomId(customId) + .WithOptions(options); + } + + public static LocalCheckboxGroupComponent CheckboxGroup(string customId, params IEnumerable options) + { + return new LocalCheckboxGroupComponent() + .WithCustomId(customId) + .WithOptions(options); + } + + public static LocalCheckboxComponent Checkbox(string customId, string label) + { + return new LocalCheckboxComponent() + .WithCustomId(customId) + .WithLabel(label); + } } diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupOption.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupOption.cs index b3a0eed6..08953ec4 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupOption.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalCheckboxGroupOption.cs @@ -5,14 +5,12 @@ namespace Disqord; public class LocalCheckboxGroupOption : ILocalConstruct, IJsonConvertible { - public string Label { get; set; } + public Optional Label { get; set; } - public string Value { get; set; } + public Optional Value { get; set; } public Optional Description { get; set; } - public Optional Emoji { get; set; } - public Optional IsDefault { get; set; } public LocalCheckboxGroupOption(string label, string value) @@ -21,12 +19,14 @@ public LocalCheckboxGroupOption(string label, string value) Value = value; } + public LocalCheckboxGroupOption() + { } + protected LocalCheckboxGroupOption(LocalCheckboxGroupOption other) { Label = other.Label; Value = other.Value; Description = other.Description; - Emoji = other.Emoji; IsDefault = other.IsDefault; } @@ -37,12 +37,14 @@ public LocalCheckboxGroupOption Clone() public CheckboxGroupOptionJsonModel ToModel() { + OptionalGuard.HasValue(Label); + OptionalGuard.HasValue(Value); + return new CheckboxGroupOptionJsonModel { - Label = Label, - Value = Value, + Label = Label.Value, + Value = Value.Value, Description = Description, - Emoji = Optional.Convert(Emoji, static emoji => emoji.ToModel()), Default = IsDefault }; } diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupOption.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupOption.cs index 99bdf8fd..31d5eccd 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupOption.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/V2/LocalRadioGroupOption.cs @@ -5,14 +5,12 @@ namespace Disqord; public class LocalRadioGroupOption : ILocalConstruct, IJsonConvertible { - public string Label { get; set; } + public Optional Label { get; set; } - public string Value { get; set; } + public Optional Value { get; set; } public Optional Description { get; set; } - public Optional Emoji { get; set; } - public Optional IsDefault { get; set; } public LocalRadioGroupOption(string label, string value) @@ -21,12 +19,14 @@ public LocalRadioGroupOption(string label, string value) Value = value; } + public LocalRadioGroupOption() + { } + protected LocalRadioGroupOption(LocalRadioGroupOption other) { Label = other.Label; Value = other.Value; Description = other.Description; - Emoji = other.Emoji; IsDefault = other.IsDefault; } @@ -37,12 +37,14 @@ public LocalRadioGroupOption Clone() public RadioGroupOptionJsonModel ToModel() { + OptionalGuard.HasValue(Label); + OptionalGuard.HasValue(Value); + return new RadioGroupOptionJsonModel { - Label = Label, - Value = Value, + Label = Label.Value, + Value = Value.Value, Description = Description, - Emoji = Optional.Convert(Emoji, static emoji => emoji.ToModel()), Default = IsDefault }; } diff --git a/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupOptionJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupOptionJsonModel.cs index bfd4f3fd..e4eb1c56 100644 --- a/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupOptionJsonModel.cs +++ b/src/Disqord.Core/Models/Interactions/Components/V2/CheckboxGroupOptionJsonModel.cs @@ -14,9 +14,6 @@ public class CheckboxGroupOptionJsonModel : JsonModel [JsonProperty("description")] public Optional Description; - [JsonProperty("emoji")] - public Optional Emoji; - [JsonProperty("default")] public Optional Default; } diff --git a/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupOptionJsonModel.cs b/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupOptionJsonModel.cs index a18bf726..9e8e9229 100644 --- a/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupOptionJsonModel.cs +++ b/src/Disqord.Core/Models/Interactions/Components/V2/RadioGroupOptionJsonModel.cs @@ -14,9 +14,6 @@ public class RadioGroupOptionJsonModel : JsonModel [JsonProperty("description")] public Optional Description; - [JsonProperty("emoji")] - public Optional Emoji; - [JsonProperty("default")] public Optional Default; } diff --git a/src/Disqord.Core/Serialization/Json/Default/Entities/Converters/ModalComponentConverter.cs b/src/Disqord.Core/Serialization/Json/Default/Entities/Converters/ModalComponentConverter.cs index 9cde6cbc..2f52d5ef 100644 --- a/src/Disqord.Core/Serialization/Json/Default/Entities/Converters/ModalComponentConverter.cs +++ b/src/Disqord.Core/Serialization/Json/Default/Entities/Converters/ModalComponentConverter.cs @@ -39,6 +39,9 @@ private static ModalBaseComponentJsonModel CreateComponentJsonModel(ComponentTyp ComponentType.TextDisplay => new ModalTextDisplayComponentJsonModel(), ComponentType.Label => new ModalLabelComponentJsonModel(), ComponentType.FileUpload => new ModalFileUploadComponentJsonModel(), + ComponentType.RadioGroup => new ModalRadioGroupComponentJsonModel(), + ComponentType.CheckboxGroup => new ModalCheckboxGroupComponentJsonModel(), + ComponentType.Checkbox => new ModalCheckboxComponentJsonModel(), _ => new ModalBaseComponentJsonModel() }; } From cbf06af3b460016ca79ea52e4e2f8738df44426d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:15:34 +0000 Subject: [PATCH 5/6] Implement new modal components: RadioGroup, CheckboxGroup, Checkbox This commit fully implements support for the new Discord modal components: Radio Group, Checkbox Group, and Checkbox. Key changes: - Added `ComponentType` enum values for `RadioGroup`, `CheckboxGroup`, and `Checkbox`. - Created JSON models for local and modal (transient) representations of these components. - Implemented `LocalComponent` classes (`LocalRadioGroupComponent`, `LocalCheckboxGroupComponent`, `LocalCheckboxComponent`) and their option classes. - Implemented `IModalComponent` interfaces and `TransientModalComponent` classes for receiving modal submissions. - Updated `LocalComponent.ToModel()` to serialize the new components properly, using `OptionalGuard` for validation. - Updated `ModalComponentConverter` to handle polymorphic deserialization of the new component types. - Updated `BindValues` command execution step to support parameter binding for these new component types in modal commands. - Added fluent builder extension methods and static factory methods (`LocalComponent.RadioGroup`, etc.). - Ensured correct use of `Optional>` for option lists and removed unsupported `Emoji` properties from options. - Added `Label` support for Checkbox (optional). Co-authored-by: Quahu <24848652+Quahu@users.noreply.github.com> --- .../LocalCheckboxGroupComponentExtensions.cs | 43 +++++++++++++------ .../LocalRadioGroupComponentExtensions.cs | 43 +++++++++++++------ .../Components/LocalComponent.Construction.cs | 12 ++++-- 3 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs index 8b51b3e3..d29b18b6 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalCheckboxGroupComponentExtensions.cs @@ -5,26 +5,43 @@ namespace Disqord; public static class LocalCheckboxGroupComponentExtensions { - public static TCheckboxGroupComponent WithOptions(this TCheckboxGroupComponent checkboxGroupComponent, IEnumerable options) + public static TCheckboxGroupComponent AddOption(this TCheckboxGroupComponent component, string label, string value, string? description = null, bool isDefault = false) where TCheckboxGroupComponent : LocalCheckboxGroupComponent { - if (checkboxGroupComponent.Options.HasValue) + var option = new LocalCheckboxGroupOption(label, value) { - checkboxGroupComponent.Options.Value.Clear(); - foreach (var option in options) - checkboxGroupComponent.Options.Value.Add(option); - } - else - { - checkboxGroupComponent.Options = new List(options); - } + IsDefault = isDefault + }; - return checkboxGroupComponent; + if (description != null) + option.Description = description; + + return component.AddOption(option); + } + + public static TCheckboxGroupComponent AddOption(this TCheckboxGroupComponent component, LocalCheckboxGroupOption option) + where TCheckboxGroupComponent : LocalCheckboxGroupComponent + { + if (component.Options.Add(option, out var list)) + component.Options = new(list); + + return component; + } + + public static TCheckboxGroupComponent WithOptions(this TCheckboxGroupComponent component, IEnumerable options) + where TCheckboxGroupComponent : LocalCheckboxGroupComponent + { + Guard.IsNotNull(options); + + if (component.Options.With(options, out var list)) + component.Options = new(list); + + return component; } - public static TCheckboxGroupComponent WithOptions(this TCheckboxGroupComponent checkboxGroupComponent, params LocalCheckboxGroupOption[] options) + public static TCheckboxGroupComponent WithOptions(this TCheckboxGroupComponent component, params LocalCheckboxGroupOption[] options) where TCheckboxGroupComponent : LocalCheckboxGroupComponent - => checkboxGroupComponent.WithOptions((IEnumerable) options); + => component.WithOptions((IEnumerable) options); public static TCheckboxGroupComponent WithMinimumSelectedOptions(this TCheckboxGroupComponent checkboxGroupComponent, int minimumSelectedOptions) where TCheckboxGroupComponent : LocalCheckboxGroupComponent diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs index c254aacb..ebd0cb4d 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/Extensions/LocalRadioGroupComponentExtensions.cs @@ -5,26 +5,43 @@ namespace Disqord; public static class LocalRadioGroupComponentExtensions { - public static TRadioGroupComponent WithOptions(this TRadioGroupComponent radioGroupComponent, IEnumerable options) + public static TRadioGroupComponent AddOption(this TRadioGroupComponent component, string label, string value, string? description = null, bool isDefault = false) where TRadioGroupComponent : LocalRadioGroupComponent { - if (radioGroupComponent.Options.HasValue) + var option = new LocalRadioGroupOption(label, value) { - radioGroupComponent.Options.Value.Clear(); - foreach (var option in options) - radioGroupComponent.Options.Value.Add(option); - } - else - { - radioGroupComponent.Options = new List(options); - } + IsDefault = isDefault + }; - return radioGroupComponent; + if (description != null) + option.Description = description; + + return component.AddOption(option); + } + + public static TRadioGroupComponent AddOption(this TRadioGroupComponent component, LocalRadioGroupOption option) + where TRadioGroupComponent : LocalRadioGroupComponent + { + if (component.Options.Add(option, out var list)) + component.Options = new(list); + + return component; + } + + public static TRadioGroupComponent WithOptions(this TRadioGroupComponent component, IEnumerable options) + where TRadioGroupComponent : LocalRadioGroupComponent + { + Guard.IsNotNull(options); + + if (component.Options.With(options, out var list)) + component.Options = new(list); + + return component; } - public static TRadioGroupComponent WithOptions(this TRadioGroupComponent radioGroupComponent, params LocalRadioGroupOption[] options) + public static TRadioGroupComponent WithOptions(this TRadioGroupComponent component, params LocalRadioGroupOption[] options) where TRadioGroupComponent : LocalRadioGroupComponent - => radioGroupComponent.WithOptions((IEnumerable) options); + => component.WithOptions((IEnumerable) options); public static TRadioGroupComponent WithIsRequired(this TRadioGroupComponent radioGroupComponent, bool isRequired = true) where TRadioGroupComponent : LocalRadioGroupComponent diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs index c1f3ad4f..73316c89 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs @@ -175,10 +175,14 @@ public static LocalCheckboxGroupComponent CheckboxGroup(string customId, params .WithOptions(options); } - public static LocalCheckboxComponent Checkbox(string customId, string label) + public static LocalCheckboxComponent Checkbox(string customId, string? label = null) { - return new LocalCheckboxComponent() - .WithCustomId(customId) - .WithLabel(label); + var component = new LocalCheckboxComponent() + .WithCustomId(customId); + + if (label != null) + component.WithLabel(label); + + return component; } } From 5295c8c8ff4a15acbd9c107fb927ffc7267a95bc Mon Sep 17 00:00:00 2001 From: Quahu Date: Mon, 16 Feb 2026 20:46:47 +0100 Subject: [PATCH 6/6] Remove label from Checkbox factory method --- .../Components/LocalComponent.Construction.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs b/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs index 73316c89..6842912b 100644 --- a/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs +++ b/src/Disqord.Core/Entities/Local/Interaction/Components/LocalComponent.Construction.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Disqord; @@ -175,14 +175,9 @@ public static LocalCheckboxGroupComponent CheckboxGroup(string customId, params .WithOptions(options); } - public static LocalCheckboxComponent Checkbox(string customId, string? label = null) + public static LocalCheckboxComponent Checkbox(string customId) { - var component = new LocalCheckboxComponent() + return new LocalCheckboxComponent() .WithCustomId(customId); - - if (label != null) - component.WithLabel(label); - - return component; } }