From 4663e0ea1723419082799556b31d0c11008a5f9d Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Fri, 25 Apr 2025 19:46:34 +0200
Subject: [PATCH 01/12] feat: components v2
---
packages/discord.js/src/index.js | 9 +
.../discord.js/src/structures/Component.js | 9 +
.../src/structures/ContainerComponent.js | 60 ++++++
.../src/structures/FileComponent.js | 40 ++++
.../src/structures/MediaGalleryComponent.js | 31 +++
.../src/structures/MediaGalleryItem.js | 51 +++++
packages/discord.js/src/structures/Message.js | 31 +--
.../structures/MessageComponentInteraction.js | 29 ++-
.../src/structures/SectionComponent.js | 42 ++++
.../src/structures/SeparatorComponent.js | 30 +++
.../src/structures/TextDisplayComponent.js | 20 ++
.../src/structures/ThumbnailComponent.js | 49 +++++
.../src/structures/UnfurledMediaItem.js | 25 +++
.../structures/interfaces/TextBasedChannel.js | 11 +-
packages/discord.js/src/util/APITypes.js | 44 ++++-
packages/discord.js/src/util/Components.js | 148 +++++++++++++--
packages/discord.js/typings/index.d.ts | 179 +++++++++++++++++-
packages/discord.js/typings/index.test-d.ts | 61 ++++++
18 files changed, 807 insertions(+), 62 deletions(-)
create mode 100644 packages/discord.js/src/structures/ContainerComponent.js
create mode 100644 packages/discord.js/src/structures/FileComponent.js
create mode 100644 packages/discord.js/src/structures/MediaGalleryComponent.js
create mode 100644 packages/discord.js/src/structures/MediaGalleryItem.js
create mode 100644 packages/discord.js/src/structures/SectionComponent.js
create mode 100644 packages/discord.js/src/structures/SeparatorComponent.js
create mode 100644 packages/discord.js/src/structures/TextDisplayComponent.js
create mode 100644 packages/discord.js/src/structures/ThumbnailComponent.js
create mode 100644 packages/discord.js/src/structures/UnfurledMediaItem.js
diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js
index 668b6f080dff..6894eb1bca92 100644
--- a/packages/discord.js/src/index.js
+++ b/packages/discord.js/src/index.js
@@ -130,12 +130,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;
@@ -164,6 +166,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 =
@@ -195,6 +199,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.StageChannel = require('./structures/StageChannel.js').StageChannel;
exports.StageInstance = require('./structures/StageInstance.js').StageInstance;
@@ -207,11 +213,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;
diff --git a/packages/discord.js/src/structures/Component.js b/packages/discord.js/src/structures/Component.js
index fbe464f76278..773335bba4b4 100644
--- a/packages/discord.js/src/structures/Component.js
+++ b/packages/discord.js/src/structures/Component.js
@@ -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}
diff --git a/packages/discord.js/src/structures/ContainerComponent.js b/packages/discord.js/src/structures/ContainerComponent.js
new file mode 100644
index 000000000000..3eb412e96c18
--- /dev/null
+++ b/packages/discord.js/src/structures/ContainerComponent.js
@@ -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;
diff --git a/packages/discord.js/src/structures/FileComponent.js b/packages/discord.js/src/structures/FileComponent.js
new file mode 100644
index 000000000000..e63750281da4
--- /dev/null
+++ b/packages/discord.js/src/structures/FileComponent.js
@@ -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;
diff --git a/packages/discord.js/src/structures/MediaGalleryComponent.js b/packages/discord.js/src/structures/MediaGalleryComponent.js
new file mode 100644
index 000000000000..3531e3c163d9
--- /dev/null
+++ b/packages/discord.js/src/structures/MediaGalleryComponent.js
@@ -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;
diff --git a/packages/discord.js/src/structures/MediaGalleryItem.js b/packages/discord.js/src/structures/MediaGalleryItem.js
new file mode 100644
index 000000000000..7b608dcdace6
--- /dev/null
+++ b/packages/discord.js/src/structures/MediaGalleryItem.js
@@ -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;
diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js
index 8a07ed612e80..d5438f1c3668 100644
--- a/packages/discord.js/src/structures/Message.js
+++ b/packages/discord.js/src/structures/Message.js
@@ -14,6 +14,7 @@ const {
const { Attachment } = require('./Attachment.js');
const { Base } = require('./Base.js');
const { ClientApplication } = require('./ClientApplication.js');
+const { Component } = require('./Component.js');
const { Embed } = require('./Embed.js');
const { InteractionCollector } = require('./InteractionCollector.js');
const { MessageMentions } = require('./MessageMentions.js');
@@ -23,7 +24,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');
@@ -151,10 +152,10 @@ class Message extends Base {
if ('components' in data) {
/**
- * An array of action rows in the message.
+ * An array of components in the message.
* This property requires the {@link GatewayIntentBits.MessageContent} privileged intent
* in a guild for messages that do not mention the client.
- * @type {ActionRow[]}
+ * @type {Component[]}
*/
this.components = data.components.map(component => createComponent(component));
} else {
@@ -683,8 +684,8 @@ class Message extends Base {
get editable() {
const precheck = Boolean(
this.author.id === this.client.user.id &&
- (!this.guild || this.channel?.viewable) &&
- this.reference?.type !== MessageReferenceType.Forward,
+ (!this.guild || this.channel?.viewable) &&
+ this.reference?.type !== MessageReferenceType.Forward,
);
// Regardless of permissions thread messages cannot be edited if
@@ -755,9 +756,9 @@ class Message extends Base {
const { channel } = this;
return Boolean(
!this.system &&
- (!this.guild ||
- (channel?.viewable &&
- channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))),
+ (!this.guild ||
+ (channel?.viewable &&
+ channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))),
);
}
@@ -787,12 +788,12 @@ class Message extends Base {
const { channel } = this;
return Boolean(
channel?.type === ChannelType.GuildAnnouncement &&
- !this.flags.has(MessageFlags.Crossposted) &&
- this.reference?.type !== MessageReferenceType.Forward &&
- this.type === MessageType.Default &&
- !this.poll &&
- channel.viewable &&
- channel.permissionsFor(this.client.user)?.has(bitfield, false),
+ !this.flags.has(MessageFlags.Crossposted) &&
+ this.reference?.type !== MessageReferenceType.Forward &&
+ this.type === MessageType.Default &&
+ !this.poll &&
+ channel.viewable &&
+ channel.permissionsFor(this.client.user)?.has(bitfield, false),
);
}
@@ -1032,7 +1033,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);
}
/**
diff --git a/packages/discord.js/src/structures/MessageComponentInteraction.js b/packages/discord.js/src/structures/MessageComponentInteraction.js
index 296451d84259..2089547e4778 100644
--- a/packages/discord.js/src/structures/MessageComponentInteraction.js
+++ b/packages/discord.js/src/structures/MessageComponentInteraction.js
@@ -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);
@@ -79,28 +80,26 @@ 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
/* eslint-disable no-empty-function */
- deferReply() {}
- reply() {}
- fetchReply() {}
- editReply() {}
- deleteReply() {}
- followUp() {}
- deferUpdate() {}
- update() {}
- launchActivity() {}
- showModal() {}
- awaitModalSubmit() {}
+ deferReply() { }
+ reply() { }
+ fetchReply() { }
+ editReply() { }
+ deleteReply() { }
+ followUp() { }
+ deferUpdate() { }
+ update() { }
+ launchActivity() { }
+ showModal() { }
+ awaitModalSubmit() { }
}
InteractionResponses.applyToClass(MessageComponentInteraction);
diff --git a/packages/discord.js/src/structures/SectionComponent.js b/packages/discord.js/src/structures/SectionComponent.js
new file mode 100644
index 000000000000..6161332c4dd5
--- /dev/null
+++ b/packages/discord.js/src/structures/SectionComponent.js
@@ -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;
diff --git a/packages/discord.js/src/structures/SeparatorComponent.js b/packages/discord.js/src/structures/SeparatorComponent.js
new file mode 100644
index 000000000000..d4861cd0623f
--- /dev/null
+++ b/packages/discord.js/src/structures/SeparatorComponent.js
@@ -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;
diff --git a/packages/discord.js/src/structures/TextDisplayComponent.js b/packages/discord.js/src/structures/TextDisplayComponent.js
new file mode 100644
index 000000000000..b31624f27326
--- /dev/null
+++ b/packages/discord.js/src/structures/TextDisplayComponent.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const { Component } = require('./Component.js');
+
+/**
+ * Represents a text display component
+ * @extends {Component}
+ */
+class TextDisplayComponent extends Component {
+ /**
+ * The content of this text display
+ * @type {string}
+ * @readonly
+ */
+ get content() {
+ return this.data.content;
+ }
+}
+
+exports.TextDisplayComponent = TextDisplayComponent;
diff --git a/packages/discord.js/src/structures/ThumbnailComponent.js b/packages/discord.js/src/structures/ThumbnailComponent.js
new file mode 100644
index 000000000000..9a8c70369908
--- /dev/null
+++ b/packages/discord.js/src/structures/ThumbnailComponent.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const { Component } = require('./Component.js');
+const { UnfurledMediaItem } = require('./UnfurledMediaItem.js');
+
+/**
+ * Represents a thumbnail component
+ * @extends {Component}
+ */
+class ThumbnailComponent extends Component {
+ constructor({ media, ...data }) {
+ super(data);
+
+ /**
+ * The media associated with this thumbnail
+ * @type {UnfurledMediaItem}
+ * @readonly
+ */
+ this.media = new UnfurledMediaItem(media);
+ }
+
+ /**
+ * The description of this thumbnail
+ * @type {?string}
+ * @readonly
+ */
+ get description() {
+ return this.data.description ?? null;
+ }
+
+ /**
+ * Whether this thumbnail is spoilered
+ * @type {boolean}
+ * @readonly
+ */
+ get spoiler() {
+ return this.data.spoiler ?? false;
+ }
+
+ /**
+ * Returns the API-compatible JSON for this component
+ * @returns {APIThumbnailComponent}
+ */
+ toJSON() {
+ return { ...this.data, media: this.media.toJSON() };
+ }
+}
+
+exports.ThumbnailComponent = ThumbnailComponent;
diff --git a/packages/discord.js/src/structures/UnfurledMediaItem.js b/packages/discord.js/src/structures/UnfurledMediaItem.js
new file mode 100644
index 000000000000..79e0dcf3bc10
--- /dev/null
+++ b/packages/discord.js/src/structures/UnfurledMediaItem.js
@@ -0,0 +1,25 @@
+'use strict';
+
+/**
+ * Represents a media item in a component
+ */
+class UnfurledMediaItem {
+ constructor(data) {
+ /**
+ * The API data associated with this media item
+ * @type {APIUnfurledMediaItem}
+ */
+ this.data = data;
+ }
+
+ /**
+ * The URL of this media gallery item
+ * @type {string}
+ * @readonly
+ */
+ get url() {
+ return this.data.url;
+ }
+}
+
+exports.UnfurledMediaItem = UnfurledMediaItem;
diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js
index d7ab76b6e6d5..8b32dee1ad9c 100644
--- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js
+++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js
@@ -77,8 +77,11 @@ class TextBasedChannel {
* (see {@link https://discord.com/developers/docs/resources/message#allowed-mentions-object here} for more details)
* @property {Array<(AttachmentBuilder|Attachment|AttachmentPayload|BufferResolvable)>} [files]
* The files to send with the message.
- * @property {Array<(ActionRowBuilder|ActionRow|APIActionRowComponent)>} [components]
- * Action rows containing interactive components for the message (buttons, select menus)
+ * @property {Array<(ActionRowBuilder|MessageTopLevelComponent|APIMessageTopLevelComponent)>} [components]
+ * Action rows containing interactive components for the message (buttons, select menus) and other
+ * top-level components.
+ * When using components v2, the flag {@link MessageFlags.IsComponentsV2} needs to be set
+ * and `content`, `embeds`, `stickers`, and `poll` cannot be used.
*/
/**
@@ -98,7 +101,9 @@ class TextBasedChannel {
* that message will be returned and no new message will be created
* @property {StickerResolvable[]} [stickers=[]] The stickers to send in the message
* @property {MessageFlags} [flags] Which flags to set for the message.
- * Only `MessageFlags.SuppressEmbeds` and `MessageFlags.SuppressNotifications` can be set.
+ * Only {@link MessageFlags.SuppressEmbeds}, {@link MessageFlags.SuppressNotifications} and
+ * {@link MessageFlags.IsComponentsV2} can be set.
+ * {@link MessageFlags.IsComponentsV2} is required if passing components that aren't action rows
*/
/**
diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js
index c40af6c289cc..60025d98a772 100644
--- a/packages/discord.js/src/util/APITypes.js
+++ b/packages/discord.js/src/util/APITypes.js
@@ -60,6 +60,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIChannelSelectComponent}
*/
+/**
+ * @external APIContainerComponent
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIContainerComponent}
+ */
+
/**
* @external APIEmbed
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIEmbed}
@@ -80,6 +85,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIEmoji}
*/
+/**
+ * @external APIFileComponent
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIFileComponent}
+ */
+
/**
* @external APIGuild
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIGuild}
@@ -135,6 +145,16 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIInteractionGuildMember}
*/
+/**
+ * @external APIMediaGalleryComponent
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMediaGalleryComponent}
+ */
+
+/**
+ * @external APIMediaGalleryItem
+ * @se {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMediaGalleryItem}
+ */
+
/**
* @external APIMentionableSelectComponent
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIMentionableSelectComponent}
@@ -146,8 +166,8 @@
*/
/**
- * @external APIMessageActionRowComponent
- * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIMessageActionRowComponent}
+ * @external APIComponentInMessageActionRow
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIComponentInMessageActionRow}
*/
/**
@@ -165,6 +185,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMessageInteractionMetadata}
*/
+/**
+ * @external APIMessageTopLevelComponent
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIMessageTopLevelComponent}
+ */
+
/**
* @external APIModalInteractionResponse
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIModalInteractionResponse}
@@ -210,6 +235,11 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISelectMenuOption}
*/
+/**
+ * @external APISectionComponent
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISectionComponent}
+ */
+
/**
* @external APISticker
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APISticker}
@@ -225,6 +255,16 @@
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APITextInputComponent}
*/
+/**
+ * @external APIThumbnailComponent
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIThumbnailComponent}
+ */
+
+/**
+ * @external APIUnfurledMediaItem
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIUnfurledMediaItem}
+ */
+
/**
* @external APIUser
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIUser}
diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js
index 5144de04564e..9d010d18df86 100644
--- a/packages/discord.js/src/util/Components.js
+++ b/packages/discord.js/src/util/Components.js
@@ -4,6 +4,7 @@ const { ComponentType } = require('discord-api-types/v10');
/**
* @typedef {Object} BaseComponentData
+ * @property {number} [id] the id of this component
* @property {ComponentType} type The type of component
*/
@@ -15,30 +16,30 @@ const { ComponentType } = require('discord-api-types/v10');
/**
* @typedef {BaseComponentData} ButtonComponentData
* @property {ButtonStyle} style The style of the button
- * @property {?boolean} disabled Whether this button is disabled
+ * @property {boolean} [disabled] Whether this button is disabled
* @property {string} label The label of this button
- * @property {?APIMessageComponentEmoji} emoji The emoji on this button
- * @property {?string} customId The custom id of the button
- * @property {?string} url The URL of the button
+ * @property {APIMessageComponentEmoji} [emoji] The emoji on this button
+ * @property {string} [customId] The custom id of the button
+ * @property {string} [url] The URL of the button
*/
/**
* @typedef {object} SelectMenuComponentOptionData
* @property {string} label The label of the option
* @property {string} value The value of the option
- * @property {?string} description The description of the option
- * @property {?APIMessageComponentEmoji} emoji The emoji on the option
- * @property {?boolean} default Whether this option is selected by default
+ * @property {string} [description] The description of the option
+ * @property {APIMessageComponentEmoji} [emoji] The emoji on the option
+ * @property {boolean} [default] Whether this option is selected by default
*/
/**
* @typedef {BaseComponentData} SelectMenuComponentData
* @property {string} customId The custom id of the select menu
- * @property {?boolean} disabled Whether the select menu is disabled or not
- * @property {?number} maxValues The maximum amount of options that can be selected
- * @property {?number} minValues The minimum amount of options that can be selected
- * @property {?SelectMenuComponentOptionData[]} options The options in this select menu
- * @property {?string} placeholder The placeholder of the select menu
+ * @property {boolean} [disabled] Whether the select menu is disabled or not
+ * @property {number} [maxValues] The maximum amount of options that can be selected
+ * @property {number} [minValues] The minimum amount of options that can be selected
+ * @property {SelectMenuComponentOptionData[]} [options] The options in this select menu
+ * @property {string} [placeholder] The placeholder of the select menu
*/
/**
@@ -50,17 +51,83 @@ const { ComponentType } = require('discord-api-types/v10');
* @property {string} customId The custom id of the text input
* @property {TextInputStyle} style The style of the text input
* @property {string} label The text that appears on top of the text input field
- * @property {?number} minLength The minimum number of characters that can be entered in the text input
- * @property {?number} maxLength The maximum number of characters that can be entered in the text input
- * @property {?boolean} required Whether or not the text input is required or not
- * @property {?string} value The pre-filled text in the text input
- * @property {?string} placeholder Placeholder for the text input
+ * @property {number} [minLength] The minimum number of characters that can be entered in the text input
+ * @property {number} [maxLength] The maximum number of characters that can be entered in the text input
+ * @property {boolean} [required] Whether or not the text input is required or not
+ * @property {string} [value] The pre-filled text in the text input
+ * @property {string} [placeholder] Placeholder for the text input
*/
/**
- * @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData} ComponentData
+ * @typedef {Object} UnfurledMediaItemData
+ * @property {string} url The url of this media item. Accepts either http:, https: or attachment: protocol
*/
+/**
+ * @typedef {BaseComponentData} ThumbnailComponentData
+ * @property {UnfurledMediaItemData} media The media for the thumbnail
+ * @property {string} [description] The description of the thumbnail
+ * @property {boolean} [spoiler] Whether the thumbnail should be spoilered
+ */
+
+/**
+ * @typedef {BaseComponentData} FileComponentData
+ * @property {UnfurledMediaItemData} file The file media in this component
+ * @property {boolean} [spoiler] Whether the file should be spoilered
+ */
+
+/**
+ * @typedef {Object} MediaGalleryItemData
+ * @property {UnfurledMediaItemData} media The media for the media gallery item
+ * @property {string} [description] The description of the media gallery item
+ * @property {boolean} [spoiler] Whether the media gallery item should be spoilered
+ */
+
+/**
+ * @typedef {BaseComponentData} MediaGalleryComponentData
+ * @property {MediaGalleryItemData[]} items The media gallery items in this media gallery component
+ */
+
+/**
+ * @typedef {BaseComponentData} SeparatorComponentData
+ * @property {SeparatorSpacingSize} [spacing] The spacing size of this component
+ * @property {boolean} [divider] Whether the separator shows as a divider
+ */
+
+/**
+ * @typedef {BaseComponentData} SectionComponentData
+ * @property {Components[]} components The components in this section
+ * @property {ButtonComponentData|ThumbnailComponentData} accessory The accessory shown next to this section
+ */
+
+/**
+ * @typedef {BaseComponentData} TextDisplayComponentData
+ * @property {string} content The content displayed in this component
+ */
+
+/**
+ * @typedef {ActionRowData|FileComponentData|MediaGalleryComponentData|SectionComponentData|
+ * SeparatorComponentData|TextDisplayComponentData} ComponentInContainerData
+*/
+
+/**
+* @typedef {BaseComponentData} ContainerComponentData
+* @property {ComponentInContainerData} components The components in this container
+* @property {?number} [accentColor] The accent color of this container
+* @property {boolean} [spoiler] Whether the container should be spoilered
+*/
+
+/**
+* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData|
+* ThumbnailComponentData|FileComponentData|MediaGalleryComponentData|SeparatorComponentData|
+* SectionComponentData|TextDisplayComponentData|ContainerComponentData} ComponentData
+ */
+
+/**
+ * @typedef {ActionRow|ContainerComponent|FileComponent|MediaGalleryComponent|
+ * SectionComponent|SeparatorComponent|TextDisplayComponent} MessageTopLevelComponent
+*/
+
/**
* Transforms API data into a component
* @param {APIMessageComponent|Component} data The data to create the component from
@@ -89,19 +156,64 @@ function createComponent(data) {
return new MentionableSelectMenuComponent(data);
case ComponentType.ChannelSelect:
return new ChannelSelectMenuComponent(data);
+ case ComponentType.Container:
+ return new ContainerComponent(data);
+ case ComponentType.TextDisplay:
+ return new TextDisplayComponent(data);
+ case ComponentType.File:
+ return new FileComponent(data);
+ case ComponentType.MediaGallery:
+ return new MediaGalleryComponent(data);
+ case ComponentType.Section:
+ return new SectionComponent(data);
+ case ComponentType.Separator:
+ return new SeparatorComponent(data);
+ case ComponentType.Thumbnail:
+ return new ThumbnailComponent(data);
default:
return new Component(data);
}
}
+/**
+ * Finds a component by customId in nested components
+ * @param {Array} components The components to search in
+ * @param {string} customId The customId to search for
+ * @returns {Component|APIMessageComponent}
+ */
+function findComponentByCustomId(components, customId) {
+ return (
+ components
+ .flatMap(component => {
+ switch (component.type) {
+ case ComponentType.ActionRow:
+ return component.components;
+ case ComponentType.Section:
+ return [component.accessory];
+ default:
+ return [component];
+ }
+ })
+ .find(component => (component.customId ?? component.custom_id) === customId) ?? null
+ );
+}
+
exports.createComponent = createComponent;
+exports.findComponentByCustomId = findComponentByCustomId;
const { ActionRow } = require('../structures/ActionRow.js');
const { ButtonComponent } = require('../structures/ButtonComponent.js');
const { ChannelSelectMenuComponent } = require('../structures/ChannelSelectMenuComponent.js');
const { Component } = require('../structures/Component.js');
+const { ContainerComponent } = require('../structures/ContainerComponent.js');
+const { FileComponent } = require('../structures/FileComponent.js');
+const { MediaGalleryComponent } = require('../structures/MediaGalleryComponent.js');
const { MentionableSelectMenuComponent } = require('../structures/MentionableSelectMenuComponent.js');
const { RoleSelectMenuComponent } = require('../structures/RoleSelectMenuComponent.js');
+const { SectionComponent } = require('../structures/SectionComponent.js');
+const { SeparatorComponent } = require('../structures/SeparatorComponent.js');
const { StringSelectMenuComponent } = require('../structures/StringSelectMenuComponent.js');
+const { TextDisplayComponent } = require('../structures/TextDisplayComponent.js');
const { TextInputComponent } = require('../structures/TextInputComponent.js');
+const { ThumbnailComponent } = require('../structures/ThumbnailComponent.js');
const { UserSelectMenuComponent } = require('../structures/UserSelectMenuComponent.js');
diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts
index 5e209fcd9d4b..e29cb873e759 100644
--- a/packages/discord.js/typings/index.d.ts
+++ b/packages/discord.js/typings/index.d.ts
@@ -160,6 +160,18 @@ import {
GatewayVoiceChannelEffectSendDispatchData,
RESTAPIPoll,
EntryPointCommandHandlerType,
+ APIComponentInContainer,
+ APIContainerComponent,
+ APIThumbnailComponent,
+ APISectionComponent,
+ APITextDisplayComponent,
+ APIUnfurledMediaItem,
+ APIMediaGalleryItem,
+ APIMediaGalleryComponent,
+ APISeparatorComponent,
+ SeparatorSpacingSize,
+ APIFileComponent,
+ APIMessageTopLevelComponent,
} from 'discord-api-types/v10';
import { ChildProcess } from 'node:child_process';
import { Stream } from 'node:stream';
@@ -689,15 +701,37 @@ export class ButtonInteraction extends Mes
export type AnyComponent =
| APIMessageComponent
| APIModalComponent
- | APIActionRowComponent;
+ | APIActionRowComponent
+ | AnyComponentV2;
export class Component {
public readonly data: Readonly;
+ public get id(): RawComponentData['id'];
public get type(): RawComponentData['type'];
public toJSON(): RawComponentData;
public equals(other: this | RawComponentData): boolean;
}
+export type AnyComponentV2 = APIComponentInContainer | APIContainerComponent | APIThumbnailComponent;
+
+export type TopLevelComponent =
+ | ActionRow
+ | ContainerComponent
+ | FileComponent
+ | MediaGalleryComponent
+ | SectionComponent
+ | SeparatorComponent
+ | TextDisplayComponent;
+
+export type TopLevelComponentData =
+ | ActionRowData
+ | ContainerComponentData
+ | FileComponentData
+ | MediaGalleryComponentData
+ | SectionComponentData
+ | SeparatorComponentData
+ | TextDisplayComponentData;
+
export class ButtonComponent extends Component {
private constructor(data: APIButtonComponent);
public get style(): ButtonStyle;
@@ -978,6 +1012,40 @@ export class ClientVoiceManager {
public adapters: Map;
}
+export type ComponentInContainer =
+ | ActionRow
+ | FileComponent
+ | MediaGalleryComponent
+ | SectionComponent
+ | SeparatorComponent
+ | TextDisplayComponent;
+
+export type ComponentInContainerData =
+ | ActionRowData
+ | FileComponentData
+ | MediaGalleryComponentData
+ | SectionComponentData
+ | SeparatorComponentData
+ | TextDisplayComponentData;
+
+export interface ContainerComponentData<
+ ComponentType extends JSONEncodable | ComponentInContainerData =
+ | JSONEncodable
+ | ComponentInContainerData,
+> extends BaseComponentData {
+ components: readonly ComponentType[];
+ accentColor?: number;
+ spoiler?: boolean;
+}
+
+export class ContainerComponent extends Component {
+ private constructor(data: APIContainerComponent);
+ public get accentColor(): number;
+ public get hexAccentColor(): HexColorString;
+ public get spoiler(): boolean;
+ public readonly components: ComponentInContainer[];
+}
+
export { Collection, ReadonlyCollection } from '@discordjs/collection';
export interface CollectorEventTypes {
@@ -1275,6 +1343,16 @@ export class Entitlement extends Base {
public isGuildSubscription(): this is this & { guildId: Snowflake; guild: Guild };
}
+export interface FileComponentData extends BaseComponentData {
+ file: UnfurledMediaItemData;
+ spoiler?: boolean;
+}
+export class FileComponent extends Component {
+ private constructor(data: APIFileComponent);
+ public readonly file: UnfurledMediaItem;
+ public get spoiler(): boolean;
+}
+
export class Guild extends AnonymousGuild {
private constructor(client: Client, data: RawGuildData);
private _sortedRoles(): Collection;
@@ -1964,6 +2042,27 @@ export class LimitedCollection extends Collection {
public keepOverLimit: ((value: Value, key: Key, collection: this) => boolean) | null;
}
+export interface MediaGalleryComponentData extends BaseComponentData {
+ items: readonly MediaGalleryItemData[];
+}
+export class MediaGalleryComponent extends Component {
+ private constructor(data: APIMediaGalleryComponent);
+ public readonly items: MediaGalleryItem[];
+}
+
+export interface MediaGalleryItemData {
+ media: UnfurledMediaItemData;
+ description?: string;
+ spoiler?: boolean;
+}
+export class MediaGalleryItem {
+ private constructor(data: APIMediaGalleryItem);
+ public readonly data: APIMediaGalleryItem;
+ public readonly media: UnfurledMediaItem;
+ public get description(): string | null;
+ public get spoiler(): boolean;
+}
+
export interface MessageCall {
get endedAt(): Date | null;
endedTimestamp: number | null;
@@ -2036,7 +2135,7 @@ export class Message extends Base {
public get channel(): If;
public channelId: Snowflake;
public get cleanContent(): string;
- public components: ActionRow[];
+ public components: TopLevelComponent[];
public content: string;
public get createdAt(): Date;
public createdTimestamp: number;
@@ -2771,6 +2870,20 @@ export class RoleFlagsBitField extends BitField {
public static resolve(bit?: BitFieldResolvable): number;
}
+export interface SectionComponentData extends BaseComponentData {
+ accessory: ButtonComponentData | ThumbnailComponentData;
+ components: readonly TextDisplayComponentData[];
+}
+
+export class SectionComponent<
+ AccessoryType extends ButtonComponent | ThumbnailComponent = ButtonComponent | ThumbnailComponent,
+> extends Component {
+ private constructor(data: APISectionComponent);
+ public readonly accessory: AccessoryType;
+ public readonly components: TextDisplayComponent[];
+ public toJSON(): APISectionComponent;
+}
+
export class StringSelectMenuInteraction<
Cached extends CacheType = CacheType,
> extends MessageComponentInteraction {
@@ -2886,6 +2999,16 @@ export type SelectMenuInteraction =
export type SelectMenuType = APISelectMenuComponent['type'];
+export interface SeparatorComponentData extends BaseComponentData {
+ spacing?: SeparatorSpacingSize;
+ dividier?: boolean;
+}
+export class SeparatorComponent extends Component {
+ private constructor(data: APISeparatorComponent);
+ public get spacing(): SeparatorSpacingSize;
+ public get divider(): boolean;
+}
+
export interface ShardEventTypes {
death: [process: ChildProcess | Worker];
disconnect: [];
@@ -3232,6 +3355,15 @@ export class TextChannel extends BaseGuildTextChannel {
public type: ChannelType.GuildText;
}
+export interface TextDisplayComponentData extends BaseComponentData {
+ content: string;
+}
+
+export class TextDisplayComponent extends Component {
+ private constructor(data: APITextDisplayComponent);
+ public readonly content: string;
+}
+
export type ForumThreadChannel = PublicThreadChannel;
export type TextThreadChannel = PublicThreadChannel | PrivateThreadChannel;
export type AnyThreadChannel = TextThreadChannel | ForumThreadChannel;
@@ -3327,6 +3459,19 @@ export class ThreadMemberFlagsBitField extends BitField
public static resolve(bit?: BitFieldResolvable): number;
}
+export interface ThumbnailComponentData extends BaseComponentData {
+ media: UnfurledMediaItemData;
+ description?: string;
+ spoiler?: boolean;
+}
+
+export class ThumbnailComponent extends Component {
+ private constructor(data: APIThumbnailComponent);
+ public readonly media: UnfurledMediaItem;
+ public get description(): string | null;
+ public get spoiler(): boolean;
+}
+
export class Typing extends Base {
private constructor(channel: TextBasedChannel, user: PartialUser, data?: RawTypingData);
public channel: TextBasedChannel;
@@ -3346,6 +3491,16 @@ export interface AvatarDecorationData {
skuId: Snowflake;
}
+export interface UnfurledMediaItemData {
+ url: string;
+}
+
+export class UnfurledMediaItem {
+ private constructor(data: APIUnfurledMediaItem);
+ public readonly data: APIUnfurledMediaItem;
+ public get url(): string;
+}
+
// tslint:disable-next-line no-empty-interface
export interface User extends PartialTextBasedChannelFields {}
export class User extends Base {
@@ -3478,7 +3633,9 @@ export function createChannel(
export type ComponentData =
| MessageActionRowComponentData
| ModalActionRowComponentData
- | ActionRowData;
+ | ComponentInContainerData
+ | ContainerComponentData
+ | ThumbnailComponentData;
export class VoiceChannel extends BaseGuildVoiceChannel {
public get speakable(): boolean;
@@ -6111,8 +6268,11 @@ export interface InteractionReplyOptions extends BaseMessageOptionsWithPoll {
withResponse?: boolean;
flags?:
| BitFieldResolvable<
- Extract,
- MessageFlags.Ephemeral | MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications
+ Extract,
+ | MessageFlags.Ephemeral
+ | MessageFlags.SuppressEmbeds
+ | MessageFlags.SuppressNotifications
+ | MessageFlags.IsComponentsV2
>
| undefined;
}
@@ -6273,9 +6433,10 @@ export interface BaseMessageOptions {
| AttachmentPayload
)[];
components?: readonly (
- | JSONEncodable>
+ | JSONEncodable
+ | TopLevelComponentData
| ActionRowData
- | APIActionRowComponent
+ | APIMessageTopLevelComponent
)[];
}
@@ -6290,8 +6451,8 @@ export interface BaseMessageCreateOptions extends BaseMessageOptionsWithPoll {
stickers?: readonly StickerResolvable[];
flags?:
| BitFieldResolvable<
- Extract,
- MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications
+ Extract,
+ MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications | MessageFlags.IsComponentsV2
>
| undefined;
}
diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts
index 4e41a80fd7ee..9484c1b5676c 100644
--- a/packages/discord.js/typings/index.test-d.ts
+++ b/packages/discord.js/typings/index.test-d.ts
@@ -29,6 +29,7 @@ import {
GuildScheduledEventRecurrenceRuleMonth,
GuildScheduledEventRecurrenceRuleWeekday,
APIButtonComponentWithCustomId,
+ MessageFlags,
} from 'discord-api-types/v10';
import {
ApplicationCommand,
@@ -214,6 +215,15 @@ import {
PrimaryButtonBuilder,
resolveColor,
createComponentBuilder,
+ SectionComponentData,
+ TextDisplayComponentData,
+ ThumbnailComponentData,
+ UnfurledMediaItemData,
+ MediaGalleryComponentData,
+ MediaGalleryItemData,
+ SeparatorComponentData,
+ FileComponentData,
+ ContainerComponentData,
} from './index.js';
import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd';
import type { ContextMenuCommandBuilder, ChatInputCommandBuilder } from '@discordjs/builders';
@@ -654,6 +664,57 @@ client.on('messageCreate', async message => {
components: [row, rawButtonsRow, buttonsRow, rawStringSelectMenuRow, stringSelectRow],
embeds: [embed, embedData],
});
+
+ const rawTextDisplay: TextDisplayComponentData = {
+ type: ComponentType.TextDisplay,
+ content: 'test',
+ };
+
+ const rawMedia: UnfurledMediaItemData = { url: 'https://discord.js.org' };
+
+ const rawThumbnail: ThumbnailComponentData = {
+ type: ComponentType.Thumbnail,
+ media: rawMedia,
+ spoiler: true,
+ description: 'test',
+ };
+
+ const rawSection: SectionComponentData = {
+ type: ComponentType.Section,
+ components: [rawTextDisplay],
+ accessory: rawThumbnail,
+ };
+
+ const rawMediaGalleryItem: MediaGalleryItemData = {
+ media: rawMedia,
+ description: 'test',
+ spoiler: false,
+ };
+
+ const rawMediaGallery: MediaGalleryComponentData = {
+ type: ComponentType.MediaGallery,
+ items: [rawMediaGalleryItem, rawMediaGalleryItem, rawMediaGalleryItem],
+ };
+
+ const rawSeparator: SeparatorComponentData = {
+ type: ComponentType.Separator,
+ spacing: 1,
+ dividier: false,
+ };
+
+ const rawFile: FileComponentData = {
+ type: ComponentType.File,
+ file: rawMedia,
+ };
+
+ const rawContainer: ContainerComponentData = {
+ type: ComponentType.Container,
+ components: [rawSection, rawSeparator, rawMediaGallery, rawFile],
+ accentColor: 0xff00ff,
+ spoiler: true,
+ };
+
+ channel.send({ flags: MessageFlags.IsComponentsV2, components: [rawContainer] });
});
client.on('messageDelete', ({ client }) => expectType>(client));
From f2671ac70d0fb0f630be15b8a59b4ec5c9576441 Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Fri, 25 Apr 2025 21:59:58 +0200
Subject: [PATCH 02/12] fix: tests
---
packages/discord.js/typings/index.d.ts | 1 +
packages/discord.js/typings/index.test-d.ts | 8 +++++---
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts
index e29cb873e759..518552ae5db7 100644
--- a/packages/discord.js/typings/index.d.ts
+++ b/packages/discord.js/typings/index.d.ts
@@ -6433,6 +6433,7 @@ export interface BaseMessageOptions {
| AttachmentPayload
)[];
components?: readonly (
+ | JSONEncodable>
| JSONEncodable
| TopLevelComponentData
| ActionRowData
diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts
index 9484c1b5676c..7bd5402e7c8b 100644
--- a/packages/discord.js/typings/index.test-d.ts
+++ b/packages/discord.js/typings/index.test-d.ts
@@ -30,6 +30,7 @@ import {
GuildScheduledEventRecurrenceRuleWeekday,
APIButtonComponentWithCustomId,
MessageFlags,
+ APITextInputComponent,
} from 'discord-api-types/v10';
import {
ApplicationCommand,
@@ -363,8 +364,6 @@ client.on('interactionCreate', async interaction => {
// @ts-expect-error
interaction.reply({ content: 'Hi!', components: [[button]] });
- void new ActionRowBuilder({});
-
// @ts-expect-error
await interaction.reply({ content: 'Hi!', components: [button] });
@@ -2591,8 +2590,11 @@ new PrimaryButtonBuilder(buttonData);
declare const buttonComp: ButtonComponent;
createComponentBuilder(buttonComp.toJSON());
+declare const textInputData: APITextInputComponent;
+new TextInputBuilder(textInputData);
+
declare const textInputComp: TextInputComponent;
-new TextInputBuilder(textInputComp);
+new TextInputBuilder(textInputComp.toJSON());
declare const embedData: APIEmbed;
new EmbedBuilder(embedData);
From b1db31779278f7eb07cfb3bdebedde3e6b9df95c Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Fri, 25 Apr 2025 22:11:44 +0200
Subject: [PATCH 03/12] fix: merge
---
packages/discord.js/typings/index.d.ts | 5 -----
1 file changed, 5 deletions(-)
diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts
index 231503d77664..bc3ec27eff67 100644
--- a/packages/discord.js/typings/index.d.ts
+++ b/packages/discord.js/typings/index.d.ts
@@ -3645,11 +3645,6 @@ export interface SendSoundboardSoundOptions {
guildId?: Snowflake;
}
-export interface SendSoundboardSoundOptions {
- soundId: Snowflake;
- guildId?: Snowflake;
-}
-
export class VoiceChannel extends BaseGuildVoiceChannel {
public get speakable(): boolean;
public type: ChannelType.GuildVoice;
From ac640d6cb2f3276d53abbeef85149e01be640c7b Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Fri, 25 Apr 2025 22:25:33 +0200
Subject: [PATCH 04/12] fix: lint
---
.../src/structures/ContainerComponent.js | 86 +++++++++----------
.../src/structures/FileComponent.js | 48 +++++------
.../src/structures/MediaGalleryComponent.js | 30 +++----
.../src/structures/MediaGalleryItem.js | 66 +++++++-------
packages/discord.js/src/structures/Message.js | 23 +++--
.../structures/MessageComponentInteraction.js | 22 ++---
.../src/structures/SectionComponent.js | 50 +++++------
.../src/structures/SeparatorComponent.js | 32 +++----
.../src/structures/TextDisplayComponent.js | 16 ++--
.../src/structures/ThumbnailComponent.js | 60 ++++++-------
.../src/structures/UnfurledMediaItem.js | 28 +++---
packages/discord.js/src/util/Components.js | 20 ++---
12 files changed, 240 insertions(+), 241 deletions(-)
diff --git a/packages/discord.js/src/structures/ContainerComponent.js b/packages/discord.js/src/structures/ContainerComponent.js
index 3eb412e96c18..11eddddefb39 100644
--- a/packages/discord.js/src/structures/ContainerComponent.js
+++ b/packages/discord.js/src/structures/ContainerComponent.js
@@ -8,53 +8,53 @@ const { createComponent } = require('../util/Components.js');
* @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);
- }
+ constructor({ components, ...data }) {
+ super(data);
/**
- * Whether this container is spoilered
- * @type {boolean}
+ * The components in this container
+ * @type {Component[]}
* @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()) };
- }
+ 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;
diff --git a/packages/discord.js/src/structures/FileComponent.js b/packages/discord.js/src/structures/FileComponent.js
index e63750281da4..63b7b0275d8a 100644
--- a/packages/discord.js/src/structures/FileComponent.js
+++ b/packages/discord.js/src/structures/FileComponent.js
@@ -1,40 +1,40 @@
'use strict';
-const Component = require('./Component.js');
-const UnfurledMediaItem = require('./UnfurledMediaItem.js');
+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);
- }
+ constructor({ file, ...data }) {
+ super(data);
/**
- * Whether this thumbnail is spoilered
- * @type {boolean}
+ * The media associated with this file
+ * @type {UnfurledMediaItem}
* @readonly
*/
- get spoiler() {
- return this.data.spoiler ?? false;
- }
+ this.file = new UnfurledMediaItem(file);
+ }
- /**
- * Returns the API-compatible JSON for this component
- * @returns {APIFileComponent}
- */
- toJSON() {
- return { ...this.data, file: this.file.toJSON() };
- }
+ /**
+ * 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;
diff --git a/packages/discord.js/src/structures/MediaGalleryComponent.js b/packages/discord.js/src/structures/MediaGalleryComponent.js
index 3531e3c163d9..6b797b3ef1e3 100644
--- a/packages/discord.js/src/structures/MediaGalleryComponent.js
+++ b/packages/discord.js/src/structures/MediaGalleryComponent.js
@@ -8,24 +8,24 @@ const { MediaGalleryItem } = require('./MediaGalleryItem.js');
* @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));
- }
+ constructor({ items, ...data }) {
+ super(data);
/**
- * Returns the API-compatible JSON for this component
- * @returns {APIMediaGalleryComponent}
+ * The items in this media gallery
+ * @type {MediaGalleryItem[]}
+ * @readonly
*/
- toJSON() {
- return { ...this.data, items: this.items.map(item => item.toJSON()) };
- }
+ 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;
diff --git a/packages/discord.js/src/structures/MediaGalleryItem.js b/packages/discord.js/src/structures/MediaGalleryItem.js
index 7b608dcdace6..7b3acc81f626 100644
--- a/packages/discord.js/src/structures/MediaGalleryItem.js
+++ b/packages/discord.js/src/structures/MediaGalleryItem.js
@@ -6,46 +6,46 @@ 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);
- }
-
+ constructor({ media, ...data }) {
/**
- * The description of this media gallery item
- * @type {?string}
- * @readonly
+ * The API data associated with this component
+ * @type {APIMediaGalleryItem}
*/
- get description() {
- return this.data.description ?? null;
- }
+ this.data = data;
/**
- * Whether this media gallery item is spoilered
- * @type {boolean}
+ * The media associated with this media gallery item
+ * @type {UnfurledMediaItem}
* @readonly
*/
- get spoiler() {
- return this.data.spoiler ?? false;
- }
+ this.media = new UnfurledMediaItem(media);
+ }
- /**
- * Returns the API-compatible JSON for this component
- * @returns {APIMediaGalleryItem}
- */
- toJSON() {
- return { ...this.data, media: this.media.toJSON() };
- }
+ /**
+ * 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;
diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js
index d5438f1c3668..f257842da3cf 100644
--- a/packages/discord.js/src/structures/Message.js
+++ b/packages/discord.js/src/structures/Message.js
@@ -14,7 +14,6 @@ const {
const { Attachment } = require('./Attachment.js');
const { Base } = require('./Base.js');
const { ClientApplication } = require('./ClientApplication.js');
-const { Component } = require('./Component.js');
const { Embed } = require('./Embed.js');
const { InteractionCollector } = require('./InteractionCollector.js');
const { MessageMentions } = require('./MessageMentions.js');
@@ -684,8 +683,8 @@ class Message extends Base {
get editable() {
const precheck = Boolean(
this.author.id === this.client.user.id &&
- (!this.guild || this.channel?.viewable) &&
- this.reference?.type !== MessageReferenceType.Forward,
+ (!this.guild || this.channel?.viewable) &&
+ this.reference?.type !== MessageReferenceType.Forward,
);
// Regardless of permissions thread messages cannot be edited if
@@ -756,9 +755,9 @@ class Message extends Base {
const { channel } = this;
return Boolean(
!this.system &&
- (!this.guild ||
- (channel?.viewable &&
- channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))),
+ (!this.guild ||
+ (channel?.viewable &&
+ channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))),
);
}
@@ -788,12 +787,12 @@ class Message extends Base {
const { channel } = this;
return Boolean(
channel?.type === ChannelType.GuildAnnouncement &&
- !this.flags.has(MessageFlags.Crossposted) &&
- this.reference?.type !== MessageReferenceType.Forward &&
- this.type === MessageType.Default &&
- !this.poll &&
- channel.viewable &&
- channel.permissionsFor(this.client.user)?.has(bitfield, false),
+ !this.flags.has(MessageFlags.Crossposted) &&
+ this.reference?.type !== MessageReferenceType.Forward &&
+ this.type === MessageType.Default &&
+ !this.poll &&
+ channel.viewable &&
+ channel.permissionsFor(this.client.user)?.has(bitfield, false),
);
}
diff --git a/packages/discord.js/src/structures/MessageComponentInteraction.js b/packages/discord.js/src/structures/MessageComponentInteraction.js
index 2089547e4778..edc25bf74eba 100644
--- a/packages/discord.js/src/structures/MessageComponentInteraction.js
+++ b/packages/discord.js/src/structures/MessageComponentInteraction.js
@@ -89,17 +89,17 @@ class MessageComponentInteraction extends BaseInteraction {
// These are here only for documentation purposes - they are implemented by InteractionResponses
/* eslint-disable no-empty-function */
- deferReply() { }
- reply() { }
- fetchReply() { }
- editReply() { }
- deleteReply() { }
- followUp() { }
- deferUpdate() { }
- update() { }
- launchActivity() { }
- showModal() { }
- awaitModalSubmit() { }
+ deferReply() {}
+ reply() {}
+ fetchReply() {}
+ editReply() {}
+ deleteReply() {}
+ followUp() {}
+ deferUpdate() {}
+ update() {}
+ launchActivity() {}
+ showModal() {}
+ awaitModalSubmit() {}
}
InteractionResponses.applyToClass(MessageComponentInteraction);
diff --git a/packages/discord.js/src/structures/SectionComponent.js b/packages/discord.js/src/structures/SectionComponent.js
index 6161332c4dd5..2ab625f4e3e8 100644
--- a/packages/discord.js/src/structures/SectionComponent.js
+++ b/packages/discord.js/src/structures/SectionComponent.js
@@ -8,35 +8,35 @@ const { createComponent } = require('../util/Components.js');
* @extends {Component}
*/
class SectionComponent extends Component {
- constructor({ accessory, components, ...data }) {
- super(data);
+ 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);
- }
+ /**
+ * The components in this section
+ * @type {Component[]}
+ * @readonly
+ */
+ this.components = components.map(component => createComponent(component));
/**
- * Returns the API-compatible JSON for this component
- * @returns {APISectionComponent}
+ * The accessory component of this section
+ * @type {Component}
+ * @readonly
*/
- toJSON() {
- return {
- ...this.data,
- accessory: this.accessory.toJSON(),
- components: this.components.map(component => component.toJSON()),
- };
- }
+ 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;
diff --git a/packages/discord.js/src/structures/SeparatorComponent.js b/packages/discord.js/src/structures/SeparatorComponent.js
index d4861cd0623f..3df1934bb1c6 100644
--- a/packages/discord.js/src/structures/SeparatorComponent.js
+++ b/packages/discord.js/src/structures/SeparatorComponent.js
@@ -8,23 +8,23 @@ const { Component } = require('./Component.js');
* @extends {Component}
*/
class SeparatorComponent extends Component {
- /**
- * The spacing of this separator
- * @type {SeparatorSpacingSize}
- * @readonly
- */
- get spacing() {
- return this.data.spacing ?? SeparatorSpacingSize.Small;
- }
+ /**
+ * 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;
- }
+ /**
+ * Whether this separator is a divider
+ * @type {boolean}
+ * @readonly
+ */
+ get divider() {
+ return this.data.divider ?? true;
+ }
}
exports.SeparatorComponent = SeparatorComponent;
diff --git a/packages/discord.js/src/structures/TextDisplayComponent.js b/packages/discord.js/src/structures/TextDisplayComponent.js
index b31624f27326..5b8fd04badb3 100644
--- a/packages/discord.js/src/structures/TextDisplayComponent.js
+++ b/packages/discord.js/src/structures/TextDisplayComponent.js
@@ -7,14 +7,14 @@ const { Component } = require('./Component.js');
* @extends {Component}
*/
class TextDisplayComponent extends Component {
- /**
- * The content of this text display
- * @type {string}
- * @readonly
- */
- get content() {
- return this.data.content;
- }
+ /**
+ * The content of this text display
+ * @type {string}
+ * @readonly
+ */
+ get content() {
+ return this.data.content;
+ }
}
exports.TextDisplayComponent = TextDisplayComponent;
diff --git a/packages/discord.js/src/structures/ThumbnailComponent.js b/packages/discord.js/src/structures/ThumbnailComponent.js
index 9a8c70369908..7c85e465b69a 100644
--- a/packages/discord.js/src/structures/ThumbnailComponent.js
+++ b/packages/discord.js/src/structures/ThumbnailComponent.js
@@ -8,42 +8,42 @@ const { UnfurledMediaItem } = require('./UnfurledMediaItem.js');
* @extends {Component}
*/
class ThumbnailComponent extends Component {
- constructor({ media, ...data }) {
- super(data);
-
- /**
- * The media associated with this thumbnail
- * @type {UnfurledMediaItem}
- * @readonly
- */
- this.media = new UnfurledMediaItem(media);
- }
+ constructor({ media, ...data }) {
+ super(data);
/**
- * The description of this thumbnail
- * @type {?string}
+ * The media associated with this thumbnail
+ * @type {UnfurledMediaItem}
* @readonly
*/
- get description() {
- return this.data.description ?? null;
- }
+ this.media = new UnfurledMediaItem(media);
+ }
- /**
- * Whether this thumbnail is spoilered
- * @type {boolean}
- * @readonly
- */
- get spoiler() {
- return this.data.spoiler ?? false;
- }
+ /**
+ * The description of this thumbnail
+ * @type {?string}
+ * @readonly
+ */
+ get description() {
+ return this.data.description ?? null;
+ }
- /**
- * Returns the API-compatible JSON for this component
- * @returns {APIThumbnailComponent}
- */
- toJSON() {
- return { ...this.data, media: this.media.toJSON() };
- }
+ /**
+ * Whether this thumbnail is spoilered
+ * @type {boolean}
+ * @readonly
+ */
+ get spoiler() {
+ return this.data.spoiler ?? false;
+ }
+
+ /**
+ * Returns the API-compatible JSON for this component
+ * @returns {APIThumbnailComponent}
+ */
+ toJSON() {
+ return { ...this.data, media: this.media.toJSON() };
+ }
}
exports.ThumbnailComponent = ThumbnailComponent;
diff --git a/packages/discord.js/src/structures/UnfurledMediaItem.js b/packages/discord.js/src/structures/UnfurledMediaItem.js
index 79e0dcf3bc10..156096aca51a 100644
--- a/packages/discord.js/src/structures/UnfurledMediaItem.js
+++ b/packages/discord.js/src/structures/UnfurledMediaItem.js
@@ -4,22 +4,22 @@
* Represents a media item in a component
*/
class UnfurledMediaItem {
- constructor(data) {
- /**
- * The API data associated with this media item
- * @type {APIUnfurledMediaItem}
- */
- this.data = data;
- }
-
+ constructor(data) {
/**
- * The URL of this media gallery item
- * @type {string}
- * @readonly
+ * The API data associated with this media item
+ * @type {APIUnfurledMediaItem}
*/
- get url() {
- return this.data.url;
- }
+ this.data = data;
+ }
+
+ /**
+ * The URL of this media gallery item
+ * @type {string}
+ * @readonly
+ */
+ get url() {
+ return this.data.url;
+ }
}
exports.UnfurledMediaItem = UnfurledMediaItem;
diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js
index 9d010d18df86..4d3512f85af6 100644
--- a/packages/discord.js/src/util/Components.js
+++ b/packages/discord.js/src/util/Components.js
@@ -108,25 +108,25 @@ const { ComponentType } = require('discord-api-types/v10');
/**
* @typedef {ActionRowData|FileComponentData|MediaGalleryComponentData|SectionComponentData|
* SeparatorComponentData|TextDisplayComponentData} ComponentInContainerData
-*/
+ */
/**
-* @typedef {BaseComponentData} ContainerComponentData
-* @property {ComponentInContainerData} components The components in this container
-* @property {?number} [accentColor] The accent color of this container
-* @property {boolean} [spoiler] Whether the container should be spoilered
-*/
+ * @typedef {BaseComponentData} ContainerComponentData
+ * @property {ComponentInContainerData} components The components in this container
+ * @property {?number} [accentColor] The accent color of this container
+ * @property {boolean} [spoiler] Whether the container should be spoilered
+ */
/**
-* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData|
-* ThumbnailComponentData|FileComponentData|MediaGalleryComponentData|SeparatorComponentData|
-* SectionComponentData|TextDisplayComponentData|ContainerComponentData} ComponentData
+ * @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData|
+ * ThumbnailComponentData|FileComponentData|MediaGalleryComponentData|SeparatorComponentData|
+ * SectionComponentData|TextDisplayComponentData|ContainerComponentData} ComponentData
*/
/**
* @typedef {ActionRow|ContainerComponent|FileComponent|MediaGalleryComponent|
* SectionComponent|SeparatorComponent|TextDisplayComponent} MessageTopLevelComponent
-*/
+ */
/**
* Transforms API data into a component
From c321b8f7aabd257c69c80032f46d03cffd2b342f Mon Sep 17 00:00:00 2001
From: Vlad Frangu
Date: Sat, 26 Apr 2025 02:36:35 +0300
Subject: [PATCH 05/12] Update packages/discord.js/src/util/Components.js
---
packages/discord.js/src/util/Components.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js
index 4d3512f85af6..891e0497d13c 100644
--- a/packages/discord.js/src/util/Components.js
+++ b/packages/discord.js/src/util/Components.js
@@ -189,7 +189,7 @@ function findComponentByCustomId(components, customId) {
case ComponentType.ActionRow:
return component.components;
case ComponentType.Section:
- return [component.accessory];
+ return [...component.components, component.accessory];
default:
return [component];
}
From 15290b0507d5c89551f5075d1cb85c901ac15b69 Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Sat, 26 Apr 2025 12:59:45 +0200
Subject: [PATCH 06/12] fix: forward-port fixes from v14
---
packages/discord.js/src/structures/MessagePayload.js | 3 ++-
packages/discord.js/typings/index.d.ts | 7 ++++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js
index bf07b026b743..f4784a86f269 100644
--- a/packages/discord.js/src/structures/MessagePayload.js
+++ b/packages/discord.js/src/structures/MessagePayload.js
@@ -217,7 +217,8 @@ class MessagePayload {
components,
username,
avatar_url: avatarURL,
- allowed_mentions: content === undefined && message_reference === undefined ? undefined : allowedMentions,
+ allowed_mentions:
+ this.isMessage() && this.target.author.id !== this.target.client.user.id ? undefined : allowedMentions,
flags,
message_reference,
attachments: this.options.attachments,
diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts
index bc3ec27eff67..befe6479a5c8 100644
--- a/packages/discord.js/typings/index.d.ts
+++ b/packages/discord.js/typings/index.d.ts
@@ -6557,7 +6557,12 @@ export interface MessageEditAttachmentData {
export interface MessageEditOptions extends Omit {
content?: string | null;
attachments?: readonly (Attachment | MessageEditAttachmentData)[];
- flags?: BitFieldResolvable, MessageFlags.SuppressEmbeds> | undefined;
+ flags?:
+ | BitFieldResolvable<
+ Extract,
+ MessageFlags.SuppressEmbeds | MessageFlags.IsComponentsV2
+ >
+ | undefined;
}
export type MessageReactionResolvable = MessageReaction | Snowflake | string;
From 2fb6e00f0d9b54676c28148f91233620129f18c1 Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Sat, 26 Apr 2025 17:39:31 +0200
Subject: [PATCH 07/12] fix: getter
---
packages/discord.js/src/structures/MessagePayload.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js
index f4784a86f269..9c8fafe0e856 100644
--- a/packages/discord.js/src/structures/MessagePayload.js
+++ b/packages/discord.js/src/structures/MessagePayload.js
@@ -218,7 +218,7 @@ class MessagePayload {
username,
avatar_url: avatarURL,
allowed_mentions:
- this.isMessage() && this.target.author.id !== this.target.client.user.id ? undefined : allowedMentions,
+ this.isMessage && this.target.author.id !== this.target.client.user.id ? undefined : allowedMentions,
flags,
message_reference,
attachments: this.options.attachments,
From 8b916a7d6d4f527a1055159c30bcef14d5de85d4 Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Sat, 26 Apr 2025 19:25:06 +0200
Subject: [PATCH 08/12] fix: missing UnfurledMediaItem#toJSON()
---
packages/discord.js/src/structures/UnfurledMediaItem.js | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/packages/discord.js/src/structures/UnfurledMediaItem.js b/packages/discord.js/src/structures/UnfurledMediaItem.js
index 156096aca51a..1f1409c5b5fd 100644
--- a/packages/discord.js/src/structures/UnfurledMediaItem.js
+++ b/packages/discord.js/src/structures/UnfurledMediaItem.js
@@ -20,6 +20,14 @@ class UnfurledMediaItem {
get url() {
return this.data.url;
}
+
+ /**
+ * Returns the API-compatible JSON for this media item
+ * @returns {APIUnfurledMediaItem}
+ */
+ toJSON() {
+ return { ...this.data };
+ }
}
exports.UnfurledMediaItem = UnfurledMediaItem;
From 3ed27b334fee533d50407707a165b6f0bd10a4b2 Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Sun, 27 Apr 2025 18:57:23 +0200
Subject: [PATCH 09/12] fix: find interactive component in container
---
packages/discord.js/src/util/Components.js | 31 +++++++++++++++-------
1 file changed, 21 insertions(+), 10 deletions(-)
diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js
index 891e0497d13c..380bce5750bb 100644
--- a/packages/discord.js/src/util/Components.js
+++ b/packages/discord.js/src/util/Components.js
@@ -175,25 +175,36 @@ function createComponent(data) {
}
}
+/**
+ * Extracts all interactive components from the component tree
+ * @param {Component|APIMessageComponent} component The component to find all interactive components in
+ * @returns {Array}
+ * @ignore
+ */
+function extractInteractiveComponents(component) {
+ switch (component.type) {
+ case ComponentType.ActionRow:
+ return component.components;
+ case ComponentType.Section:
+ return [...component.components, component.accessory];
+ case ComponentType.Container:
+ return component.components.flatMap(extractInteractiveComponents);
+ default:
+ return [component];
+ }
+}
+
/**
* Finds a component by customId in nested components
* @param {Array} components The components to search in
* @param {string} customId The customId to search for
* @returns {Component|APIMessageComponent}
+ * @ignore
*/
function findComponentByCustomId(components, customId) {
return (
components
- .flatMap(component => {
- switch (component.type) {
- case ComponentType.ActionRow:
- return component.components;
- case ComponentType.Section:
- return [...component.components, component.accessory];
- default:
- return [component];
- }
- })
+ .flatMap(extractInteractiveComponents)
.find(component => (component.customId ?? component.custom_id) === customId) ?? null
);
}
From 5cfa9b03b228be5ca1c1ed454251243bbe404105 Mon Sep 17 00:00:00 2001
From: Jiralite <33201955+Jiralite@users.noreply.github.com>
Date: Tue, 29 Apr 2025 18:26:27 +0100
Subject: [PATCH 10/12] docs(APIMediaGalleryItem): Correct tag
---
packages/discord.js/src/util/APITypes.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js
index 60025d98a772..3772ebb1d1b8 100644
--- a/packages/discord.js/src/util/APITypes.js
+++ b/packages/discord.js/src/util/APITypes.js
@@ -152,7 +152,7 @@
/**
* @external APIMediaGalleryItem
- * @se {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMediaGalleryItem}
+ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMediaGalleryItem}
*/
/**
From bcd07c15ae92d440045826157452315b729474b6 Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Fri, 2 May 2025 21:32:47 +0200
Subject: [PATCH 11/12] fix: forward port
---
packages/discord.js/.lintstagedrc.json | 2 +-
.../src/structures/MessagePayload.js | 4 +-
packages/discord.js/test/messages.js | 156 ++++++++++++++++++
packages/discord.js/typings/index.d.ts | 2 +-
packages/discord.js/typings/index.test-d.ts | 2 +-
5 files changed, 162 insertions(+), 4 deletions(-)
create mode 100644 packages/discord.js/test/messages.js
diff --git a/packages/discord.js/.lintstagedrc.json b/packages/discord.js/.lintstagedrc.json
index e4e385774d7f..88488d113874 100644
--- a/packages/discord.js/.lintstagedrc.json
+++ b/packages/discord.js/.lintstagedrc.json
@@ -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"
}
diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js
index 9c8fafe0e856..eb542ddcf29e 100644
--- a/packages/discord.js/src/structures/MessagePayload.js
+++ b/packages/discord.js/src/structures/MessagePayload.js
@@ -218,7 +218,9 @@ class MessagePayload {
username,
avatar_url: avatarURL,
allowed_mentions:
- this.isMessage && this.target.author.id !== this.target.client.user.id ? undefined : allowedMentions,
+ this.isMessage && message_reference === undefined && this.target.author.id !== this.target.client.user.id
+ ? undefined
+ : allowedMentions,
flags,
message_reference,
attachments: this.options.attachments,
diff --git a/packages/discord.js/test/messages.js b/packages/discord.js/test/messages.js
new file mode 100644
index 000000000000..4b8b169f11ee
--- /dev/null
+++ b/packages/discord.js/test/messages.js
@@ -0,0 +1,156 @@
+'use strict';
+
+const { Buffer } = require('node:buffer');
+const { createReadStream } = require('node:fs');
+const { readFile } = require('node:fs/promises');
+const path = require('node:path');
+const process = require('node:process');
+const { setTimeout: sleep } = require('node:timers/promises');
+const { fetch } = require('undici');
+const {
+ Client,
+ GatewayIntentBits,
+ AttachmentBuilder,
+ EmbedBuilder,
+ MessageFlags,
+ ComponentType,
+} = require('../src/index.js');
+
+const client = new Client({
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent],
+});
+
+const buffer = l =>
+ fetch(l)
+ .then(res => res.arrayBuffer())
+ .then(Buffer.from);
+
+const linkA = 'https://discord.js.org/static/logo.svg';
+const fileA = path.join(__dirname, 'blobReach.png');
+
+const embed = () => new EmbedBuilder();
+const attach = (attachment, name) => new AttachmentBuilder(attachment, { name });
+
+const tests = [
+ m => m.channel.send('x'),
+
+ m => m.channel.send({ content: 'x', embeds: [{ description: 'a' }] }),
+ m => m.channel.send({ embeds: [{ description: 'a' }] }),
+ m => m.channel.send({ files: [{ attachment: fileA }] }),
+ m =>
+ m.channel.send({
+ embeds: [{ description: 'a' }],
+ files: [{ attachment: fileA, name: 'xyz.png' }],
+ }),
+
+ m => m.channel.send({ content: 'x', embeds: [embed().setDescription('a')] }),
+ m => m.channel.send({ embeds: [embed().setDescription('a')] }),
+ m => m.channel.send({ embeds: [embed().setDescription('a'), embed().setDescription('b')] }),
+
+ m => m.channel.send({ content: 'x', files: [attach(fileA)] }),
+ m => m.channel.send({ files: [fileA] }),
+ m => m.channel.send({ files: [attach(fileA)] }),
+ async m => m.channel.send({ files: [await buffer(linkA)] }),
+ async m => m.channel.send({ files: [{ attachment: await buffer(linkA) }] }),
+ m => m.channel.send({ files: [attach(fileA), attach(fileA)] }),
+
+ m => m.channel.send({ embeds: [{ description: 'a' }] }).then(m2 => m2.edit('x')),
+ m => m.channel.send({ embeds: [embed().setDescription('a')] }).then(m2 => m2.edit('x')),
+
+ m => m.channel.send('x').then(m2 => m2.edit({ embeds: [{ description: 'a' }] })),
+ m => m.channel.send('x').then(m2 => m2.edit({ embeds: [embed().setDescription('a')] })),
+
+ m => m.channel.send({ embeds: [{ description: 'a' }] }).then(m2 => m2.edit({ content: 'x', embeds: [] })),
+ m => m.channel.send({ embeds: [embed().setDescription('a')] }).then(m2 => m2.edit({ content: 'x', embeds: [] })),
+
+ m => m.channel.send({ content: 'x', embeds: [embed().setDescription('a')], files: [attach(fileA)] }),
+ m => m.channel.send({ content: 'x', files: [attach(fileA), attach(fileA)] }),
+
+ m => m.channel.send({ embeds: [embed().setDescription('a')], files: [attach(fileA)] }),
+ m =>
+ m.channel.send({
+ embeds: [embed().setImage('attachment://two.png')],
+ files: [attach(fileA, 'two.png')],
+ }),
+ m => m.channel.send({ content: 'x', files: [attach(fileA)] }),
+ m => m.channel.send({ files: [fileA] }),
+ m => m.channel.send({ files: [attach(fileA)] }),
+ async m => m.channel.send({ files: [await readFile(fileA)] }),
+
+ m => m.channel.send({ content: 'x', files: [attach(createReadStream(fileA))] }),
+ m => m.channel.send({ files: [createReadStream(fileA)] }),
+ m => m.channel.send({ files: [{ attachment: createReadStream(fileA) }] }),
+
+ m => m.reply({ content: 'x', allowedMentions: { repliedUser: false } }),
+ m => m.reply({ content: 'x', allowedMentions: { repliedUser: true } }),
+ m => m.reply({ content: 'x' }),
+ m => m.reply({ content: `${m.author}`, allowedMentions: { repliedUser: false } }),
+ m => m.reply({ content: `${m.author}`, allowedMentions: { parse: ['users'], repliedUser: false } }),
+ m => m.reply({ content: `${m.author}`, allowedMentions: { parse: ['users'], repliedUser: true } }),
+ m => m.reply({ content: `${m.author}` }),
+
+ m => m.edit({ flags: MessageFlags.SuppressEmbeds }),
+ m => m.edit({ flags: MessageFlags.SuppressEmbeds, allowedMentions: { repliedUser: false } }),
+
+ m =>
+ m
+ .reply({ content: 'x', allowedMentions: { repliedUser: false } })
+ .then(msg => msg.edit({ content: 'a', allowedMentions: { repliedUser: true } })),
+
+ m =>
+ m.channel.send({
+ components: [{ type: ComponentType.TextDisplay, content: `${m.author}` }],
+ flags: MessageFlags.IsComponentsV2,
+ }),
+ m =>
+ m.channel.send({
+ components: [{ type: ComponentType.TextDisplay, content: `${m.author}` }],
+ flags: MessageFlags.IsComponentsV2,
+ allowedMentions: { parse: ['users'] },
+ }),
+ m =>
+ m.channel.send({
+ components: [{ type: ComponentType.TextDisplay, content: `${m.author}` }],
+ flags: MessageFlags.IsComponentsV2,
+ allowedMentions: { parse: [] },
+ }),
+ m =>
+ m.reply({
+ components: [{ type: ComponentType.TextDisplay, content: `${m.author}` }],
+ flags: MessageFlags.IsComponentsV2,
+ allowedMentions: { parse: [], repliedUser: true },
+ }),
+ m =>
+ m.reply({
+ components: [{ type: ComponentType.TextDisplay, content: `${m.author}` }],
+ flags: MessageFlags.IsComponentsV2,
+ allowedMentions: { parse: [], repliedUser: false },
+ }),
+
+ m => m.channel.send('Done!'),
+];
+
+client.on('messageCreate', async message => {
+ if (message.author.id !== process.env.OWNER) return;
+ const match = message.content.match(/^do (.+)$/);
+ if (match?.[1] === 'it') {
+ /* eslint-disable no-await-in-loop */
+ for (const [i, test] of tests.entries()) {
+ await message.channel.send(`**#${i}**\n\`\`\`js\n${test.toString()}\`\`\``);
+ await test(message).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``));
+ await sleep(1_000);
+ }
+ /* eslint-enable no-await-in-loop */
+ } else if (match) {
+ const n = parseInt(match[1]) || 0;
+ const test = tests.slice(n)[0];
+ const i = tests.indexOf(test);
+ await message.channel.send(`**#${i}**\n\`\`\`js\n${test.toString()}\`\`\``);
+ await test(message).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``));
+ }
+});
+
+client.login();
+
+// eslint-disable-next-line no-console
+process.on('unhandledRejection', console.error);
diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts
index befe6479a5c8..7bf1cac8bbd1 100644
--- a/packages/discord.js/typings/index.d.ts
+++ b/packages/discord.js/typings/index.d.ts
@@ -3004,7 +3004,7 @@ export type SelectMenuType = APISelectMenuComponent['type'];
export interface SeparatorComponentData extends BaseComponentData {
spacing?: SeparatorSpacingSize;
- dividier?: boolean;
+ divider?: boolean;
}
export class SeparatorComponent extends Component {
private constructor(data: APISeparatorComponent);
diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts
index 7bd5402e7c8b..eacfcfdaa429 100644
--- a/packages/discord.js/typings/index.test-d.ts
+++ b/packages/discord.js/typings/index.test-d.ts
@@ -698,7 +698,7 @@ client.on('messageCreate', async message => {
const rawSeparator: SeparatorComponentData = {
type: ComponentType.Separator,
spacing: 1,
- dividier: false,
+ divider: false,
};
const rawFile: FileComponentData = {
From 3d9e6fb2ae995df2c164c3ee2910ea5fd3f516ce Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Sun, 11 May 2025 22:36:45 +0200
Subject: [PATCH 12/12] Apply suggestions from code review
Co-authored-by: Danial Raza
---
packages/discord.js/src/structures/UnfurledMediaItem.js | 2 +-
packages/discord.js/typings/index.d.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/discord.js/src/structures/UnfurledMediaItem.js b/packages/discord.js/src/structures/UnfurledMediaItem.js
index 1f1409c5b5fd..3e525e96640c 100644
--- a/packages/discord.js/src/structures/UnfurledMediaItem.js
+++ b/packages/discord.js/src/structures/UnfurledMediaItem.js
@@ -13,7 +13,7 @@ class UnfurledMediaItem {
}
/**
- * The URL of this media gallery item
+ * The URL of this media item
* @type {string}
* @readonly
*/
diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts
index 865bb9d35de1..0eb807c076dd 100644
--- a/packages/discord.js/typings/index.d.ts
+++ b/packages/discord.js/typings/index.d.ts
@@ -3364,7 +3364,7 @@ export interface TextDisplayComponentData extends BaseComponentData {
export class TextDisplayComponent extends Component {
private constructor(data: APITextDisplayComponent);
- public readonly content: string;
+ public get content(): string;
}
export type ForumThreadChannel = PublicThreadChannel;