Skip to content

feat: components v2 #10847

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 19 commits into from
May 11, 2025
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion packages/discord.js/.lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://json.schemastore.org/lintstagedrc.schema.json",
"*": "prettier --ignore-unknown --write",
"{src/**,test/**,typings/**,scripts/**}.{mjs,js,ts}": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint --ext mjs,js,ts --fix"
"{src/**,test/**,typings/**,scripts/**}.{mjs,js,ts}": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint --fix --format=pretty"
}
9 changes: 9 additions & 0 deletions packages/discord.js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,14 @@ exports.CommandInteraction = require('./structures/CommandInteraction.js').Comma
exports.CommandInteractionOptionResolver =
require('./structures/CommandInteractionOptionResolver.js').CommandInteractionOptionResolver;
exports.Component = require('./structures/Component.js').Component;
exports.ContainerComponent = require('./structures/ContainerComponent.js').ContainerComponent;
exports.ContextMenuCommandInteraction =
require('./structures/ContextMenuCommandInteraction.js').ContextMenuCommandInteraction;
exports.DMChannel = require('./structures/DMChannel.js').DMChannel;
exports.Embed = require('./structures/Embed.js').Embed;
exports.Emoji = require('./structures/Emoji.js').Emoji;
exports.Entitlement = require('./structures/Entitlement.js').Entitlement;
exports.FileComponent = require('./structures/FileComponent.js').FileComponent;
exports.ForumChannel = require('./structures/ForumChannel.js').ForumChannel;
exports.Guild = require('./structures/Guild.js').Guild;
exports.GuildAuditLogs = require('./structures/GuildAuditLogs.js').GuildAuditLogs;
Expand Down Expand Up @@ -165,6 +167,8 @@ exports.InteractionWebhook = require('./structures/InteractionWebhook.js').Inter
exports.Invite = require('./structures/Invite.js').Invite;
exports.InviteGuild = require('./structures/InviteGuild.js').InviteGuild;
exports.MediaChannel = require('./structures/MediaChannel.js').MediaChannel;
exports.MediaGalleryComponent = require('./structures/MediaGalleryComponent.js').MediaGalleryComponent;
exports.MediaGalleryItem = require('./structures/MediaGalleryItem.js').MediaGalleryItem;
exports.MentionableSelectMenuComponent =
require('./structures/MentionableSelectMenuComponent.js').MentionableSelectMenuComponent;
exports.MentionableSelectMenuInteraction =
Expand Down Expand Up @@ -196,6 +200,8 @@ exports.RichPresenceAssets = require('./structures/Presence.js').RichPresenceAss
exports.Role = require('./structures/Role.js').Role;
exports.RoleSelectMenuComponent = require('./structures/RoleSelectMenuComponent.js').RoleSelectMenuComponent;
exports.RoleSelectMenuInteraction = require('./structures/RoleSelectMenuInteraction.js').RoleSelectMenuInteraction;
exports.SectionComponent = require('./structures/SectionComponent.js').SectionComponent;
exports.SeparatorComponent = require('./structures/SeparatorComponent.js').SeparatorComponent;
exports.SKU = require('./structures/SKU.js').SKU;
exports.SoundboardSound = require('./structures/SoundboardSound.js').SoundboardSound;
exports.StageChannel = require('./structures/StageChannel.js').StageChannel;
Expand All @@ -209,11 +215,14 @@ exports.Subscription = require('./structures/Subscription.js').Subscription;
exports.Team = require('./structures/Team.js').Team;
exports.TeamMember = require('./structures/TeamMember.js').TeamMember;
exports.TextChannel = require('./structures/TextChannel.js').TextChannel;
exports.TextDisplayComponent = require('./structures/TextDisplayComponent.js').TextDisplayComponent;
exports.TextInputComponent = require('./structures/TextInputComponent.js').TextInputComponent;
exports.ThreadChannel = require('./structures/ThreadChannel.js').ThreadChannel;
exports.ThreadMember = require('./structures/ThreadMember.js').ThreadMember;
exports.ThreadOnlyChannel = require('./structures/ThreadOnlyChannel.js').ThreadOnlyChannel;
exports.ThumbnailComponent = require('./structures/ThumbnailComponent.js').ThumbnailComponent;
exports.Typing = require('./structures/Typing.js').Typing;
exports.UnfurledMediaItem = require('./structures/UnfurledMediaItem.js').UnfurledMediaItem;
exports.User = require('./structures/User.js').User;
exports.UserContextMenuCommandInteraction =
require('./structures/UserContextMenuCommandInteraction.js').UserContextMenuCommandInteraction;
Expand Down
9 changes: 9 additions & 0 deletions packages/discord.js/src/structures/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
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.js');
const { createComponent } = require('../util/Components.js');

/**
* 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()) };
}
}

exports.ContainerComponent = 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.js');
const { UnfurledMediaItem } = require('./UnfurledMediaItem.js');

/**
* 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() };
}
}

exports.FileComponent = 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.js');
const { MediaGalleryItem } = require('./MediaGalleryItem.js');

/**
* 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()) };
}
}

exports.MediaGalleryComponent = 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.js');

/**
* 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() };
}
}

exports.MediaGalleryItem = MediaGalleryItem;
8 changes: 4 additions & 4 deletions packages/discord.js/src/structures/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const { ReactionCollector } = require('./ReactionCollector.js');
const { Sticker } = require('./Sticker.js');
const { DiscordjsError, ErrorCodes } = require('../errors/index.js');
const { ReactionManager } = require('../managers/ReactionManager.js');
const { createComponent } = require('../util/Components.js');
const { createComponent, findComponentByCustomId } = require('../util/Components.js');
const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, UndeletableMessageTypes } = require('../util/Constants.js');
const { MessageFlagsBitField } = require('../util/MessageFlagsBitField.js');
const { PermissionsBitField } = require('../util/PermissionsBitField.js');
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1032,7 +1032,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);
}

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

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

Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion packages/discord.js/src/structures/MessagePayload.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ class MessagePayload {
components,
username,
avatar_url: avatarURL,
allowed_mentions: content === undefined && message_reference === undefined ? undefined : allowedMentions,
allowed_mentions:
this.isMessage && message_reference === undefined && this.target.author.id !== this.target.client.user.id
? undefined
: allowedMentions,
flags,
message_reference,
attachments: this.options.attachments,
Expand Down
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.js');
const { createComponent } = require('../util/Components.js');

/**
* 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()),
};
}
}

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

const { SeparatorSpacingSize } = require('discord-api-types/v10');
const { Component } = require('./Component.js');

/**
* Represents a separator component
* @extends {Component}
*/
class SeparatorComponent extends Component {
/**
* The spacing of this separator
* @type {SeparatorSpacingSize}
* @readonly
*/
get spacing() {
return this.data.spacing ?? SeparatorSpacingSize.Small;
}

/**
* Whether this separator is a divider
* @type {boolean}
* @readonly
*/
get divider() {
return this.data.divider ?? true;
}
}

exports.SeparatorComponent = SeparatorComponent;
Loading