Skip to content

Commit

Permalink
feat(MessageSelectMenu): droppybois (#5692)
Browse files Browse the repository at this point in the history
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
  • Loading branch information
monbrey and kyranet committed Jun 24, 2021
1 parent d984ac9 commit e5fcf0b
Show file tree
Hide file tree
Showing 11 changed files with 336 additions and 14 deletions.
3 changes: 3 additions & 0 deletions src/client/actions/InteractionCreate.js
Expand Up @@ -21,6 +21,9 @@ class InteractionCreateAction extends Action {
case MessageComponentTypes.BUTTON:
InteractionType = Structures.get('ButtonInteraction');
break;
case MessageComponentTypes.SELECT_MENU:
InteractionType = Structures.get('SelectMenuInteraction');
break;
default:
client.emit(
Events.DEBUG,
Expand Down
6 changes: 6 additions & 0 deletions src/errors/Messages.js
Expand Up @@ -50,6 +50,12 @@ const Messages = {
BUTTON_URL: 'MessageButton url must be a string',
BUTTON_CUSTOM_ID: 'MessageButton customID must be a string',

SELECT_MENU_CUSTOM_ID: 'MessageSelectMenu customID must be a string',
SELECT_MENU_PLACEHOLDER: 'MessageSelectMenu placeholder must be a string',
SELECT_OPTION_LABEL: 'MessageSelectOption label must be a string',
SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string',
SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string',

INTERACTION_COLLECTOR_ERROR: reason => `Collector received no interactions before ending with reason: ${reason}`,

FILE_NOT_FOUND: file => `File could not be found: ${file}`,
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Expand Up @@ -97,6 +97,7 @@ module.exports = {
MessageEmbed: require('./structures/MessageEmbed'),
MessageMentions: require('./structures/MessageMentions'),
MessageReaction: require('./structures/MessageReaction'),
MessageSelectMenu: require('./structures/MessageSelectMenu'),
NewsChannel: require('./structures/NewsChannel'),
OAuth2Guild: require('./structures/OAuth2Guild'),
PermissionOverwrites: require('./structures/PermissionOverwrites'),
Expand All @@ -106,6 +107,7 @@ module.exports = {
ReactionEmoji: require('./structures/ReactionEmoji'),
RichPresenceAssets: require('./structures/Presence').RichPresenceAssets,
Role: require('./structures/Role'),
SelectMenuInteraction: require('./structures/SelectMenuInteraction'),
Sticker: require('./structures/Sticker'),
StoreChannel: require('./structures/StoreChannel'),
StageChannel: require('./structures/StageChannel'),
Expand Down
11 changes: 9 additions & 2 deletions src/structures/BaseMessageComponent.js
Expand Up @@ -18,14 +18,16 @@ class BaseMessageComponent {
* Data that can be resolved into options for a MessageComponent. This can be:
* * MessageActionRowOptions
* * MessageButtonOptions
* @typedef {MessageActionRowOptions|MessageButtonOptions} MessageComponentOptions
* * MessageSelectMenuOptions
* @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions
*/

/**
* Components that can be sent in a message. This can be:
* * MessageActionRow
* * MessageButton
* @typedef {MessageActionRow|MessageButton} MessageComponent
* * MessageSelectMenu
* @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent
*/

/**
Expand Down Expand Up @@ -72,6 +74,11 @@ class BaseMessageComponent {
component = new MessageButton(data);
break;
}
case MessageComponentTypes.SELECT_MENU: {
const MessageSelectMenu = require('./MessageSelectMenu');
component = new MessageSelectMenu(data);
break;
}
default:
if (client) {
client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
Expand Down
20 changes: 17 additions & 3 deletions src/structures/Interaction.js
@@ -1,7 +1,7 @@
'use strict';

const Base = require('./Base');
const { InteractionTypes } = require('../util/Constants');
const { InteractionTypes, MessageComponentTypes } = require('../util/Constants');
const SnowflakeUtil = require('../util/SnowflakeUtil');

/**
Expand Down Expand Up @@ -114,7 +114,7 @@ class Interaction extends Base {
}

/**
* Indicates whether this interaction is a component interaction.
* Indicates whether this interaction is a message component interaction.
* @returns {boolean}
*/
isMessageComponent() {
Expand All @@ -126,7 +126,21 @@ class Interaction extends Base {
* @returns {boolean}
*/
isButton() {
return InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT && this.componentType === 'BUTTON';
return (
InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT &&
MessageComponentTypes[this.componentType] === MessageComponentTypes.BUTTON
);
}

/**
* Indicates whether this interaction is a select menu interaction.
* @returns {boolean}
*/
isSelectMenu() {
return (
InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT &&
MessageComponentTypes[this.componentType] === MessageComponentTypes.SELECT_MENU
);
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/structures/MessageActionRow.js
Expand Up @@ -11,17 +11,19 @@ class MessageActionRow extends BaseMessageComponent {
/**
* Components that can be placed in an action row
* * MessageButton
* @typedef {MessageButton} MessageActionRowComponent
* * MessageSelectMenu
* @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent
*/

/**
* Options for components that can be placed in an action row
* * MessageButtonOptions
* @typedef {MessageButtonOptions} MessageActionRowComponentOptions
* * MessageSelectMenuOptions
* @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions
*/

/**
* Data that can be resolved into a components that can be placed in an action row
* Data that can be resolved into components that can be placed in an action row
* * MessageActionRowComponent
* * MessageActionRowComponentOptions
* @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable
Expand Down Expand Up @@ -61,7 +63,7 @@ class MessageActionRow extends BaseMessageComponent {
* @param {number} index The index to start at
* @param {number} deleteCount The number of components to remove
* @param {...MessageActionRowComponentResolvable[]} [components] The replacing components
* @returns {MessageSelectMenu}
* @returns {MessageActionRow}
*/
spliceComponents(index, deleteCount, ...components) {
this.components.splice(
Expand Down
202 changes: 202 additions & 0 deletions src/structures/MessageSelectMenu.js
@@ -0,0 +1,202 @@
'use strict';

const BaseMessageComponent = require('./BaseMessageComponent');
const { MessageComponentTypes } = require('../util/Constants');
const Util = require('../util/Util');

/**
* Represents a select menu message component
* @extends {BaseMessageComponent}
*/
class MessageSelectMenu extends BaseMessageComponent {
/**
* @typedef {BaseMessageComponentOptions} MessageSelectMenuOptions
* @property {string} [customID] A unique string to be sent in the interaction when clicked
* @property {string} [placeholder] Custom placeholder text to display when nothing is selected
* @property {number} [minValues] The minimum number of selections required
* @property {number} [maxValues] The maximum number of selections allowed
* @property {MessageSelectOption[]} [options] Options for the select menu
* @property {boolean} [disabled=false] Disables the select menu to prevent interactions
*/

/**
* @typedef {Object} MessageSelectOption
* @property {string} label The text to be displayed on this option
* @property {string} value The value to be sent for this option
* @property {?string} description Optional description to show for this option
* @property {?RawEmoji} emoji Emoji to display for this option
* @property {boolean} default Render this option as the default selection
*/

/**
* @typedef {Object} MessageSelectOptionData
* @property {string} label The text to be displayed on this option
* @property {string} value The value to be sent for this option
* @property {string} [description] Optional description to show for this option
* @property {EmojiIdentifierResolvable} [emoji] Emoji to display for this option
* @property {boolean} [default] Render this option as the default selection
*/

/**
* @param {MessageSelectMenu|MessageSelectMenuOptions} [data={}] MessageSelectMenu to clone or raw data
*/
constructor(data = {}) {
super({ type: 'SELECT_MENU' });

this.setup(data);
}

setup(data) {
/**
* A unique string to be sent in the interaction when clicked
* @type {?string}
*/
this.customID = data.custom_id ?? data.customID ?? null;

/**
* Custom placeholder text to display when nothing is selected
* @type {?string}
*/
this.placeholder = data.placeholder ?? null;

/**
* The minimum number of selections required
* @type {?number}
*/
this.minValues = data.min_values ?? data.minValues ?? null;

/**
* The maximum number of selections allowed
* @type {?number}
*/
this.maxValues = data.max_values ?? data.maxValues ?? null;

/**
* Options for the select menu
* @type {MessageSelectOption[]}
*/
this.options = this.constructor.normalizeOptions(data.options ?? []);

/**
* Whether this select menu is currently disabled
* @type {?boolean}
*/
this.disabled = data.disabled ?? false;
}

/**
* Sets the custom ID of this select menu
* @param {string} customID A unique string to be sent in the interaction when clicked
* @returns {MessageSelectMenu}
*/
setCustomID(customID) {
this.customID = Util.verifyString(customID, RangeError, 'SELECT_MENU_CUSTOM_ID');
return this;
}

/**
* Sets the interactive status of the select menu
* @param {boolean} disabled Whether this select menu should be disabled
* @returns {MessageSelectMenu}
*/
setDisabled(disabled) {
this.disabled = disabled;
return this;
}

/**
* Sets the maximum number of selections allowed for this select menu
* @param {number} maxValues Number of selections to be allowed
* @returns {MessageSelectMenu}
*/
setMaxValues(maxValues) {
this.maxValues = maxValues;
return this;
}

/**
* Sets the minimum number of selections required for this select menu
* <info>This will default the maxValues to the number of options, unless manually set</info>
* @param {number} minValues Number of selections to be required
* @returns {MessageSelectMenu}
*/
setMinValues(minValues) {
this.minValues = minValues;
return this;
}

/**
* Sets the placeholder of this select menu
* @param {string} placeholder Custom placeholder text to display when nothing is selected
* @returns {MessageSelectMenu}
*/
setPlaceholder(placeholder) {
this.placeholder = Util.verifyString(placeholder, RangeError, 'SELECT_MENU_PLACEHOLDER');
return this;
}

/**
* Adds options to the select menu.
* @param {...(MessageSelectOption[]|MessageSelectOption[])} options The options to add
* @returns {MessageSelectMenu}
*/
addOptions(...options) {
this.options.push(...this.constructor.normalizeOptions(options));
return this;
}

/**
* Removes, replaces, and inserts options in the select menu.
* @param {number} index The index to start at
* @param {number} deleteCount The number of options to remove
* @param {...MessageSelectOption|MessageSelectOption[]} [options] The replacing option objects
* @returns {MessageSelectMenu}
*/
spliceOptions(index, deleteCount, ...options) {
this.options.splice(index, deleteCount, ...this.constructor.normalizeOptions(...options));
return this;
}

/**
* Transforms this select menu to a plain object
* @returns {Object} The raw data of this select menu
*/
toJSON() {
return {
custom_id: this.customID,
disabled: this.disabled,
placeholder: this.placeholder,
min_values: this.minValues,
max_values: this.maxValues ?? (this.minValues ? this.options.length : undefined),
options: this.options,
type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type,
};
}

/**
* Normalizes option input and resolves strings and emojis.
* @param {MessageSelectOptionData} option The select menu option to normalize
* @returns {MessageSelectOption}
*/
static normalizeOption(option) {
let { label, value, description, emoji } = option;

label = Util.verifyString(label, RangeError, 'SELECT_OPTION_LABEL');
value = Util.verifyString(value, RangeError, 'SELECT_OPTION_VALUE');
emoji = emoji ? Util.resolvePartialEmoji(emoji) : null;
description = description ? Util.verifyString(description, RangeError, 'SELECT_OPTION_DESCRIPTION', true) : null;

return { label, value, description, emoji, default: option.default ?? false };
}

/**
* Normalizes option input and resolves strings and emojis.
* @param {...MessageSelectOptionData|MessageSelectOption[]} options The select menu options to normalize
* @returns {MessageSelectOption[]}
*/
static normalizeOptions(...options) {
return options.flat(Infinity).map(option => this.normalizeOption(option));
}
}

module.exports = MessageSelectMenu;
21 changes: 21 additions & 0 deletions src/structures/SelectMenuInteraction.js
@@ -0,0 +1,21 @@
'use strict';

const MessageComponentInteraction = require('./MessageComponentInteraction');

/**
* Represents a select menu interaction.
* @extends {MessageComponentInteraction}
*/
class SelectMenuInteraction extends MessageComponentInteraction {
constructor(client, data) {
super(client, data);

/**
* The values selected, if the component which was interacted with was a select menu
* @type {string[]}
*/
this.values = this.componentType === 'SELECT_MENU' ? data.data.values : null;
}
}

module.exports = SelectMenuInteraction;
3 changes: 2 additions & 1 deletion src/util/Constants.js
Expand Up @@ -944,9 +944,10 @@ exports.InteractionResponseTypes = createEnum([
* The type of a message component
* * ACTION_ROW
* * BUTTON
* * SELECT_MENU
* @typedef {string} MessageComponentType
*/
exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON']);
exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON', 'SELECT_MENU']);

/**
* The style of a message button
Expand Down
2 changes: 2 additions & 0 deletions src/util/Structures.js
Expand Up @@ -24,6 +24,7 @@
* * **`CommandInteraction`**
* * **`ButtonInteraction`**
* * **`StageInstance`**
* * **`SelectMenuInteraction`**
* @typedef {string} ExtendableStructure
*/

Expand Down Expand Up @@ -118,6 +119,7 @@ const structures = {
User: require('../structures/User'),
CommandInteraction: require('../structures/CommandInteraction'),
ButtonInteraction: require('../structures/ButtonInteraction'),
SelectMenuInteraction: require('../structures/SelectMenuInteraction'),
StageInstance: require('../structures/StageInstance'),
};

Expand Down

0 comments on commit e5fcf0b

Please sign in to comment.