Skip to content

feat: components v2 in v14 #10781

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

Merged
merged 23 commits into from
Apr 25, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -565,7 +565,7 @@ describe('Slash Commands', () => {
});

describe('integration types', () => {
test('GIVEN a builder with valid integraton types THEN does not throw an error', () => {
test('GIVEN a builder with valid integration types THEN does not throw an error', () => {
expect(() =>
getBuilder().setIntegrationTypes([
ApplicationIntegrationType.GuildInstall,
4 changes: 2 additions & 2 deletions packages/discord.js/package.json
Original file line number Diff line number Diff line change
@@ -65,14 +65,14 @@
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"@discordjs/builders": "^1.10.1",
"@discordjs/builders": "^1.11.0",
"@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.0",
"@discordjs/rest": "workspace:^",
"@discordjs/util": "workspace:^",
"@discordjs/ws": "^1.2.1",
"@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.37.119",
"discord-api-types": "^0.38.1",
"fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1",
"magic-bytes.js": "^1.10.0",
9 changes: 9 additions & 0 deletions packages/discord.js/src/index.js
Original file line number Diff line number Diff line change
@@ -124,12 +124,14 @@ exports.CommandInteraction = require('./structures/CommandInteraction');
exports.Collector = require('./structures/interfaces/Collector');
exports.CommandInteractionOptionResolver = require('./structures/CommandInteractionOptionResolver');
exports.Component = require('./structures/Component');
exports.ContainerComponent = require('./structures/ContainerComponent');
exports.ContextMenuCommandInteraction = require('./structures/ContextMenuCommandInteraction');
exports.DMChannel = require('./structures/DMChannel');
exports.Embed = require('./structures/Embed');
exports.EmbedBuilder = require('./structures/EmbedBuilder');
exports.Emoji = require('./structures/Emoji').Emoji;
exports.Entitlement = require('./structures/Entitlement').Entitlement;
exports.FileComponent = require('./structures/FileComponent');
exports.ForumChannel = require('./structures/ForumChannel');
exports.Guild = require('./structures/Guild').Guild;
exports.GuildAuditLogs = require('./structures/GuildAuditLogs');
@@ -162,6 +164,8 @@ exports.Attachment = require('./structures/Attachment');
exports.AttachmentBuilder = require('./structures/AttachmentBuilder');
exports.ModalBuilder = require('./structures/ModalBuilder');
exports.MediaChannel = require('./structures/MediaChannel');
exports.MediaGalleryComponent = require('./structures/MediaGalleryComponent');
exports.MediaGalleryItem = require('./structures/MediaGalleryItem');
exports.MessageCollector = require('./structures/MessageCollector');
exports.MessageComponentInteraction = require('./structures/MessageComponentInteraction');
exports.MessageContextMenuCommandInteraction = require('./structures/MessageContextMenuCommandInteraction');
@@ -181,6 +185,7 @@ exports.ReactionCollector = require('./structures/ReactionCollector');
exports.ReactionEmoji = require('./structures/ReactionEmoji');
exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets;
exports.Role = require('./structures/Role').Role;
exports.SectionComponent = require('./structures/SectionComponent');
exports.SelectMenuBuilder = require('./structures/SelectMenuBuilder');
exports.ChannelSelectMenuBuilder = require('./structures/ChannelSelectMenuBuilder');
exports.MentionableSelectMenuBuilder = require('./structures/MentionableSelectMenuBuilder');
@@ -202,6 +207,7 @@ exports.RoleSelectMenuInteraction = require('./structures/RoleSelectMenuInteract
exports.StringSelectMenuInteraction = require('./structures/StringSelectMenuInteraction');
exports.UserSelectMenuInteraction = require('./structures/UserSelectMenuInteraction');
exports.SelectMenuOptionBuilder = require('./structures/SelectMenuOptionBuilder');
exports.SeparatorComponent = require('./structures/SeparatorComponent');
exports.SKU = require('./structures/SKU').SKU;
exports.SoundboardSound = require('./structures/SoundboardSound.js').SoundboardSound;
exports.StringSelectMenuOptionBuilder = require('./structures/StringSelectMenuOptionBuilder');
@@ -213,12 +219,15 @@ exports.StickerPack = require('./structures/StickerPack');
exports.Team = require('./structures/Team');
exports.TeamMember = require('./structures/TeamMember');
exports.TextChannel = require('./structures/TextChannel');
exports.TextDisplayComponent = require('./structures/TextDisplayComponent');
exports.TextInputBuilder = require('./structures/TextInputBuilder');
exports.TextInputComponent = require('./structures/TextInputComponent');
exports.ThreadChannel = require('./structures/ThreadChannel');
exports.ThreadMember = require('./structures/ThreadMember');
exports.ThreadOnlyChannel = require('./structures/ThreadOnlyChannel');
exports.ThumbnailComponent = require('./structures/ThumbnailComponent');
exports.Typing = require('./structures/Typing');
exports.UnfurledMediaItem = require('./structures/UnfurledMediaItem');
exports.User = require('./structures/User');
exports.UserContextMenuCommandInteraction = require('./structures/UserContextMenuCommandInteraction');
exports.VoiceChannelEffect = require('./structures/VoiceChannelEffect');
9 changes: 9 additions & 0 deletions packages/discord.js/src/structures/Component.js
Original file line number Diff line number Diff line change
@@ -14,6 +14,15 @@ class Component {
this.data = data;
}

/**
* The id of this component
* @type {number}
* @readonly
*/
get id() {
return this.data.id;
}

/**
* The type of the component
* @type {ComponentType}
60 changes: 60 additions & 0 deletions packages/discord.js/src/structures/ContainerComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

const Component = require('./Component');
const { createComponent } = require('../util/Components');

/**
* Represents a container component
* @extends {Component}
*/
class ContainerComponent extends Component {
constructor({ components, ...data }) {
super(data);

/**
* The components in this container
* @type {Component[]}
* @readonly
*/
this.components = components.map(component => createComponent(component));
}

/**
* The accent color of this container
* @type {?number}
* @readonly
*/
get accentColor() {
return this.data.accent_color ?? null;
}

/**
* The hex accent color of this container
* @type {?string}
* @readonly
*/
get hexAccentColor() {
return typeof this.data.accent_color === 'number'
? `#${this.data.accent_color.toString(16).padStart(6, '0')}`
: (this.data.accent_color ?? null);
}

/**
* Whether this container is spoilered
* @type {boolean}
* @readonly
*/
get spoiler() {
return this.data.spoiler ?? false;
}

/**
* Returns the API-compatible JSON for this component
* @returns {APIContainerComponent}
*/
toJSON() {
return { ...this.data, components: this.components.map(component => component.toJSON()) };
}
}

module.exports = ContainerComponent;
40 changes: 40 additions & 0 deletions packages/discord.js/src/structures/FileComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict';

const Component = require('./Component');
const UnfurledMediaItem = require('./UnfurledMediaItem');

/**
* Represents a file component
* @extends {Component}
*/
class FileComponent extends Component {
constructor({ file, ...data }) {
super(data);

/**
* The media associated with this file
* @type {UnfurledMediaItem}
* @readonly
*/
this.file = new UnfurledMediaItem(file);
}

/**
* Whether this thumbnail is spoilered
* @type {boolean}
* @readonly
*/
get spoiler() {
return this.data.spoiler ?? false;
}

/**
* Returns the API-compatible JSON for this component
* @returns {APIFileComponent}
*/
toJSON() {
return { ...this.data, file: this.file.toJSON() };
}
}

module.exports = FileComponent;
31 changes: 31 additions & 0 deletions packages/discord.js/src/structures/MediaGalleryComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';

const Component = require('./Component');
const MediaGalleryItem = require('./MediaGalleryItem');

/**
* Represents a media gallery component
* @extends {Component}
*/
class MediaGalleryComponent extends Component {
constructor({ items, ...data }) {
super(data);

/**
* The items in this media gallery
* @type {MediaGalleryItem[]}
* @readonly
*/
this.items = items.map(item => new MediaGalleryItem(item));
}

/**
* Returns the API-compatible JSON for this component
* @returns {APIMediaGalleryComponent}
*/
toJSON() {
return { ...this.data, items: this.items.map(item => item.toJSON()) };
}
}

module.exports = MediaGalleryComponent;
51 changes: 51 additions & 0 deletions packages/discord.js/src/structures/MediaGalleryItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';

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

/**
* Represents an item in a media gallery
*/
class MediaGalleryItem {
constructor({ media, ...data }) {
/**
* The API data associated with this component
* @type {APIMediaGalleryItem}
*/
this.data = data;

/**
* The media associated with this media gallery item
* @type {UnfurledMediaItem}
* @readonly
*/
this.media = new UnfurledMediaItem(media);
}

/**
* The description of this media gallery item
* @type {?string}
* @readonly
*/
get description() {
return this.data.description ?? null;
}

/**
* Whether this media gallery item is spoilered
* @type {boolean}
* @readonly
*/
get spoiler() {
return this.data.spoiler ?? false;
}

/**
* Returns the API-compatible JSON for this component
* @returns {APIMediaGalleryItem}
*/
toJSON() {
return { ...this.data, media: this.media.toJSON() };
}
}

module.exports = MediaGalleryItem;
8 changes: 4 additions & 4 deletions packages/discord.js/src/structures/Message.js
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ const ReactionCollector = require('./ReactionCollector');
const { Sticker } = require('./Sticker');
const { DiscordjsError, ErrorCodes } = require('../errors');
const ReactionManager = require('../managers/ReactionManager');
const { createComponent } = require('../util/Components');
const { createComponent, findComponentByCustomId } = require('../util/Components');
const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, UndeletableMessageTypes } = require('../util/Constants');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
const PermissionsBitField = require('../util/PermissionsBitField');
@@ -151,10 +151,10 @@ class Message extends Base {

if ('components' in data) {
/**
* An array of action rows in the message.
* An array of components in the message.
* <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent
* in a guild for messages that do not mention the client.</info>
* @type {ActionRow[]}
* @type {Component[]}
*/
this.components = data.components.map(component => createComponent(component));
} else {
@@ -1055,7 +1055,7 @@ class Message extends Base {
* @returns {?MessageActionRowComponent}
*/
resolveComponent(customId) {
return this.components.flatMap(row => row.components).find(component => component.customId === customId) ?? null;
return findComponentByCustomId(this.components, customId);
}

/**
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ const { lazy } = require('@discordjs/util');
const BaseInteraction = require('./BaseInteraction');
const InteractionWebhook = require('./InteractionWebhook');
const InteractionResponses = require('./interfaces/InteractionResponses');
const { findComponentByCustomId } = require('../util/Components');

const getMessage = lazy(() => require('./Message').Message);

@@ -79,13 +80,11 @@ class MessageComponentInteraction extends BaseInteraction {

/**
* The component which was interacted with
* @type {MessageActionRowComponent|APIMessageActionRowComponent}
* @type {MessageActionRowComponent|APIComponentInMessageActionRow}
* @readonly
*/
get component() {
return this.message.components
.flatMap(row => row.components)
.find(component => (component.customId ?? component.custom_id) === this.customId);
return findComponentByCustomId(this.message.components, this.customId);
}

// These are here only for documentation purposes - they are implemented by InteractionResponses
3 changes: 1 addition & 2 deletions packages/discord.js/src/structures/MessagePayload.js
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@ const { Buffer } = require('node:buffer');
const { lazy, isJSONEncodable } = require('@discordjs/util');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { MessageFlags, MessageReferenceType } = require('discord-api-types/v10');
const ActionRowBuilder = require('./ActionRowBuilder');
const { DiscordjsError, DiscordjsRangeError, ErrorCodes } = require('../errors');
const { resolveFile } = require('../util/DataResolver');
const MessageFlagsBitField = require('../util/MessageFlagsBitField');
@@ -149,7 +148,7 @@ class MessagePayload {
}

const components = this.options.components?.map(component =>
(isJSONEncodable(component) ? component : new ActionRowBuilder(component)).toJSON(),
isJSONEncodable(component) ? component.toJSON() : this.target.client.options.jsonTransformer(component),
);

let username;
42 changes: 42 additions & 0 deletions packages/discord.js/src/structures/SectionComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

const Component = require('./Component');
const { createComponent } = require('../util/Components');

/**
* Represents a section component
* @extends {Component}
*/
class SectionComponent extends Component {
constructor({ accessory, components, ...data }) {
super(data);

/**
* The components in this section
* @type {Component[]}
* @readonly
*/
this.components = components.map(component => createComponent(component));

/**
* The accessory component of this section
* @type {Component}
* @readonly
*/
this.accessory = createComponent(accessory);
}

/**
* Returns the API-compatible JSON for this component
* @returns {APISectionComponent}
*/
toJSON() {
return {
...this.data,
accessory: this.accessory.toJSON(),
components: this.components.map(component => component.toJSON()),
};
}
}

module.exports = SectionComponent;
Loading