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
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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/discord.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions packages/discord.js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
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');
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
Expand Up @@ -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');
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 @@ -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);
}

/**
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');
const InteractionWebhook = require('./InteractionWebhook');
const InteractionResponses = require('./interfaces/InteractionResponses');
const { findComponentByCustomId } = require('../util/Components');

const getMessage = lazy(() => require('./Message').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
3 changes: 1 addition & 2 deletions packages/discord.js/src/structures/MessagePayload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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;
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');
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