Skip to content

Commit

Permalink
feat(modals): modals, input text components and modal submits, v13 st…
Browse files Browse the repository at this point in the history
…yle (#7431)
  • Loading branch information
monbrey committed Apr 9, 2022
1 parent 5e8162a commit e1cdcfa
Show file tree
Hide file tree
Showing 20 changed files with 836 additions and 56 deletions.
31 changes: 21 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -55,7 +55,7 @@
"@sapphire/async-queue": "^1.1.9",
"@types/node-fetch": "^2.5.12",
"@types/ws": "^8.2.2",
"discord-api-types": "^0.26.0",
"discord-api-types": "^0.27.1",
"form-data": "^4.0.0",
"node-fetch": "^2.6.1",
"ws": "^8.4.0"
Expand Down
4 changes: 4 additions & 0 deletions src/client/actions/InteractionCreate.js
Expand Up @@ -6,6 +6,7 @@ const AutocompleteInteraction = require('../../structures/AutocompleteInteractio
const ButtonInteraction = require('../../structures/ButtonInteraction');
const CommandInteraction = require('../../structures/CommandInteraction');
const MessageContextMenuInteraction = require('../../structures/MessageContextMenuInteraction');
const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction');
const SelectMenuInteraction = require('../../structures/SelectMenuInteraction');
const UserContextMenuInteraction = require('../../structures/UserContextMenuInteraction');
const { Events, InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../../util/Constants');
Expand Down Expand Up @@ -59,6 +60,9 @@ class InteractionCreateAction extends Action {
case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE:
InteractionType = AutocompleteInteraction;
break;
case InteractionTypes.MODAL_SUBMIT:
InteractionType = ModalSubmitInteraction;
break;
default:
client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
return;
Expand Down
12 changes: 12 additions & 0 deletions src/errors/Messages.js
Expand Up @@ -58,6 +58,14 @@ const Messages = {
SELECT_OPTION_VALUE: 'MessageSelectOption value must be a string',
SELECT_OPTION_DESCRIPTION: 'MessageSelectOption description must be a string',

TEXT_INPUT_CUSTOM_ID: 'TextInputComponent customId must be a string',
TEXT_INPUT_LABEL: 'TextInputComponent label must be a string',
TEXT_INPUT_PLACEHOLDER: 'TextInputComponent placeholder must be a string',
TEXT_INPUT_VALUE: 'TextInputComponent value must be a string',

MODAL_CUSTOM_ID: 'Modal customId must be a string',
MODAL_TITLE: 'Modal title 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 Expand Up @@ -148,6 +156,10 @@ const Messages = {
COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No subcommand group specified for interaction.',
AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: 'No focused option for autocomplete interaction.',

MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND: customId => `Required field with custom id "${customId}" not found.`,
MODAL_SUBMIT_INTERACTION_FIELD_TYPE: (customId, type, expected) =>
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,

INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite',

NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`,
Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Expand Up @@ -122,6 +122,8 @@ exports.MessageMentions = require('./structures/MessageMentions');
exports.MessagePayload = require('./structures/MessagePayload');
exports.MessageReaction = require('./structures/MessageReaction');
exports.MessageSelectMenu = require('./structures/MessageSelectMenu');
exports.Modal = require('./structures/Modal');
exports.ModalSubmitInteraction = require('./structures/ModalSubmitInteraction');
exports.NewsChannel = require('./structures/NewsChannel');
exports.OAuth2Guild = require('./structures/OAuth2Guild');
exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel');
Expand All @@ -140,6 +142,7 @@ exports.StoreChannel = require('./structures/StoreChannel');
exports.Team = require('./structures/Team');
exports.TeamMember = require('./structures/TeamMember');
exports.TextChannel = require('./structures/TextChannel');
exports.TextInputComponent = require('./structures/TextInputComponent');
exports.ThreadChannel = require('./structures/ThreadChannel');
exports.ThreadMember = require('./structures/ThreadMember');
exports.Typing = require('./structures/Typing');
Expand Down
2 changes: 2 additions & 0 deletions src/structures/BaseCommandInteraction.js
Expand Up @@ -196,6 +196,8 @@ class BaseCommandInteraction extends Interaction {
editReply() {}
deleteReply() {}
followUp() {}
showModal() {}
awaitModalSubmit() {}
}

InteractionResponses.applyToClass(BaseCommandInteraction, ['deferUpdate', 'update']);
Expand Down
19 changes: 13 additions & 6 deletions src/structures/BaseMessageComponent.js
Expand Up @@ -4,7 +4,7 @@ const { TypeError } = require('../errors');
const { MessageComponentTypes, Events } = require('../util/Constants');

/**
* Represents an interactive component of a Message. It should not be necessary to construct this directly.
* Represents an interactive component of a Message or Modal. It should not be necessary to construct this directly.
* See {@link MessageComponent}
*/
class BaseMessageComponent {
Expand All @@ -15,18 +15,20 @@ class BaseMessageComponent {
*/

/**
* Data that can be resolved into options for a MessageComponent. This can be:
* Data that can be resolved into options for a component. This can be:
* * MessageActionRowOptions
* * MessageButtonOptions
* * MessageSelectMenuOptions
* * TextInputComponentOptions
* @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions
*/

/**
* Components that can be sent in a message. These can be:
* Components that can be sent in a payload. These can be:
* * MessageActionRow
* * MessageButton
* * MessageSelectMenu
* * TextInputComponent
* @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent
* @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types}
*/
Expand All @@ -51,10 +53,10 @@ class BaseMessageComponent {
}

/**
* Constructs a MessageComponent based on the type of the incoming data
* Constructs a component based on the type of the incoming data
* @param {MessageComponentOptions} data Data for a MessageComponent
* @param {Client|WebhookClient} [client] Client constructing this component
* @returns {?MessageComponent}
* @returns {?(MessageComponent|ModalComponent)}
* @private
*/
static create(data, client) {
Expand All @@ -79,6 +81,11 @@ class BaseMessageComponent {
component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data);
break;
}
case MessageComponentTypes.TEXT_INPUT: {
const TextInputComponent = require('./TextInputComponent');
component = data instanceof TextInputComponent ? data : new TextInputComponent(data);
break;
}
default:
if (client) {
client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
Expand All @@ -90,7 +97,7 @@ class BaseMessageComponent {
}

/**
* Resolves the type of a MessageComponent
* Resolves the type of a component
* @param {MessageComponentTypeResolvable} type The type to resolve
* @returns {MessageComponentType}
* @private
Expand Down
8 changes: 8 additions & 0 deletions src/structures/Interaction.js
Expand Up @@ -173,6 +173,14 @@ class Interaction extends Base {
return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND && typeof this.targetId !== 'undefined';
}

/**
* Indicates whether this interaction is a {@link ModalSubmitInteraction}
* @returns {boolean}
*/
isModalSubmit() {
return InteractionTypes[this.type] === InteractionTypes.MODAL_SUBMIT;
}

/**
* Indicates whether this interaction is a {@link UserContextMenuInteraction}
* @returns {boolean}
Expand Down
6 changes: 4 additions & 2 deletions src/structures/MessageActionRow.js
Expand Up @@ -12,14 +12,16 @@ class MessageActionRow extends BaseMessageComponent {
* Components that can be placed in an action row
* * MessageButton
* * MessageSelectMenu
* @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent
* * TextInputComponent
* @typedef {MessageButton|MessageSelectMenu|TextInputComponent} MessageActionRowComponent
*/

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

/**
Expand Down
2 changes: 2 additions & 0 deletions src/structures/MessageComponentInteraction.js
Expand Up @@ -101,6 +101,8 @@ class MessageComponentInteraction extends Interaction {
followUp() {}
deferUpdate() {}
update() {}
showModal() {}
awaitModalSubmit() {}
}

InteractionResponses.applyToClass(MessageComponentInteraction);
Expand Down
103 changes: 103 additions & 0 deletions src/structures/Modal.js
@@ -0,0 +1,103 @@
'use strict';

const BaseMessageComponent = require('./BaseMessageComponent');
const Util = require('../util/Util');

/**
* Represents a modal (form) to be shown in response to an interaction
*/
class Modal {
/**
* @typedef {Object} ModalOptions
* @property {string} [customId] A unique string to be sent in the interaction when clicked
* @property {string} [title] The title to be displayed on this modal
* @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
* Action rows containing interactive components for the modal (text input components)
*/

/**
* @param {Modal|ModalOptions} data Modal to clone or raw data
* @param {Client} client The client constructing this Modal, if provided
*/
constructor(data = {}, client = null) {
/**
* A list of MessageActionRows in the modal
* @type {MessageActionRow[]}
*/
this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? [];

/**
* A unique string to be sent in the interaction when submitted
* @type {?string}
*/
this.customId = data.custom_id ?? data.customId ?? null;

/**
* The title to be displayed on this modal
* @type {?string}
*/
this.title = data.title ?? null;
}

/**
* Adds components to the modal.
* @param {...MessageActionRowResolvable[]} components The components to add
* @returns {Modal}
*/
addComponents(...components) {
this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
return this;
}

/**
* Sets the components of the modal.
* @param {...MessageActionRowResolvable[]} components The components to set
* @returns {Modal}
*/
setComponents(...components) {
this.spliceComponents(0, this.components.length, components);
return this;
}

/**
* Sets the custom id for this modal
* @param {string} customId A unique string to be sent in the interaction when submitted
* @returns {Modal}
*/
setCustomId(customId) {
this.customId = Util.verifyString(customId, RangeError, 'MODAL_CUSTOM_ID');
return this;
}

/**
* Removes, replaces, and inserts components in the modal.
* @param {number} index The index to start at
* @param {number} deleteCount The number of components to remove
* @param {...MessageActionRowResolvable[]} [components] The replacing components
* @returns {Modal}
*/
spliceComponents(index, deleteCount, ...components) {
this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
return this;
}

/**
* Sets the title of this modal
* @param {string} title The title to be displayed on this modal
* @returns {Modal}
*/
setTitle(title) {
this.title = Util.verifyString(title, RangeError, 'MODAL_TITLE');
return this;
}

toJSON() {
return {
components: this.components.map(c => c.toJSON()),
custom_id: this.customId,
title: this.title,
};
}
}

module.exports = Modal;

0 comments on commit e1cdcfa

Please sign in to comment.