Plugin
RoleColorEverywhere
Adds the top role color anywhere possible
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import { definePluginSettings } from "@api/Settings";8
import ErrorBoundary from "@components/ErrorBoundary";9
import { Devs } from "@utils/constants";10
import { Logger } from "@utils/Logger";11
import definePlugin, { makeRange, OptionType } from "@utils/types";12
import { findByCodeLazy } from "@webpack";13
import { ChannelStore, GuildMemberStore, GuildRoleStore, GuildStore } from "@webpack/common";14
15
const useMessageAuthor = findByCodeLazy(039;"Result cannot be null because the message is not null"039;);16
17
const settings = definePluginSettings({18
chatMentions: {19
type: OptionType.BOOLEAN,20
default: true,21
description: "Show role colors in chat mentions (including in the message box)",22
restartNeeded: true23
},24
memberList: {25
type: OptionType.BOOLEAN,26
default: true,27
description: "Show role colors in member list role headers",28
restartNeeded: true29
},30
voiceUsers: {31
type: OptionType.BOOLEAN,32
default: true,33
description: "Show role colors in the voice chat user list",34
restartNeeded: true35
},36
reactorsList: {37
type: OptionType.BOOLEAN,38
default: true,39
description: "Show role colors in the reactors list",40
restartNeeded: true41
},42
pollResults: {43
type: OptionType.BOOLEAN,44
default: true,45
description: "Show role colors in the poll results",46
restartNeeded: true47
},48
colorChatMessages: {49
type: OptionType.BOOLEAN,50
default: false,51
description: "Color chat messages based on the author039;s role color",52
restartNeeded: true,53
},54
messageSaturation: {55
type: OptionType.SLIDER,56
description: "Intensity of message coloring.",57
markers: makeRange(0, 100, 10),58
default: 3059
}60
});61
62
export default definePlugin({63
name: "RoleColorEverywhere",64
authors: [Devs.KingFish, Devs.lewisakura, Devs.AutumnVN, Devs.Kyuuhachi, Devs.jamesbt365],65
description: "Adds the top role color anywhere possible",66
tags: ["Roles", "Appearance"],67
settings,68
69
patches: [70
// Chat Mentions71
{72
find: ".USER_MENTION)",73
replacement: [74
{75
match: /(?<=user:(\i),guildId:([^,]+?),.{0,100}?children:\i=>\i)\((\i)\)/,76
replace: "({...$3,color:$self.getColorInt($1?.id,$2)})",77
}78
],79
predicate: () => settings.store.chatMentions80
},81
// Slate82
{83
// Same find as FullUserInChatbox84
find: 039;"text":"locked"039;,85
replacement: [86
{87
match: /let\{id:(\i),guildId:\i,channelId:(\i)[^}]*\}.*?\.\i,{(?=children)/,88
replace: "$&color:$self.getColorInt($1,$2),"89
}90
],91
predicate: () => settings.store.chatMentions92
},93
// Member List Role Headers94
{95
find: 039;tutorialId:"whos-online039;,96
replacement: [97
{98
match: /(#{intl::CHANNEL_MEMBERS_A11Y_LABEL}.+}\):null,).{0,100}?(?:—|\\u2014) ",\i\]\}\)\]/,99
replace: "$1$self.RoleGroupColor(arguments[0])]"100
},101
],102
predicate: () => settings.store.memberList103
},104
{105
find: "#{intl::THREAD_BROWSER_PRIVATE}",106
replacement: [107
{108
match: /children:\[\i," (?:—|\\u2014) ",\i\]/,109
replace: "children:[$self.RoleGroupColor(arguments[0])]"110
},111
],112
predicate: () => settings.store.memberList113
},114
// Voice Users115
{116
find: "#{intl::GUEST_NAME_SUFFIX})]",117
replacement: [118
{119
match: /#{intl::GUEST_NAME_SUFFIX}.{0,50}?""\](?<=guildId:(\i),.+?user:(\i).+?)/,120
replace: "$&,style:$self.getColorStyle($2.id,$1),"121
}122
],123
predicate: () => settings.store.voiceUsers124
},125
// Reaction List126
{127
find: "MessageReactions.render:",128
replacement: {129
match: /tag:"strong",variant:"text-md\/medium"(?<=onContextMenu:.{0,15}\((\i),(\i),\i\).+?)/,130
replace: "$&,style:$self.getColorStyle($2?.id,$1?.channel?.id)"131
},132
predicate: () => settings.store.reactorsList,133
},134
// Poll Results135
{136
find: ",reactionVoteCounts",137
replacement: {138
match: /\.SIZE_32.+?variant:"text-md\/normal",className:\i\.\i,(?="aria-label":)/,139
replace: "$&style:$self.getColorStyle(arguments[0]?.user?.id,arguments[0]?.channel?.id),"140
},141
predicate: () => settings.store.pollResults142
},143
// Messages144
{145
find: ".SEND_FAILED,",146
replacement: {147
match: /(?<=\]:(\i)\.isUnsupported.{0,50}?,)(?=children:\[)/,148
replace: "style:$self.useMessageColorsStyle($1),"149
},150
predicate: () => settings.store.colorChatMessages151
}152
],153
154
getColorString(userId: string, channelOrGuildId: string) {155
try {156
const guildId = ChannelStore.getChannel(channelOrGuildId)?.guild_id ?? GuildStore.getGuild(channelOrGuildId)?.id;157
if (guildId == null) return null;158
159
return GuildMemberStore.getMember(guildId, userId)?.colorString ?? null;160
} catch (e) {161
new Logger("RoleColorEverywhere").error("Failed to get color string", e);162
}163
164
return null;165
},166
167
getColorInt(userId: string, channelOrGuildId: string) {168
const colorString = this.getColorString(userId, channelOrGuildId);169
return colorString && parseInt(colorString.slice(1), 16);170
},171
172
getColorStyle(userId: string, channelOrGuildId: string) {173
const colorString = this.getColorString(userId, channelOrGuildId);174
175
return colorString && {176
color: colorString177
};178
},179
180
useMessageColorsStyle(message: any) {181
try {182
const { messageSaturation } = settings.use(["messageSaturation"]);183
const author = useMessageAuthor(message);184
185
// Do not apply role color if the send fails, otherwise it becomes indistinguishable186
if (message.state === "SEND_FAILED") return;187
188
if (author.colorString != null && messageSaturation !== 0) {189
const value = `color-mix(in oklab, ${author.colorString} ${messageSaturation}%, var({DEFAULT}))`;190
191
return {192
color: value.replace("{DEFAULT}", "--text-default"),193
"--text-strong": value.replace("{DEFAULT}", "--text-strong"),194
"--text-muted": value.replace("{DEFAULT}", "--text-muted")195
};196
}197
} catch (e) {198
new Logger("RoleColorEverywhere").error("Failed to get message color", e);199
}200
201
return null;202
},203
204
RoleGroupColor: ErrorBoundary.wrap(({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) => {205
const role = GuildRoleStore.getRole(guildId, id);206
207
return (208
<span style={{209
color: role?.colorString,210
fontWeight: "unset",211
letterSpacing: ".05em"212
}}>213
{title ?? label} — {count}214
</span>215
);216
}, { noop: true })217
});218