Plugin

RoleColorEverywhere

Adds the top role color anywhere possible

Roles Appearance
index.tsx
Download

Source

src/plugins/roleColorEverywhere/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import { definePluginSettings } from "@api/Settings";
8import ErrorBoundary from "@components/ErrorBoundary";
9import { Devs } from "@utils/constants";
10import { Logger } from "@utils/Logger";
11import definePlugin, { makeRange, OptionType } from "@utils/types";
12import { findByCodeLazy } from "@webpack";
13import { ChannelStore, GuildMemberStore, GuildRoleStore, GuildStore } from "@webpack/common";
14
15const useMessageAuthor = findByCodeLazy('"Result cannot be null because the message is not null"');
16
17const 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: true
23 },
24 memberList: {
25 type: OptionType.BOOLEAN,
26 default: true,
27 description: "Show role colors in member list role headers",
28 restartNeeded: true
29 },
30 voiceUsers: {
31 type: OptionType.BOOLEAN,
32 default: true,
33 description: "Show role colors in the voice chat user list",
34 restartNeeded: true
35 },
36 reactorsList: {
37 type: OptionType.BOOLEAN,
38 default: true,
39 description: "Show role colors in the reactors list",
40 restartNeeded: true
41 },
42 pollResults: {
43 type: OptionType.BOOLEAN,
44 default: true,
45 description: "Show role colors in the poll results",
46 restartNeeded: true
47 },
48 colorChatMessages: {
49 type: OptionType.BOOLEAN,
50 default: false,
51 description: "Color chat messages based on the author'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: 30
59 }
60});
61
62export 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 Mentions
71 {
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.chatMentions
80 },
81 // Slate
82 {
83 // Same find as FullUserInChatbox
84 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.chatMentions
92 },
93 // Member List Role Headers
94 {
95 find: &#039;tutorialId:"whos-online&#039;,
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.memberList
103 },
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.memberList
113 },
114 // Voice Users
115 {
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.voiceUsers
124 },
125 // Reaction List
126 {
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 Results
135 {
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.pollResults
142 },
143 // Messages
144 {
145 find: ".SEND_FAILED,",
146 replacement: {
147 match: /(?<=\]:(\i)\.isUnsupported.{0,50}?,)(?=children:\[)/,
148 replace: "style:$self.useMessageColorsStyle($1),"
149 },
150 predicate: () => settings.store.colorChatMessages
151 }
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: colorString
177 };
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 indistinguishable
186 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} &mdash; {count}
214 </span>
215 );
216 }, { noop: true })
217});
218