diff --git a/src/builder/create_components.rs b/src/builder/create_components.rs index 636068399f7..6c3e20d539c 100644 --- a/src/builder/create_components.rs +++ b/src/builder/create_components.rs @@ -4,25 +4,14 @@ use serde::Serialize; use crate::model::prelude::*; -#[derive(Clone, Debug)] -struct StaticU8; - -impl Serialize for StaticU8 { - fn serialize(&self, ser: S) -> Result { - ser.serialize_u8(VAL) - } -} - /// A builder for creating a components action row in a message. /// -/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#component-object). +/// [Discord docs](https://discord.com/developers/docs/components/reference#action-row). #[derive(Clone, Debug)] #[must_use] pub enum CreateActionRow<'a> { Buttons(Cow<'a, [CreateButton<'a>]>), SelectMenu(CreateSelectMenu<'a>), - /// Only valid in modals! - InputText(CreateInputText<'a>), } impl<'a> CreateActionRow<'a> { @@ -33,10 +22,6 @@ impl<'a> CreateActionRow<'a> { pub fn select_menu(select_menu: impl Into>) -> Self { Self::SelectMenu(select_menu.into()) } - - pub fn input_text(input_text: impl Into>) -> Self { - Self::InputText(input_text.into()) - } } impl serde::Serialize for CreateActionRow<'_> { @@ -49,7 +34,6 @@ impl serde::Serialize for CreateActionRow<'_> { match self { CreateActionRow::Buttons(buttons) => map.serialize_entry("components", &buttons)?, CreateActionRow::SelectMenu(select) => map.serialize_entry("components", &[select])?, - CreateActionRow::InputText(input) => map.serialize_entry("components", &[input])?, } map.end() @@ -105,6 +89,10 @@ pub enum CreateComponent<'a> { /// /// A container is a flexible component that can hold multiple nested components. Container(CreateContainer<'a>), + /// Represents a label component (V2). + /// + /// A label is used to hold other components in a modal. + Label(CreateLabel<'a>), } /// A builder to create a section component, supports up to a max of **3** components with an @@ -113,7 +101,7 @@ pub enum CreateComponent<'a> { #[must_use] pub struct CreateSection<'a> { #[serde(rename = "type")] - kind: StaticU8<9>, + kind: ComponentType, #[serde(skip_serializing_if = "<[_]>::is_empty")] components: Cow<'a, [CreateSectionComponent<'a>]>, accessory: CreateSectionAccessory<'a>, @@ -128,7 +116,7 @@ impl<'a> CreateSection<'a> { accessory: CreateSectionAccessory<'a>, ) -> Self { CreateSection { - kind: StaticU8::<9>, + kind: ComponentType::Section, components: components.into(), accessory, } @@ -173,7 +161,7 @@ pub enum CreateSectionComponent<'a> { #[derive(Clone, Debug, Serialize)] pub struct CreateTextDisplay<'a> { #[serde(rename = "type")] - kind: StaticU8<10>, + kind: ComponentType, content: Cow<'a, str>, } @@ -183,7 +171,7 @@ impl<'a> CreateTextDisplay<'a> { /// Note: All components on a message shares the same **4000** character limit. pub fn new(content: impl Into>) -> Self { CreateTextDisplay { - kind: StaticU8::<10>, + kind: ComponentType::TextDisplay, content: content.into(), } } @@ -213,7 +201,7 @@ pub enum CreateSectionAccessory<'a> { #[must_use] pub struct CreateThumbnail<'a> { #[serde(rename = "type")] - kind: StaticU8<11>, + kind: ComponentType, media: CreateUnfurledMediaItem<'a>, #[serde(skip_serializing_if = "Option::is_none")] description: Option>, @@ -225,7 +213,7 @@ impl<'a> CreateThumbnail<'a> { /// Creates a new thumbnail with a media item. pub fn new(media: CreateUnfurledMediaItem<'a>) -> Self { CreateThumbnail { - kind: StaticU8::<11>, + kind: ComponentType::Thumbnail, media, description: None, spoiler: None, @@ -280,7 +268,7 @@ impl<'a> CreateUnfurledMediaItem<'a> { #[must_use] pub struct CreateMediaGallery<'a> { #[serde(rename = "type")] - kind: StaticU8<12>, + kind: ComponentType, items: Cow<'a, [CreateMediaGalleryItem<'a>]>, } @@ -288,7 +276,7 @@ impl<'a> CreateMediaGallery<'a> { /// Creates a new media gallery with up to **10** items. pub fn new(items: impl Into]>>) -> Self { CreateMediaGallery { - kind: StaticU8::<12>, + kind: ComponentType::MediaGallery, items: items.into(), } } @@ -373,7 +361,7 @@ impl<'a> CreateMediaGalleryItem<'a> { #[must_use] pub struct CreateFile<'a> { #[serde(rename = "type")] - kind: StaticU8<13>, + kind: ComponentType, file: CreateUnfurledMediaItem<'a>, #[serde(skip_serializing_if = "Option::is_none")] spoiler: Option, @@ -384,7 +372,7 @@ impl<'a> CreateFile<'a> { /// limits. pub fn new(file: impl Into>) -> Self { CreateFile { - kind: StaticU8::<13>, + kind: ComponentType::File, file: file.into(), spoiler: None, } @@ -409,7 +397,7 @@ impl<'a> CreateFile<'a> { #[must_use] pub struct CreateSeparator { #[serde(rename = "type")] - kind: StaticU8<14>, + kind: ComponentType, divider: bool, #[serde(skip_serializing_if = "Option::is_none")] spacing: Option, @@ -419,7 +407,7 @@ impl CreateSeparator { /// Creates a new separator, with or without a divider. pub fn new(divider: bool) -> Self { CreateSeparator { - kind: StaticU8::<14>, + kind: ComponentType::Separator, divider, spacing: None, } @@ -444,7 +432,7 @@ impl CreateSeparator { #[must_use] pub struct CreateContainer<'a> { #[serde(rename = "type")] - kind: StaticU8<17>, + kind: ComponentType, #[serde(skip_serializing_if = "Option::is_none")] accent_color: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -457,7 +445,7 @@ impl<'a> CreateContainer<'a> { /// other component except another container! pub fn new(components: impl Into]>>) -> Self { CreateContainer { - kind: StaticU8::<17>, + kind: ComponentType::Container, accent_color: None, spoiler: None, components: components.into(), @@ -501,6 +489,113 @@ impl<'a> CreateContainer<'a> { } } +/// A builder for creating a label that can hold an [`InputText`] or [`SelectMenu`]. +/// +/// [Discord docs](https://discord.com/developers/docs/components/reference#label). +#[derive(Clone, Debug, Serialize)] +#[must_use] +pub struct CreateLabel<'a> { + #[serde(rename = "type")] + kind: ComponentType, + label: Cow<'a, str>, + description: Option>, + component: CreateLabelComponent<'a>, +} + +impl<'a> CreateLabel<'a> { + /// Create a select menu with a specific label. + pub fn select_menu(label: impl Into>, select_menu: CreateSelectMenu<'a>) -> Self { + Self { + kind: ComponentType::Label, + label: label.into(), + description: None, + component: CreateLabelComponent::SelectMenu(select_menu), + } + } + + /// Create a text input with a specific label. + pub fn input_text(label: impl Into>, input_text: CreateInputText<'a>) -> Self { + Self { + kind: ComponentType::Label, + label: label.into(), + description: None, + component: CreateLabelComponent::InputText(input_text), + } + } + + /// Create a file upload with a specific label. + pub fn file_upload(label: impl Into>, file_upload: CreateFileUpload<'a>) -> Self { + Self { + kind: ComponentType::Label, + label: label.into(), + description: None, + component: CreateLabelComponent::FileUpload(file_upload), + } + } + + /// Sets the description of this component, which will display underneath the label text. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = Some(description.into()); + self + } +} + +/// An enum of all valid label components. +#[derive(Clone, Debug, Serialize)] +#[must_use] +#[serde(untagged)] +enum CreateLabelComponent<'a> { + SelectMenu(CreateSelectMenu<'a>), + InputText(CreateInputText<'a>), + FileUpload(CreateFileUpload<'a>), +} + +/// A builder for creating a file upload in a modal. +/// +/// [Discord docs](https://discord.com/developers/docs/components/reference#file-upload). +#[derive(Clone, Debug, Serialize)] +#[must_use] +pub struct CreateFileUpload<'a> { + #[serde(rename = "type")] + kind: ComponentType, + custom_id: Cow<'a, str>, + min_values: u8, + max_values: u8, + required: bool, +} + +impl<'a> CreateFileUpload<'a> { + /// Creates a builder with the given custom id. + pub fn new(custom_id: impl Into>) -> Self { + Self { + kind: ComponentType::FileUpload, + custom_id: custom_id.into(), + min_values: 1, + max_values: 1, + required: true, + } + } + + /// The minimum number of files that must be uploaded. Must be a number from 0 through 10, and + /// defaults to 1. + pub fn min_values(mut self, min_values: u8) -> Self { + self.min_values = min_values; + self + } + + /// The maximum number of files that can be uploaded. Defaults to 1, but can be at most 10. + pub fn max_values(mut self, max_values: u8) -> Self { + self.max_values = max_values; + self + } + + // Whether the file upload is required. + pub fn required(mut self, required: bool) -> Self { + self.required = required; + self + } +} + enum_number! { #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] @@ -671,7 +766,7 @@ impl Serialize for CreateSelectMenuDefault { } } -/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure). +/// [Discord docs](https://discord.com/developers/docs/components/reference#component-object-component-types). #[derive(Clone, Debug)] pub enum CreateSelectMenuKind<'a> { String { @@ -759,7 +854,7 @@ impl Serialize for CreateSelectMenuKind<'_> { /// A builder for creating a select menu component in a message /// -/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure). +/// [Discord docs](https://discord.com/developers/docs/components/reference#component-object-component-types). #[derive(Clone, Debug, Serialize)] #[must_use] pub struct CreateSelectMenu<'a> { @@ -825,7 +920,7 @@ impl<'a> CreateSelectMenu<'a> { /// A builder for creating an option of a select menu component in a message /// -/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure) +/// [Discord docs](https://discord.com/developers/docs/components/reference#string-select-select-option-structure) #[derive(Clone, Debug, Serialize)] #[must_use] pub struct CreateSelectMenuOption<'a> { @@ -885,7 +980,7 @@ impl<'a> CreateSelectMenuOption<'a> { /// A builder for creating an input text component in a modal /// -/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure). +/// [Discord docs](https://discord.com/developers/docs/components/reference#text-input). #[derive(Clone, Debug, Serialize)] #[must_use] pub struct CreateInputText<'a> { @@ -893,7 +988,6 @@ pub struct CreateInputText<'a> { kind: ComponentType, custom_id: Cow<'a, str>, style: InputTextStyle, - label: Option>, min_length: Option, max_length: Option, required: bool, @@ -906,14 +1000,9 @@ pub struct CreateInputText<'a> { impl<'a> CreateInputText<'a> { /// Creates a text input with the given style, label, and custom id (a developer-defined /// identifier), leaving all other fields empty. - pub fn new( - style: InputTextStyle, - label: impl Into>, - custom_id: impl Into>, - ) -> Self { + pub fn new(style: InputTextStyle, custom_id: impl Into>) -> Self { Self { style, - label: Some(label.into()), custom_id: custom_id.into(), placeholder: None, @@ -932,12 +1021,6 @@ impl<'a> CreateInputText<'a> { self } - /// Sets the label of this input text. Replaces the current value as set in [`Self::new`]. - pub fn label(mut self, label: impl Into>) -> Self { - self.label = Some(label.into()); - self - } - /// Sets the custom id of the input text, a developer-defined identifier. Replaces the current /// value as set in [`Self::new`]. pub fn custom_id(mut self, id: impl Into>) -> Self { diff --git a/src/builder/create_interaction_response.rs b/src/builder/create_interaction_response.rs index 77a46af196e..f1c86a9182e 100644 --- a/src/builder/create_interaction_response.rs +++ b/src/builder/create_interaction_response.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use super::create_poll::Ready; use super::{ - CreateActionRow, CreateAllowedMentions, CreateAttachment, CreateComponent, @@ -424,7 +423,7 @@ impl<'a> CreateAutocompleteResponse<'a> { #[derive(Clone, Debug, Default, Serialize)] #[must_use] pub struct CreateModal<'a> { - components: Cow<'a, [CreateActionRow<'a>]>, + components: Cow<'a, [CreateComponent<'a>]>, custom_id: Cow<'a, str>, title: Cow<'a, str>, } @@ -442,7 +441,7 @@ impl<'a> CreateModal<'a> { /// Sets the components of this message. /// /// Overwrites existing components. - pub fn components(mut self, components: impl Into]>>) -> Self { + pub fn components(mut self, components: impl Into]>>) -> Self { self.components = components.into(); self } diff --git a/src/collector/quick_modal.rs b/src/collector/quick_modal.rs index d4d0c40d74f..395ec183bfb 100644 --- a/src/collector/quick_modal.rs +++ b/src/collector/quick_modal.rs @@ -1,6 +1,13 @@ use std::borrow::Cow; -use crate::builder::{CreateActionRow, CreateInputText, CreateInteractionResponse, CreateModal}; +use crate::builder::{ + CreateComponent, + CreateInputText, + CreateInteractionResponse, + CreateLabel, + CreateModal, + CreateTextDisplay, +}; use crate::collector::ModalInteractionCollector; use crate::gateway::client::Context; use crate::internal::prelude::*; @@ -31,7 +38,7 @@ pub struct QuickModalResponse { pub struct CreateQuickModal<'a> { title: Cow<'a, str>, timeout: Option, - input_texts: Vec>, + components: Vec>, } impl<'a> CreateQuickModal<'a> { @@ -39,7 +46,7 @@ impl<'a> CreateQuickModal<'a> { Self { title: title.into(), timeout: None, - input_texts: Vec::new(), + components: Vec::new(), } } @@ -52,12 +59,21 @@ impl<'a> CreateQuickModal<'a> { self } + /// Adds a text display field. + pub fn text(mut self, content: impl Into>) -> Self { + self.components.push(CreateComponent::TextDisplay(CreateTextDisplay::new(content))); + self + } + /// Adds an input text field. - /// - /// As the `custom_id` field of [`CreateInputText`], just supply an empty string. All custom - /// IDs are overwritten by [`CreateQuickModal`] when sending the modal. - pub fn field(mut self, input_text: CreateInputText<'a>) -> Self { - self.input_texts.push(input_text); + pub fn field( + mut self, + label: impl Into>, + input_text: CreateInputText<'a>, + ) -> Self { + self.components.push(CreateComponent::Label( + CreateLabel::input_text(label, input_text).description("test"), + )); self } @@ -65,14 +81,18 @@ impl<'a> CreateQuickModal<'a> { /// /// Wraps [`Self::field`]. pub fn short_field(self, label: impl Into>) -> Self { - self.field(CreateInputText::new(InputTextStyle::Short, label, "")) + let input_text = + CreateInputText::new(InputTextStyle::Short, self.components.len().to_string()); + self.field(label, input_text) } /// Convenience method to add a multi-line input text field. /// /// Wraps [`Self::field`]. pub fn paragraph_field(self, label: impl Into>) -> Self { - self.field(CreateInputText::new(InputTextStyle::Paragraph, label, "")) + let input_text = + CreateInputText::new(InputTextStyle::Paragraph, self.components.len().to_string()); + self.field(label, input_text) } /// # Errors @@ -84,22 +104,13 @@ impl<'a> CreateQuickModal<'a> { interaction_id: InteractionId, token: &str, ) -> Result, crate::Error> { - let modal_custom_id = interaction_id.to_arraystring(); let builder = CreateInteractionResponse::Modal( - CreateModal::new(modal_custom_id.as_str(), self.title).components( - self.input_texts - .into_iter() - .enumerate() - .map(|(i, input_text)| { - CreateActionRow::InputText(input_text.custom_id(i.to_string())) - }) - .collect::>(), - ), + CreateModal::new(interaction_id.to_string(), self.title).components(self.components), ); builder.execute(&ctx.http, interaction_id, token).await?; let collector = ModalInteractionCollector::new(ctx) - .custom_ids(vec![FixedString::from_str_trunc(&modal_custom_id)]); + .custom_ids(vec![FixedString::from_str_trunc(&interaction_id.to_string())]); let collector = match self.timeout { Some(timeout) => collector.timeout(timeout), @@ -114,23 +125,22 @@ impl<'a> CreateQuickModal<'a> { .data .components .iter() - .filter_map(|row| match row.components.first() { - Some(ActionRowComponent::InputText(text)) => { + .filter_map(|component| { + if let Component::Label(label) = component + && let LabelComponent::InputText(text) = &label.component + { if let Some(value) = &text.value { Some(value.clone()) } else { tracing::warn!("input text value was empty in modal response"); None } - }, - Some(other) => { - tracing::warn!("expected input text in modal response, got {:?}", other); - None - }, - None => { - tracing::warn!("empty action row"); + } else { + if !matches!(component, Component::TextDisplay(_)) { + tracing::warn!("expected input text in modal response, got {component:?}"); + } None - }, + } }) .collect(); diff --git a/src/model/application/command_interaction.rs b/src/model/application/command_interaction.rs index 362144f6084..354095cf592 100644 --- a/src/model/application/command_interaction.rs +++ b/src/model/application/command_interaction.rs @@ -228,7 +228,7 @@ impl Serialize for CommandInteraction { /// The command data payload. /// -/// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-data-structure). +/// [Discord docs](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-application-command-data-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] @@ -240,9 +240,10 @@ pub struct CommandData { /// The application command type of the triggered application command. #[serde(rename = "type")] pub kind: CommandType, - /// The parameters and the given values. The converted objects from the given options. + /// The converted objects from the given options. #[serde(default)] pub resolved: CommandDataResolved, + /// The parameters and the given values. #[serde(default)] pub options: FixedArray, /// The Id of the guild the command is registered to. diff --git a/src/model/application/component.rs b/src/model/application/component.rs index fbb1c9aeabf..8548700e5fb 100644 --- a/src/model/application/component.rs +++ b/src/model/application/component.rs @@ -27,6 +27,8 @@ enum_number! { File = 13, Separator = 14, Container = 17, + Label = 18, + FileUpload = 19, _ => Unknown(u8), } } @@ -53,11 +55,12 @@ pub enum Component { Separator(Separator), File(FileComponent), Container(Container), + Label(Label), Unknown(u8), } impl<'de> Deserialize<'de> for Component { - fn deserialize(deserializer: D) -> std::result::Result + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { @@ -93,6 +96,7 @@ impl<'de> Deserialize<'de> for Component { ComponentType::File => Deserialize::deserialize(value).map(Component::File), ComponentType::Container => Deserialize::deserialize(value).map(Component::Container), ComponentType::Thumbnail => Deserialize::deserialize(value).map(Component::Thumbnail), + ComponentType::Label => Deserialize::deserialize(value).map(Component::Label), ComponentType(i) => Ok(Component::Unknown(i)), } .map_err(DeError::custom) @@ -142,7 +146,7 @@ pub struct Thumbnail { /// A url or attachment. /// -/// [Discord docs](https://discord.com/developers/docs/components/reference#unfurled-media-item-structure) +/// [Discord docs](https://discord.com/developers/docs/components/reference#unfurled-media-item) #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] @@ -272,9 +276,83 @@ pub struct Container { pub components: FixedArray, } +/// A layout component that wraps modal components with a label and optional description. +/// +/// **Note**: Labels can only appear within modals, and will not include the `label` or +/// `description` field when part of a modal response. +/// +/// [Discord docs](https://discord.com/developers/docs/components/reference#label-label-interaction-response-structure) +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[non_exhaustive] +pub struct Label { + /// Always [`ComponentType::Label`] + #[serde(rename = "type")] + pub kind: ComponentType, + /// The component within the label. + pub component: LabelComponent, +} + +#[derive(Clone, Debug, Serialize)] +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[serde(untagged)] +#[non_exhaustive] +pub enum LabelComponent { + SelectMenu(SelectMenu), + InputText(InputText), + FileUpload(FileUpload), +} + +impl<'de> Deserialize<'de> for LabelComponent { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct LabelComponentRaw { + #[serde(rename = "type")] + kind: ComponentType, + } + + let raw_data = <&RawValue>::deserialize(deserializer)?; + let raw = LabelComponentRaw::deserialize(raw_data).map_err(DeError::custom)?; + + match raw.kind { + ComponentType::StringSelect + | ComponentType::UserSelect + | ComponentType::RoleSelect + | ComponentType::MentionableSelect + | ComponentType::ChannelSelect => { + Deserialize::deserialize(raw_data).map(LabelComponent::SelectMenu) + }, + ComponentType::InputText => { + Deserialize::deserialize(raw_data).map(LabelComponent::InputText) + }, + ComponentType::FileUpload => { + Deserialize::deserialize(raw_data).map(LabelComponent::FileUpload) + }, + ComponentType(i) => { + return Err(DeError::custom(format_args!("Unknown component type {i}"))); + }, + } + .map_err(DeError::custom) + } +} + +/// An interactive component that allows users to upload files in modals. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[non_exhaustive] +pub struct FileUpload { + /// Always [`ComponentType::FileUpload`] + #[serde(rename = "type")] + pub kind: ComponentType, + /// Developer-defined identifier for the file upload; max 100 characters + pub custom_id: FixedString, + /// IDs of the uploaded files found in [`ModalInteractionData::resolved`]. + pub values: FixedArray, +} + /// An action row. /// -/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#action-rows). +/// [Discord docs](https://discord.com/developers/docs/components/reference#action-row). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] @@ -289,18 +367,18 @@ pub struct ActionRow { /// A component which can be inside of an [`ActionRow`]. /// -/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#component-object-component-types). +/// [Discord docs](https://discord.com/developers/docs/components/reference#action-row-action-row-child-components). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] #[non_exhaustive] pub enum ActionRowComponent { Button(Button), SelectMenu(SelectMenu), - InputText(InputText), } impl<'de> Deserialize<'de> for ActionRowComponent { - fn deserialize>(deserializer: D) -> std::result::Result { + fn deserialize>(deserializer: D) -> Result { #[derive(Deserialize)] struct ActionRowRaw { #[serde(rename = "type")] @@ -314,9 +392,6 @@ impl<'de> Deserialize<'de> for ActionRowComponent { ComponentType::Button => { Deserialize::deserialize(raw_data).map(ActionRowComponent::Button) }, - ComponentType::InputText => { - Deserialize::deserialize(raw_data).map(ActionRowComponent::InputText) - }, ComponentType::StringSelect | ComponentType::UserSelect | ComponentType::RoleSelect @@ -335,16 +410,6 @@ impl<'de> Deserialize<'de> for ActionRowComponent { } } -impl Serialize for ActionRowComponent { - fn serialize(&self, serializer: S) -> std::result::Result { - match self { - Self::Button(c) => c.serialize(serializer), - Self::InputText(c) => c.serialize(serializer), - Self::SelectMenu(c) => c.serialize(serializer), - } - } -} - impl From