From 9d6fdf8979d29787a13912cfa55f1ff3961c6b68 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:34:52 +0100 Subject: [PATCH] feat: Role gradient colours (#10986) * feat: backport role gradient colours * chore: add deprecations --- .../src/managers/GuildMemberRoleManager.js | 2 +- .../discord.js/src/managers/RoleManager.js | 80 +++++++++++++++++-- packages/discord.js/src/structures/Role.js | 64 +++++++++++++-- packages/discord.js/src/util/Constants.js | 16 ++++ packages/discord.js/typings/index.d.ts | 23 ++++++ 5 files changed, 173 insertions(+), 12 deletions(-) diff --git a/packages/discord.js/src/managers/GuildMemberRoleManager.js b/packages/discord.js/src/managers/GuildMemberRoleManager.js index 7d19bf778..81c5a1fe7 100644 --- a/packages/discord.js/src/managers/GuildMemberRoleManager.js +++ b/packages/discord.js/src/managers/GuildMemberRoleManager.js @@ -65,7 +65,7 @@ class GuildMemberRoleManager extends DataManager { * @readonly */ get color() { - const coloredRoles = this.cache.filter(role => role.color); + const coloredRoles = this.cache.filter(role => role.colors.primaryColor); if (!coloredRoles.size) return null; return coloredRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev)); } diff --git a/packages/discord.js/src/managers/RoleManager.js b/packages/discord.js/src/managers/RoleManager.js index a69018f54..f711d8ad9 100644 --- a/packages/discord.js/src/managers/RoleManager.js +++ b/packages/discord.js/src/managers/RoleManager.js @@ -12,6 +12,8 @@ const PermissionsBitField = require('../util/PermissionsBitField'); const { setPosition, resolveColor } = require('../util/Util'); let cacheWarningEmitted = false; +let deprecationEmittedForCreate = false; +let deprecationEmittedForEdit = false; /** * Manages API methods for roles and stores their cache. @@ -112,11 +114,24 @@ class RoleManager extends CachedManager { * @returns {?Snowflake} */ + /** + * @typedef {Object} RoleColorsResolvable + * @property {ColorResolvable} primaryColor The primary color of the role + * @property {ColorResolvable} [secondaryColor] The secondary color of the role. + * This will make the role a gradient between the other provided colors + * @property {ColorResolvable} [tertiaryColor] The tertiary color of the role. + * When sending `tertiaryColor` the API enforces the role color to be a holographic style + * with values of `primaryColor = 11127295`, `secondaryColor = 16759788`, and `tertiaryColor = 16761760`. + * These values are available as a constant: `Constants.HolographicStyle` + */ + /** * Options used to create a new role. * @typedef {Object} RoleCreateOptions * @property {string} [name] The name of the new role * @property {ColorResolvable} [color] The data to create the role with + * This property is deprecated. Use `colors` instead. + * @property {RoleColorsResolvable} [colors] The colors to create the role with * @property {boolean} [hoist] Whether or not the new role should be hoisted * @property {PermissionResolvable} [permissions] The permissions for the new role * @property {number} [position] The position of the new role @@ -142,15 +157,30 @@ class RoleManager extends CachedManager { * // Create a new role with data and a reason * guild.roles.create({ * name: 'Super Cool Blue People', - * color: Colors.Blue, * reason: 'we needed a role for Super Cool People', + * colors: { + * primaryColor: Colors.Blue, + * }, + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Create a role with holographic colors + * guild.roles.create({ + * name: 'Holographic Role', + * reason: 'Creating a role with holographic effect', + * colors: { + * primaryColor: Constants.HolographicStyle.Primary, + * secondaryColor: Constants.HolographicStyle.Secondary, + * tertiaryColor: Constants.HolographicStyle.Tertiary, + * }, * }) * .then(console.log) * .catch(console.error); */ async create(options = {}) { - let { name, color, hoist, permissions, position, mentionable, reason, icon, unicodeEmoji } = options; - color &&= resolveColor(color); + let { permissions, icon } = options; + const { name, color, hoist, position, mentionable, reason, unicodeEmoji } = options; if (permissions !== undefined) permissions = new PermissionsBitField(permissions); if (icon) { const guildEmojiURL = this.guild.emojis.resolve(icon)?.imageURL(); @@ -158,10 +188,30 @@ class RoleManager extends CachedManager { if (typeof icon !== 'string') icon = undefined; } + let colors = options.colors && { + primary_color: resolveColor(options.colors.primaryColor), + secondary_color: options.colors.secondaryColor && resolveColor(options.colors.secondaryColor), + tertiary_color: options.colors.tertiaryColor && resolveColor(options.colors.tertiaryColor), + }; + + if (color !== undefined) { + if (!deprecationEmittedForCreate) { + process.emitWarning(`Passing "color" to RoleManager#create() is deprecated. Use "colors" instead.`); + } + + deprecationEmittedForCreate = true; + + colors = { + primary_color: resolveColor(color), + secondary_color: null, + tertiary_color: null, + }; + } + const data = await this.client.rest.post(Routes.guildRoles(this.guild.id), { body: { name, - color, + colors, hoist, permissions, mentionable, @@ -210,9 +260,29 @@ class RoleManager extends CachedManager { if (typeof icon !== 'string') icon = undefined; } + let colors = options.colors && { + primary_color: resolveColor(options.colors.primaryColor), + secondary_color: options.colors.secondaryColor && resolveColor(options.colors.secondaryColor), + tertiary_color: options.colors.tertiaryColor && resolveColor(options.colors.tertiaryColor), + }; + + if (options.color !== undefined) { + if (!deprecationEmittedForEdit) { + process.emitWarning(`Passing "color" to RoleManager#edit() is deprecated. Use "colors" instead.`); + } + + deprecationEmittedForEdit = true; + + colors = { + primary_color: resolveColor(options.color), + secondary_color: null, + tertiary_color: null, + }; + } + const body = { name: options.name, - color: options.color === undefined ? undefined : resolveColor(options.color), + colors, hoist: options.hoist, permissions: options.permissions === undefined ? undefined : new PermissionsBitField(options.permissions), mentionable: options.mentionable, diff --git a/packages/discord.js/src/structures/Role.js b/packages/discord.js/src/structures/Role.js index 06cbac60b..2bea358e0 100644 --- a/packages/discord.js/src/structures/Role.js +++ b/packages/discord.js/src/structures/Role.js @@ -54,11 +54,37 @@ class Role extends Base { if ('color' in data) { /** * The base 10 color of the role + * * @type {number} + * @deprecated Use {@link Role#colors} instead. */ this.color = data.color; } + /** + * @typedef {Object} RoleColors + * @property {number} primaryColor The primary color of the role + * @property {?number} secondaryColor The secondary color of the role. + * This will make the role a gradient between the other provided colors + * @property {?number} tertiaryColor The tertiary color of the role. + * When sending `tertiaryColor` the API enforces the role color to be a holographic style + * with values of `primaryColor = 11127295`, `secondaryColor = 16759788`, and `tertiaryColor = 16761760`. + * These values are available as a constant: `Constants.HolographicStyle` + */ + + if ('colors' in data) { + /** + * The colors of the role + * + * @type {RoleColors} + */ + this.colors = { + primaryColor: data.colors.primary_color, + secondaryColor: data.colors.secondary_color, + tertiaryColor: data.colors.tertiary_color, + }; + } + if ('hoist' in data) { /** * If true, users that are part of this role will appear in a separate category in the users list @@ -231,6 +257,8 @@ class Role extends Base { * @typedef {Object} RoleData * @property {string} [name] The name of the role * @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number + * This property is deprecated. Use `colors` instead. + * @property {RoleColorsResolvable} [colors] The colors of the role * @property {boolean} [hoist] Whether or not the role should be hoisted * @property {number} [position] The position of the role * @property {PermissionResolvable} [permissions] The permissions of the role @@ -286,17 +314,39 @@ class Role extends Base { /** * Sets a new color for the role. + * * @param {ColorResolvable} color The color of the role * @param {string} [reason] Reason for changing the role's color * @returns {Promise} + * @deprecated Use {@link Role#setColors} instead. + */ + async setColor(color, reason) { + return this.edit({ color, reason }); + } + + /** + * Sets new colors for the role. + * + * @param {RoleColorsResolvable} colors The colors of the role + * @param {string} [reason] Reason for changing the role's colors + * @returns {Promise} * @example - * // Set the color of a role - * role.setColor('#FF0000') - * .then(updated => console.log(`Set color of role to ${updated.color}`)) + * // Set the colors of a role + * role.setColors({ primaryColor: '#FF0000', secondaryColor: '#00FF00', tertiaryColor: '#0000FF' }) + * .then(updated => console.log(`Set colors of role to ${updated.colors}`)) + * .catch(console.error); + * @example + * // Set holographic colors using constants + * role.setColors({ + * primaryColor: Constants.HolographicStyle.Primary, + * secondaryColor: Constants.HolographicStyle.Secondary, + * tertiaryColor: Constants.HolographicStyle.Tertiary, + * }) + * .then(updated => console.log(`Set holographic colors for role ${updated.name}`)) * .catch(console.error); */ - setColor(color, reason) { - return this.edit({ color, reason }); + async setColors(colors, reason) { + return this.edit({ colors, reason }); } /** @@ -434,7 +484,9 @@ class Role extends Base { role && this.id === role.id && this.name === role.name && - this.color === role.color && + this.colors.primaryColor === role.colors.primaryColor && + this.colors.secondaryColor === role.colors.secondaryColor && + this.colors.tertiaryColor === role.colors.tertiaryColor && this.hoist === role.hoist && this.position === role.position && this.permissions.bitfield === role.permissions.bitfield && diff --git a/packages/discord.js/src/util/Constants.js b/packages/discord.js/src/util/Constants.js index 8babdfdbe..528d11f8b 100644 --- a/packages/discord.js/src/util/Constants.js +++ b/packages/discord.js/src/util/Constants.js @@ -254,6 +254,21 @@ exports.StickerFormatExtensionMap = { [StickerFormatType.GIF]: ImageFormat.GIF, }; +/** + * Holographic color values for role styling. + * When using `tertiaryColor`, the API enforces these specific values for holographic effect. + * + * @typedef {Object} HolographicStyle + * @property {number} Primary 11127295 (0xA9FFFF) + * @property {number} Secondary 16759788 (0xFFCCCC) + * @property {number} Tertiary 16761760 (0xFFE0A0) + */ +exports.HolographicStyle = { + Primary: 11_127_295, + Secondary: 16_759_788, + Tertiary: 16_761_760, +}; + /** * @typedef {Object} Constants Constants that can be used in an enum or object-like way. * @property {number} MaxBulkDeletableMessageAge Max bulk deletable message age @@ -264,4 +279,5 @@ exports.StickerFormatExtensionMap = { * @property {VoiceBasedChannelTypes} VoiceBasedChannelTypes The types of channels that are voice-based * @property {SelectMenuTypes} SelectMenuTypes The types of components that are select menus. * @property {Object} StickerFormatExtensionMap A mapping between sticker formats and their respective image formats. + * @property {HolographicStyle} HolographicStyle Holographic color values for role styling. */ diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 1b3254c8b..a306b4dbc 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3080,9 +3080,23 @@ export class RichPresenceAssets { public smallImageURL(options?: ImageURLOptions): string | null; } +export interface RoleColors { + primaryColor: number; + secondaryColor: number | null; + tertiaryColor: number | null; +} + +export interface RoleColorsResolvable { + primaryColor: ColorResolvable; + secondaryColor?: ColorResolvable; + tertiaryColor?: ColorResolvable; +} + export class Role extends Base { private constructor(client: Client, data: RawRoleData, guild: Guild); + /** @deprecated Use {@link Role.colors} instead. */ public color: number; + public colors: RoleColors; public get createdAt(): Date; public get createdTimestamp(): number; public get editable(): boolean; @@ -3110,7 +3124,9 @@ export class Role extends Base { channel: NonThreadGuildBasedChannel | Snowflake, checkAdmin?: boolean, ): Readonly; + /** @deprecated Use {@link Role.setColors} instead. */ public setColor(color: ColorResolvable, reason?: string): Promise; + public setColors(colors: RoleColorsResolvable, reason?: string): Promise; public setHoist(hoist?: boolean, reason?: string): Promise; public setMentionable(mentionable?: boolean, reason?: string): Promise; public setName(name: string, reason?: string): Promise; @@ -4262,6 +4278,11 @@ export type DeletableMessageType = | MessageType.UserJoin; export const Constants: { + HolographicStyle: { + Primary: 11_127_295; + Secondary: 16_759_788; + Tertiary: 16_761_760; + }; MaxBulkDeletableMessageAge: 1_209_600_000; SweeperKeys: SweeperKey[]; NonSystemMessageTypes: NonSystemMessageType[]; @@ -7372,7 +7393,9 @@ export interface ResolvedOverwriteOptions { export interface RoleData { name?: string; + /** @deprecated Use {@link RoleData.colors} instead. */ color?: ColorResolvable; + colors?: RoleColorsResolvable; hoist?: boolean; position?: number; permissions?: PermissionResolvable;