Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(MessageSelectMenu): droppybois #5692

Merged
merged 27 commits into from Jun 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
97701e7
chore: initial structure for SelectMenu, with typings
monbrey May 28, 2021
984839a
feat: set methods for MessageSelectMenu
monbrey May 28, 2021
df2cb8a
feat: toJSON method
monbrey May 28, 2021
c2d59f2
fix: missing docs
monbrey May 28, 2021
88e5dd4
feat: support creating select menus
monbrey May 28, 2021
840f37f
chore: transform min/max correctly
monbrey May 28, 2021
b8d0e42
types: done
monbrey May 28, 2021
0bb5e1a
fix: emoji typings
monbrey May 28, 2021
b5f822f
feat: stricter types for components
monbrey May 30, 2021
320cb86
fix: splice method
monbrey May 31, 2021
f6bd2c3
docs: update after rebase
monbrey Jun 1, 2021
47e4b3f
docs: updates to MessageSelectMenu
monbrey Jun 4, 2021
3088880
types: remove MessageSelectMenu#addOption
monbrey Jun 4, 2021
4106506
docs: updates and consistency
monbrey Jun 4, 2021
b751e82
docs: realised docgen does not need links here
monbrey Jun 4, 2021
993f708
feat: verify strings
monbrey Jun 5, 2021
c3cab42
feat: improved emoji resolution for components
monbrey Jun 5, 2021
3c35454
fix: max_values binding order
monbrey Jun 5, 2021
a9b46a7
feat: support for disabling
monbrey Jun 5, 2021
221f5a3
docs: minor update
monbrey Jun 6, 2021
c3c0594
chore: final fixes
monbrey Jun 6, 2021
315e86d
refactor: component interaction classes
monbrey Jun 7, 2021
6686469
chore: minor rebase cleanup
monbrey Jun 9, 2021
d182d2e
fix; update typings/index.d.ts
monbrey Jun 10, 2021
1c6ceb4
types: extendable
monbrey Jun 10, 2021
8d242f2
docs: fix incorrect return type
monbrey Jun 12, 2021
33f284e
refactor(Interaction): use enum
monbrey Jun 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
kyranet marked this conversation as resolved.
Show resolved Hide resolved
* @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