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(modals): modals, input text components and modal submits, v13 style #7431

Merged
merged 34 commits into from Apr 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3745394
feat(modals): modals, input texts and submit interactions, v13 style
monbrey Feb 9, 2022
d536ec7
fix(types): raw modal data
monbrey Feb 9, 2022
f73dcd8
feat(types): attempt at making the action row types generic
monbrey Feb 9, 2022
20a2c8f
refactor: remove the separate ModalActionRow
monbrey Feb 9, 2022
aff3756
fix(inputtext): fix customId casing
monbrey Feb 9, 2022
72881d8
refactor: rename component
monbrey Feb 9, 2022
6daef2a
refactor: missed some renaming
monbrey Feb 9, 2022
4da9c39
feat(interactions): provide value getter for modals
monbrey Feb 9, 2022
451aba6
fix(types): setRequired param is optional
monbrey Feb 9, 2022
073415d
fix: apply suggestions from code review
monbrey Feb 9, 2022
fb242f6
feat(resolver): fields resolver, missing errors
monbrey Feb 9, 2022
01cb2d2
fix(resolver): some v13ish things
monbrey Feb 9, 2022
8f5fa3f
fix: apply suggestions from code review
monbrey Feb 9, 2022
1b35108
fix(types): action rows in modals
monbrey Feb 10, 2022
ee15294
fix(modal submit): missing props
monbrey Feb 12, 2022
1614ee2
Fox: update src/util/Constants.js
monbrey Feb 13, 2022
5b72ff1
refactor: rename presentModal to showModal
monbrey Feb 13, 2022
b2cb219
docs: some were missing
monbrey Feb 13, 2022
2c18bf0
types(modal): nullable props
monbrey Feb 13, 2022
c9d7db3
docs: improve wording
monbrey Feb 13, 2022
f7bde7d
docs; update wording
monbrey Feb 14, 2022
b88e699
feat(modals): awaitModalSubmit
monbrey Feb 15, 2022
4ef3fe6
refactor: remove duplicate code
monbrey Feb 15, 2022
52a1d3f
feat: bump api-types dep and use new types
monbrey Feb 15, 2022
065af02
types: a little stricter
monbrey Feb 15, 2022
8ba97d9
refactor: align with v14
monbrey Feb 15, 2022
1235bf5
fix: remove unused directive and relocate test
monbrey Feb 16, 2022
0a1d032
refactor: revert renaming
monbrey Feb 16, 2022
5275590
feat: support for update and deferUpdate
monbrey Feb 16, 2022
16990c6
feat: message reference and typeguarding
monbrey Feb 16, 2022
e4db99d
types(modal): add update typings
monbrey Feb 22, 2022
d4ae328
fix(modal): check and set replied flags
monbrey Feb 22, 2022
28b6e69
fix(modals): correctly set message prop on interaction if present
monbrey Feb 22, 2022
77604a6
types: fix after rebase
monbrey Mar 26, 2022
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
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',
monbrey marked this conversation as resolved.
Show resolved Hide resolved

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() {}
monbrey marked this conversation as resolved.
Show resolved Hide resolved
}

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
monbrey marked this conversation as resolved.
Show resolved Hide resolved
* @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;