diff --git a/src/Discord/Builders/Components/ActionRow.php b/src/Discord/Builders/Components/ActionRow.php index 3a740f9c1..957486d61 100644 --- a/src/Discord/Builders/Components/ActionRow.php +++ b/src/Discord/Builders/Components/ActionRow.php @@ -24,6 +24,7 @@ */ class ActionRow extends Layout { + /** Usage of ActionRow in Modal is deprecated. Use `Component::Label` as the top-level container. */ public const USAGE = ['Message', 'Modal']; /** diff --git a/src/Discord/Builders/Components/Component.php b/src/Discord/Builders/Components/Component.php index 28718319b..1cd4a2bf0 100644 --- a/src/Discord/Builders/Components/Component.php +++ b/src/Discord/Builders/Components/Component.php @@ -43,6 +43,7 @@ abstract class Component implements JsonSerializable public const TYPE_SEPARATOR = 14; public const TYPE_CONTENT_INVENTORY_ENTRY = 16; // Not documented public const TYPE_CONTAINER = 17; + public const TYPE_LABEL = 18; /** @deprecated 7.4.0 Use `Component::TYPE_STRING_SELECT` */ public const TYPE_SELECT_MENU = 3; diff --git a/src/Discord/Builders/Components/Label.php b/src/Discord/Builders/Components/Label.php new file mode 100644 index 000000000..2ce121ad5 --- /dev/null +++ b/src/Discord/Builders/Components/Label.php @@ -0,0 +1,156 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\Builders\Components; + +use function Discord\poly_strlen; + +/** + * A Label is a top-level component. + * + * @link https://discord.com/developers/docs/components/reference#label + * + * @todo Update to match Discord's documentation upon public release. + * @todo Update Label class to extend the relevant base class. + * @todo Confirm if Label will be usable in Message components. + * + * @since 10.19.0 + * + * @property int $type 18 for label component. + * @property string $label The text for the label. Must be between 1 and 45 characters. + * @property string|null $description Optional description for the label. Max 100 characters. + * @property StringSelect|TextInput $component The component associated with the label. + */ +class Label extends ComponentObject +{ + public const USAGE = ['Modal']; + + /** + * Component type. + * + * @var int + */ + protected $type = Component::TYPE_LABEL; + + /** + * The text for the label. + * + * @var string + */ + protected $label; + + /** + * Optional description for the label. + * + * @var string|null + */ + protected $description; + + /** + * The component associated with the label. + * + * @var StringSelect|TextInput + */ + protected $component; + + /** + * Creates a new label component. + * + * @param string $label The text for the label. + * @param StringSelect|TextInput $component The component associated with the label. + * @param string|null $description Optional description for the label. + * + * @return self + */ + public static function new(string $label, $component, ?string $description = null): self + { + $label_component = new self(); + + $label_component->setLabel($label); + $label_component->setComponent($component); + $label_component->setDescription($description); + + return $label_component; + } + + /** + * Sets the label text. + * + * @param string $label The text for the label. Must be between 1 and 45 characters. + * + * @return self + */ + public function setLabel(string $label): self + { + if (poly_strlen($label) === 0 || poly_strlen($label) > 45) { + throw new \LengthException('Label must be between 1 and 45 in length.'); + } + + $this->label = $label; + + return $this; + } + + /** + * Sets the description text. + * + * @param string|null $description The description for the label. Max 100 characters. + * + * @return self + */ + public function setDescription(?string $description = null): self + { + if (poly_strlen($description) === 0) { + $description = null; + } + + if (poly_strlen($description) > 100) { + throw new \LengthException('Description must be between 0 and 100 in length.'); + } + + $this->description = $description; + + return $this; + } + + /** Sets the component associated with the label. + * + * @param StringSelect|TextInput $component The component associated with the label. + * + * @return self + */ + public function setComponent($component): self + { + $this->component = $component; + + return $this; + } + + /** + * @inheritDoc + */ + public function jsonSerialize(): array + { + $data = [ + 'type' => $this->type, + 'label' => $this->label, + 'component' => $this->component, + ]; + + if (isset($this->description)) { + $data['description'] = $this->description; + } + + return $data; + } +} diff --git a/src/Discord/Builders/Components/SelectMenu.php b/src/Discord/Builders/Components/SelectMenu.php index 667a234d6..6ad19494e 100644 --- a/src/Discord/Builders/Components/SelectMenu.php +++ b/src/Discord/Builders/Components/SelectMenu.php @@ -103,6 +103,13 @@ abstract class SelectMenu extends Interactive */ protected $disabled; + /** + * Whether the select menu is required. Defaults to true. (Modal only). + * + * @var bool|null + */ + protected $required; + /** * Callback used to listen for `INTERACTION_CREATE` events. * @@ -296,7 +303,7 @@ public function setMaxValues(?int $max_values): self } /** - * Sets the select menus disabled state. + * Sets the select menus disabled state. (Message only) * * @param bool $disabled * @@ -505,7 +512,7 @@ public function isDisabled(): ?bool } /** - * {@inheritDoc} + * @inheritDoc */ public function jsonSerialize(): array { @@ -532,24 +539,31 @@ public function jsonSerialize(): array if (isset($this->min_values)) { if (isset($this->options) && $this->min_values > count($this->options)) { - throw new \OutOfBoundsException('There are less options than the minimum number of options to be selected.'); + throw new \DomainException('There are less options than the minimum number of options to be selected.'); } $content['min_values'] = $this->min_values; } - if ($this->max_values) { + if (isset($this->max_values) && $this->max_values) { if (isset($this->options) && $this->max_values > count($this->options)) { - throw new \OutOfBoundsException('There are less options than the maximum number of options to be selected.'); + throw new \DomainException('There are less options than the maximum number of options to be selected.'); } $content['max_values'] = $this->max_values; } - if ($this->disabled) { + if (isset($this->disabled) && $this->disabled) { $content['disabled'] = true; } + if (isset($this->required)) { + $content['required'] = true; + if ($this->min_values === null || $this->min_values === 0) { + throw new \LengthException('Required select menus must have a minimum value greater than 0.'); + } + } + return $content; } } diff --git a/src/Discord/Builders/Components/StringSelect.php b/src/Discord/Builders/Components/StringSelect.php index 33595f21d..ddac1e5d5 100644 --- a/src/Discord/Builders/Components/StringSelect.php +++ b/src/Discord/Builders/Components/StringSelect.php @@ -23,7 +23,7 @@ */ class StringSelect extends SelectMenu { - public const USAGE = ['Message']; + public const USAGE = ['Message', 'Modal']; /** * Component type. @@ -39,6 +39,20 @@ class StringSelect extends SelectMenu */ protected $options = []; + /** + * Sets whether the select menu is required. (Modal only). + * + * @param bool|null $required + * + * @return $this + */ + public function setRequired(?bool $required = false): self + { + $this->required = $required; + + return $this; + } + /** * Adds an option to the select menu. Maximum 25 options. * diff --git a/src/Discord/Builders/Components/TextInput.php b/src/Discord/Builders/Components/TextInput.php index f47c747f0..de8c3373b 100644 --- a/src/Discord/Builders/Components/TextInput.php +++ b/src/Discord/Builders/Components/TextInput.php @@ -46,7 +46,10 @@ class TextInput extends Interactive /** * Label for the text input. * - * @var string + * Deprecated for use with modals. Use a top-level Component::Label. + * + * @var string|null + * */ private $label; @@ -92,7 +95,7 @@ class TextInput extends Interactive * @param int $style The style of the text input. * @param string|null $custom_id The custom ID of the text input. If not given, a UUID will be used */ - public function __construct(string $label, int $style, ?string $custom_id = null) + public function __construct(?string $label = null, int $style, ?string $custom_id = null) { $this->setLabel($label); $this->setStyle($style); @@ -108,7 +111,7 @@ public function __construct(string $label, int $style, ?string $custom_id = null * * @return self */ - public static function new(string $label, int $style, ?string $custom_id = null): self + public static function new(?string $label = null, int $style, ?string $custom_id = null): self { return new self($label, $style, $custom_id); } @@ -156,15 +159,15 @@ public function setStyle(int $style): self /** * Sets the label of the text input. * - * @param string $label Label of the text input. Maximum 45 characters. + * @param string|null $label Label of the text input. Maximum 45 characters. * * @throws \LengthException * * @return $this */ - public function setLabel(string $label): self + public function setLabel(?string $label = null): self { - if (poly_strlen($label) > 45) { + if (isset($label) && poly_strlen($label) > 45) { throw new \LengthException('Label must be maximum 45 characters.'); } @@ -318,7 +321,7 @@ public function isRequired(): ?bool } /** - * {@inheritDoc} + * @inheritDoc */ public function jsonSerialize(): array { @@ -326,9 +329,12 @@ public function jsonSerialize(): array 'type' => $this->type, 'custom_id' => $this->custom_id, 'style' => $this->style, - 'label' => $this->label, ]; + if (isset($this->label)) { + $content['label'] = $this->label; + } + if (isset($this->min_length)) { $content['min_length'] = $this->min_length; } diff --git a/src/Discord/Parts/Channel/Message/ActionRow.php b/src/Discord/Parts/Channel/Message/ActionRow.php index 7fe3b8228..116671ead 100644 --- a/src/Discord/Parts/Channel/Message/ActionRow.php +++ b/src/Discord/Parts/Channel/Message/ActionRow.php @@ -14,10 +14,12 @@ namespace Discord\Parts\Channel\Message; /** - * An Action Row is a top-level layout component used in messages and modals. + * An Action Row is a top-level layout component used in messages. + * + * Using ActionRows in modals is now deprecated - use Component::Label as the top level component! * * Action Rows can contain: - + * * Up to 5 contextually grouped buttons * A single text input * A single select component (string select, user select, role select, mentionable select, or channel select) diff --git a/src/Discord/Parts/Channel/Message/Component.php b/src/Discord/Parts/Channel/Message/Component.php index e55dc89a7..aa5df24eb 100644 --- a/src/Discord/Parts/Channel/Message/Component.php +++ b/src/Discord/Parts/Channel/Message/Component.php @@ -56,10 +56,11 @@ class Component extends Part ComponentBuilder::TYPE_FILE => File::class, ComponentBuilder::TYPE_SEPARATOR => Separator::class, ComponentBuilder::TYPE_CONTAINER => Container::class, + ComponentBuilder::TYPE_LABEL => Label::class, ]; /** - * {@inheritDoc} + * @inheritDoc */ protected $fillable = [ 'type', diff --git a/src/Discord/Parts/Channel/Message/Label.php b/src/Discord/Parts/Channel/Message/Label.php new file mode 100644 index 000000000..afc39eb1e --- /dev/null +++ b/src/Discord/Parts/Channel/Message/Label.php @@ -0,0 +1,48 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE.md file. + */ + +namespace Discord\Parts\Channel\Message; + +/** + * A Label is a top-level component. + * + * @link https://discord.com/developers/docs/components/reference#label + * + * @todo Update to match Discord's documentation upon public release. + * @todo Update Label class to extend the relevant base class. + * + * @since 10.19.0 + * + * @property int $type 18 for label component. + * @property string $label The text for the label. + * @property string|null $description Optional description for the label. + * @property StringSelect|TextInput $component The component associated with the label. + */ +class Label extends Component +{ + /** + * @inheritDoc + */ + protected $fillable = [ + 'id', + 'type', + 'label', + 'description', + 'component', + ]; + + public function getComponentAttribute(): StringSelect|TextInput + { + return $this->createOf(Component::TYPES[$this->attributes['component']->type ?? 0], $this->attributes['component']); + } +} diff --git a/src/Discord/Parts/Interactions/Interaction.php b/src/Discord/Parts/Interactions/Interaction.php index bcab46f9c..f2660198d 100644 --- a/src/Discord/Parts/Interactions/Interaction.php +++ b/src/Discord/Parts/Interactions/Interaction.php @@ -69,7 +69,7 @@ class Interaction extends Part { /** - * {@inheritDoc} + * @inheritDoc */ protected $fillable = [ 'id', @@ -700,9 +700,11 @@ protected function createListener(string $custom_id, callable $submit, int|float $listener = function (Interaction $interaction) use ($custom_id, $submit, &$listener, &$timer) { if ($interaction->type == self::TYPE_MODAL_SUBMIT && $interaction->data->custom_id == $custom_id) { $components = Collection::for(RequestComponent::class, 'custom_id'); - foreach ($interaction->data->components as $actionrow) { - if ($actionrow->type == Component::TYPE_ACTION_ROW) { - foreach ($actionrow->components as $component) { + foreach ($interaction->data->components as $container) { + if ($container->type == Component::TYPE_LABEL) { + $components->pushItem($container->component); + } elseif ($container->type == Component::TYPE_ACTION_ROW) { + foreach ($container->components as $component) { $components->pushItem($component); } }