mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-16 19:43:29 +01:00
feat: add missing v13 component methods (#7466)
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com> Co-authored-by: Rodry <38259440+ImRodry@users.noreply.github.com> Co-authored-by: Antonio Román <kyradiscord@gmail.com>
This commit is contained in:
@@ -1,6 +1,40 @@
|
|||||||
import { APIActionRowComponent, APIMessageComponent, ButtonStyle, ComponentType } from 'discord-api-types/v9';
|
import { APIActionRowComponent, APIMessageComponent, ButtonStyle, ComponentType } from 'discord-api-types/v9';
|
||||||
import { ActionRow, ButtonComponent, createComponent, SelectMenuComponent, SelectMenuOption } from '../../src';
|
import { ActionRow, ButtonComponent, createComponent, SelectMenuComponent, SelectMenuOption } from '../../src';
|
||||||
|
|
||||||
|
const rowWithButtonData: APIActionRowComponent<APIMessageComponent> = {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Button,
|
||||||
|
label: 'test',
|
||||||
|
custom_id: '123',
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowWithSelectMenuData: APIActionRowComponent<APIMessageComponent> = {
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.SelectMenu,
|
||||||
|
custom_id: '1234',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'one',
|
||||||
|
value: 'one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'two',
|
||||||
|
value: 'two',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
max_values: 10,
|
||||||
|
min_values: 12,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
describe('Action Row Components', () => {
|
describe('Action Row Components', () => {
|
||||||
describe('Assertion Tests', () => {
|
describe('Assertion Tests', () => {
|
||||||
test('GIVEN valid components THEN do not throw', () => {
|
test('GIVEN valid components THEN do not throw', () => {
|
||||||
@@ -45,40 +79,6 @@ describe('Action Row Components', () => {
|
|||||||
expect(() => createComponent({ type: 42, components: [] })).toThrowError();
|
expect(() => createComponent({ type: 42, components: [] })).toThrowError();
|
||||||
});
|
});
|
||||||
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
test('GIVEN valid builder options THEN valid JSON output is given', () => {
|
||||||
const rowWithButtonData: APIActionRowComponent<APIMessageComponent> = {
|
|
||||||
type: ComponentType.ActionRow,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: ComponentType.Button,
|
|
||||||
label: 'test',
|
|
||||||
custom_id: '123',
|
|
||||||
style: ButtonStyle.Primary,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const rowWithSelectMenuData: APIActionRowComponent<APIMessageComponent> = {
|
|
||||||
type: ComponentType.ActionRow,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: ComponentType.SelectMenu,
|
|
||||||
custom_id: '1234',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: 'one',
|
|
||||||
value: 'one',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'two',
|
|
||||||
value: 'two',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
max_values: 10,
|
|
||||||
min_values: 12,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const button = new ButtonComponent().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
const button = new ButtonComponent().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
||||||
const selectMenu = new SelectMenuComponent()
|
const selectMenu = new SelectMenuComponent()
|
||||||
.setCustomId('1234')
|
.setCustomId('1234')
|
||||||
@@ -92,5 +92,9 @@ describe('Action Row Components', () => {
|
|||||||
expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData);
|
expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData);
|
||||||
expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
|
expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
|
||||||
});
|
});
|
||||||
|
test('Given JSON data THEN builder is equal to it and itself', () => {
|
||||||
|
expect(new ActionRow(rowWithSelectMenuData).equals(rowWithSelectMenuData)).toBeTruthy();
|
||||||
|
expect(new ActionRow(rowWithButtonData).equals(new ActionRow(rowWithButtonData))).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -142,5 +142,17 @@ describe('Button Components', () => {
|
|||||||
|
|
||||||
expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url));
|
expect(buttonComponent().setLabel(linkData.label).setDisabled(true).setURL(linkData.url));
|
||||||
});
|
});
|
||||||
|
test('Given JSON data THEN builder is equal to it and itself', () => {
|
||||||
|
const buttonData: APIButtonComponentWithCustomId = {
|
||||||
|
type: ComponentType.Button,
|
||||||
|
custom_id: 'test',
|
||||||
|
label: 'test',
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(new ButtonComponent(buttonData).equals(buttonData)).toBeTruthy();
|
||||||
|
expect(new ButtonComponent(buttonData).equals(new ButtonComponent(buttonData))).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,29 @@ const selectMenuOption = () => new SelectMenuOption();
|
|||||||
const longStr =
|
const longStr =
|
||||||
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
|
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
|
||||||
|
|
||||||
describe('Button Components', () => {
|
const selectMenuOptionData: APISelectMenuOption = {
|
||||||
|
label: 'test',
|
||||||
|
value: 'test',
|
||||||
|
emoji: { name: 'test' },
|
||||||
|
default: true,
|
||||||
|
description: 'test',
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectMenuDataWithoutOptions = {
|
||||||
|
type: ComponentType.SelectMenu,
|
||||||
|
custom_id: 'test',
|
||||||
|
max_values: 10,
|
||||||
|
min_values: 3,
|
||||||
|
disabled: true,
|
||||||
|
placeholder: 'test',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const selectMenuData: APISelectMenuComponent = {
|
||||||
|
...selectMenuDataWithoutOptions,
|
||||||
|
options: [selectMenuOptionData],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Select Menu Components', () => {
|
||||||
describe('Assertion Tests', () => {
|
describe('Assertion Tests', () => {
|
||||||
test('GIVEN valid inputs THEN Select Menu does not throw', () => {
|
test('GIVEN valid inputs THEN Select Menu does not throw', () => {
|
||||||
expect(() => selectMenu().setCustomId('foo')).not.toThrowError();
|
expect(() => selectMenu().setCustomId('foo')).not.toThrowError();
|
||||||
@@ -24,6 +46,7 @@ describe('Button Components', () => {
|
|||||||
.setDescription('description');
|
.setDescription('description');
|
||||||
expect(() => selectMenu().addOptions(option)).not.toThrowError();
|
expect(() => selectMenu().addOptions(option)).not.toThrowError();
|
||||||
expect(() => selectMenu().setOptions([option])).not.toThrowError();
|
expect(() => selectMenu().setOptions([option])).not.toThrowError();
|
||||||
|
expect(() => selectMenu().setOptions([{ label: 'test', value: 'test' }])).not.toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN invalid inputs THEN Select Menu does throw', () => {
|
test('GIVEN invalid inputs THEN Select Menu does throw', () => {
|
||||||
@@ -47,28 +70,6 @@ describe('Button Components', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
|
test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
|
||||||
const selectMenuOptionData: APISelectMenuOption = {
|
|
||||||
label: 'test',
|
|
||||||
value: 'test',
|
|
||||||
emoji: { name: 'test' },
|
|
||||||
default: true,
|
|
||||||
description: 'test',
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectMenuDataWithoutOptions = {
|
|
||||||
type: ComponentType.SelectMenu,
|
|
||||||
custom_id: 'test',
|
|
||||||
max_values: 10,
|
|
||||||
min_values: 3,
|
|
||||||
disabled: true,
|
|
||||||
placeholder: 'test',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const selectMenuData: APISelectMenuComponent = {
|
|
||||||
...selectMenuDataWithoutOptions,
|
|
||||||
options: [selectMenuOptionData],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
new SelectMenuComponent(selectMenuDataWithoutOptions)
|
new SelectMenuComponent(selectMenuDataWithoutOptions)
|
||||||
.addOptions(new SelectMenuOption(selectMenuOptionData))
|
.addOptions(new SelectMenuOption(selectMenuOptionData))
|
||||||
@@ -76,5 +77,10 @@ describe('Button Components', () => {
|
|||||||
).toEqual(selectMenuData);
|
).toEqual(selectMenuData);
|
||||||
expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
|
expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Given JSON data THEN builder is equal to it and itself', () => {
|
||||||
|
expect(new SelectMenuComponent(selectMenuData).equals(selectMenuData)).toBeTruthy();
|
||||||
|
expect(new SelectMenuComponent(selectMenuData).equals(new SelectMenuComponent(selectMenuData))).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -115,10 +115,8 @@ describe('Embed', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
|
||||||
const embed = new Embed();
|
expect(new Embed().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 });
|
||||||
embed.setColor(0xff0000);
|
expect(new Embed().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 });
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
|
||||||
@@ -133,6 +131,9 @@ describe('Embed', () => {
|
|||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
expect(() => embed.setColor('RED')).toThrowError();
|
expect(() => embed.setColor('RED')).toThrowError();
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(() => embed.setColor([42, 36])).toThrowError();
|
||||||
|
expect(() => embed.setColor([42, 36, 1000])).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sindresorhus/is": "^4.4.0",
|
"@sindresorhus/is": "^4.4.0",
|
||||||
"discord-api-types": "^0.27.0",
|
"discord-api-types": "^0.27.0",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
"ts-mixer": "^6.0.0",
|
"ts-mixer": "^6.0.0",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"zod": "^3.11.6"
|
"zod": "^3.11.6"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { type APIActionRowComponent, ComponentType, APIMessageComponent } from '
|
|||||||
import type { ButtonComponent, SelectMenuComponent } from '..';
|
import type { ButtonComponent, SelectMenuComponent } from '..';
|
||||||
import { Component } from './Component';
|
import { Component } from './Component';
|
||||||
import { createComponent } from './Components';
|
import { createComponent } from './Components';
|
||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
|
||||||
export type MessageComponent = ActionRowComponent | ActionRow;
|
export type MessageComponent = ActionRowComponent | ActionRow;
|
||||||
|
|
||||||
@@ -46,4 +47,14 @@ export class ActionRow<T extends ActionRowComponent = ActionRowComponent> extend
|
|||||||
components: this.components.map((component) => component.toJSON()),
|
components: this.components.map((component) => component.toJSON()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public equals(other: APIActionRowComponent<APIMessageComponent> | ActionRow) {
|
||||||
|
if (other instanceof ActionRow) {
|
||||||
|
return isEqual(other.data, this.data) && isEqual(other.components, this.components);
|
||||||
|
}
|
||||||
|
return isEqual(other, {
|
||||||
|
...this.data,
|
||||||
|
components: this.components.map((component) => component.toJSON()),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { JSONEncodable } from '../util/jsonEncodable';
|
import type { JSONEncodable } from '../util/jsonEncodable';
|
||||||
import type { APIBaseComponent, APIMessageComponent, ComponentType } from 'discord-api-types/v9';
|
import type {
|
||||||
|
APIActionRowComponentTypes,
|
||||||
|
APIBaseComponent,
|
||||||
|
APIMessageComponent,
|
||||||
|
ComponentType,
|
||||||
|
} from 'discord-api-types/v9';
|
||||||
|
import type { Equatable } from '../util/equatable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a discord component
|
* Represents a discord component
|
||||||
@@ -8,18 +14,17 @@ export abstract class Component<
|
|||||||
DataType extends Partial<APIBaseComponent<ComponentType>> & {
|
DataType extends Partial<APIBaseComponent<ComponentType>> & {
|
||||||
type: ComponentType;
|
type: ComponentType;
|
||||||
} = APIBaseComponent<ComponentType>,
|
} = APIBaseComponent<ComponentType>,
|
||||||
> implements JSONEncodable<APIMessageComponent>
|
> implements JSONEncodable<APIMessageComponent>, Equatable<Component | APIActionRowComponentTypes>
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The API data associated with this component
|
* The API data associated with this component
|
||||||
*/
|
*/
|
||||||
protected readonly data: DataType;
|
protected readonly data: DataType;
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts this component to an API-compatible JSON object
|
|
||||||
*/
|
|
||||||
public abstract toJSON(): APIMessageComponent;
|
public abstract toJSON(): APIMessageComponent;
|
||||||
|
|
||||||
|
public abstract equals(other: Component | APIActionRowComponentTypes): boolean;
|
||||||
|
|
||||||
public constructor(data: DataType) {
|
public constructor(data: DataType) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
type APIButtonComponentWithCustomId,
|
type APIButtonComponentWithCustomId,
|
||||||
} from 'discord-api-types/v9';
|
} from 'discord-api-types/v9';
|
||||||
import { Component } from '../Component';
|
import { Component } from '../Component';
|
||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a non-validated button component
|
* Represents a non-validated button component
|
||||||
@@ -118,4 +119,11 @@ export class UnsafeButtonComponent extends Component<Partial<APIButtonComponent>
|
|||||||
...this.data,
|
...this.data,
|
||||||
} as APIButtonComponent;
|
} as APIButtonComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public equals(other: APIButtonComponent | UnsafeButtonComponent) {
|
||||||
|
if (other instanceof UnsafeButtonComponent) {
|
||||||
|
return isEqual(other.data, this.data);
|
||||||
|
}
|
||||||
|
return isEqual(other, this.data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9';
|
import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9';
|
||||||
import { Component } from '../Component';
|
import { Component } from '../Component';
|
||||||
import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption';
|
import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption';
|
||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a non-validated select menu component
|
* Represents a non-validated select menu component
|
||||||
@@ -101,8 +102,12 @@ export class UnsafeSelectMenuComponent extends Component<
|
|||||||
* @param options The options to add to this select menu
|
* @param options The options to add to this select menu
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public addOptions(...options: UnsafeSelectMenuOption[]) {
|
public addOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) {
|
||||||
this.options.push(...options);
|
this.options.push(
|
||||||
|
...options.map((option) =>
|
||||||
|
option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option),
|
||||||
|
),
|
||||||
|
);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +115,14 @@ export class UnsafeSelectMenuComponent extends Component<
|
|||||||
* Sets the options on this select menu
|
* Sets the options on this select menu
|
||||||
* @param options The options to set on this select menu
|
* @param options The options to set on this select menu
|
||||||
*/
|
*/
|
||||||
public setOptions(...options: UnsafeSelectMenuOption[]) {
|
public setOptions(...options: (UnsafeSelectMenuOption | APISelectMenuOption)[]) {
|
||||||
this.options.splice(0, this.options.length, ...options);
|
this.options.splice(
|
||||||
|
0,
|
||||||
|
this.options.length,
|
||||||
|
...options.map((option) =>
|
||||||
|
option instanceof UnsafeSelectMenuOption ? option : new UnsafeSelectMenuOption(option),
|
||||||
|
),
|
||||||
|
);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,4 +133,14 @@ export class UnsafeSelectMenuComponent extends Component<
|
|||||||
options: this.options.map((o) => o.toJSON()),
|
options: this.options.map((o) => o.toJSON()),
|
||||||
} as APISelectMenuComponent;
|
} as APISelectMenuComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public equals(other: APISelectMenuComponent | UnsafeSelectMenuComponent): boolean {
|
||||||
|
if (other instanceof UnsafeSelectMenuComponent) {
|
||||||
|
return isEqual(other.data, this.data) && isEqual(other.options, this.options);
|
||||||
|
}
|
||||||
|
return isEqual(other, {
|
||||||
|
...this.data,
|
||||||
|
options: this.options.map((o) => o.toJSON()),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,14 @@ export const authorNamePredicate = fieldNamePredicate.nullable();
|
|||||||
|
|
||||||
export const urlPredicate = z.string().url().nullish();
|
export const urlPredicate = z.string().url().nullish();
|
||||||
|
|
||||||
export const colorPredicate = z.number().gte(0).lte(0xffffff).nullable();
|
export const RGBPredicate = z.number().int().gte(0).lte(255);
|
||||||
|
export const colorPredicate = z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.gte(0)
|
||||||
|
.lte(0xffffff)
|
||||||
|
.nullable()
|
||||||
|
.or(z.tuple([RGBPredicate, RGBPredicate, RGBPredicate]));
|
||||||
|
|
||||||
export const descriptionPredicate = z.string().min(1).max(4096).nullable();
|
export const descriptionPredicate = z.string().min(1).max(4096).nullable();
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
urlPredicate,
|
urlPredicate,
|
||||||
validateFieldLength,
|
validateFieldLength,
|
||||||
} from './Assertions';
|
} from './Assertions';
|
||||||
import { EmbedAuthorOptions, EmbedFooterOptions, UnsafeEmbed } from './UnsafeEmbed';
|
import { EmbedAuthorOptions, EmbedFooterOptions, RGBTuple, UnsafeEmbed } from './UnsafeEmbed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a validated embed in a message (image/video preview, rich embed, etc.)
|
* Represents a validated embed in a message (image/video preview, rich embed, etc.)
|
||||||
@@ -48,7 +48,7 @@ export class Embed extends UnsafeEmbed {
|
|||||||
return super.setAuthor(options);
|
return super.setAuthor(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override setColor(color: number | null): this {
|
public override setColor(color: number | RGBTuple | null): this {
|
||||||
// Data assertions
|
// Data assertions
|
||||||
return super.setColor(colorPredicate.parse(color));
|
return super.setColor(colorPredicate.parse(color));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import type {
|
|||||||
APIEmbedImage,
|
APIEmbedImage,
|
||||||
APIEmbedVideo,
|
APIEmbedVideo,
|
||||||
} from 'discord-api-types/v9';
|
} from 'discord-api-types/v9';
|
||||||
|
import type { Equatable } from '../../util/equatable';
|
||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
|
||||||
|
export type RGBTuple = [red: number, green: number, blue: number];
|
||||||
|
|
||||||
export interface IconData {
|
export interface IconData {
|
||||||
/**
|
/**
|
||||||
@@ -36,7 +40,7 @@ export interface EmbedImageData extends Omit<APIEmbedImage, 'proxy_url'> {
|
|||||||
/**
|
/**
|
||||||
* Represents a non-validated embed in a message (image/video preview, rich embed, etc.)
|
* Represents a non-validated embed in a message (image/video preview, rich embed, etc.)
|
||||||
*/
|
*/
|
||||||
export class UnsafeEmbed {
|
export class UnsafeEmbed implements Equatable<APIEmbed | UnsafeEmbed> {
|
||||||
protected data: APIEmbed;
|
protected data: APIEmbed;
|
||||||
|
|
||||||
public constructor(data: APIEmbed = {}) {
|
public constructor(data: APIEmbed = {}) {
|
||||||
@@ -164,6 +168,13 @@ export class UnsafeEmbed {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hex color of the current color of the embed
|
||||||
|
*/
|
||||||
|
public get hexColor() {
|
||||||
|
return typeof this.data.color === 'number' ? `#${this.data.color.toString(16).padStart(6, '0')}` : this.data.color;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a field to the embed (max 25)
|
* Adds a field to the embed (max 25)
|
||||||
*
|
*
|
||||||
@@ -228,7 +239,12 @@ export class UnsafeEmbed {
|
|||||||
*
|
*
|
||||||
* @param color The color of the embed
|
* @param color The color of the embed
|
||||||
*/
|
*/
|
||||||
public setColor(color: number | null): this {
|
public setColor(color: number | RGBTuple | null): this {
|
||||||
|
if (Array.isArray(color)) {
|
||||||
|
const [red, green, blue] = color;
|
||||||
|
this.data.color = (red << 16) + (green << 8) + blue;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
this.data.color = color ?? undefined;
|
this.data.color = color ?? undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -315,6 +331,13 @@ export class UnsafeEmbed {
|
|||||||
return { ...this.data };
|
return { ...this.data };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public equals(other: UnsafeEmbed | APIEmbed) {
|
||||||
|
const { image: thisImage, thumbnail: thisThumbnail, ...thisData } = this.data;
|
||||||
|
const data = other instanceof UnsafeEmbed ? other.data : other;
|
||||||
|
const { image, thumbnail, ...otherData } = data;
|
||||||
|
return isEqual(otherData, thisData) && image?.url === thisImage?.url && thumbnail?.url === thisThumbnail?.url;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes field input and resolves strings
|
* Normalizes field input and resolves strings
|
||||||
*
|
*
|
||||||
|
|||||||
14
packages/builders/src/util/equatable.ts
Normal file
14
packages/builders/src/util/equatable.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export interface Equatable<T> {
|
||||||
|
/**
|
||||||
|
* Whether or not this is equal to another structure
|
||||||
|
*/
|
||||||
|
equals: (other: T) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if an object is equatable or not.
|
||||||
|
* @param maybeEquatable The object to check against
|
||||||
|
*/
|
||||||
|
export function isEquatable(maybeEquatable: unknown): maybeEquatable is Equatable<unknown> {
|
||||||
|
return maybeEquatable !== null && typeof maybeEquatable === 'object' && 'equals' in maybeEquatable;
|
||||||
|
}
|
||||||
@@ -1769,6 +1769,7 @@ __metadata:
|
|||||||
eslint-config-marine: ^9.3.2
|
eslint-config-marine: ^9.3.2
|
||||||
eslint-config-prettier: ^8.3.0
|
eslint-config-prettier: ^8.3.0
|
||||||
eslint-plugin-prettier: ^4.0.0
|
eslint-plugin-prettier: ^4.0.0
|
||||||
|
fast-deep-equal: ^3.1.3
|
||||||
jest: ^27.5.1
|
jest: ^27.5.1
|
||||||
prettier: ^2.5.1
|
prettier: ^2.5.1
|
||||||
ts-mixer: ^6.0.0
|
ts-mixer: ^6.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user