Plugin

BetterRoleContext

Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile or in the member list

Roles Appearance
index.tsx
Download

Source

src/plugins/betterRoleContext/index.tsx
1import { definePluginSettings } from "@api/Settings";
2import { getUserSettingLazy } from "@api/UserSettings";
3import { CopyIdIcon, ImageIcon } from "@components/Icons";
4import { copyToClipboard } from "@utils/clipboard";
5import { Devs } from "@utils/constants";
6import { getCurrentChannel, getCurrentGuild, getIntlMessage, openImageModal } from "@utils/discord";
7import { isTruthy } from "@utils/guards";
8import { classes } from "@utils/misc";
9import definePlugin, { OptionType } from "@utils/types";
10import { Guild, PopoutProps, Role } from "@vencord/discord-types";
11import { findByCodeLazy, findByPropsLazy, findCssClassesLazy } from "@webpack";
12import { ContextMenuApi, GuildRoleStore, Menu, PermissionStore, Popout, useRef } from "@webpack/common";
13import { ComponentType } from "react";
14
15const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
16const MenuItemClasses = findCssClassesLazy("item", "labelContainer", "colorDefault", "label", "iconContainer");
17const loadRoleMembers = findByCodeLazy(".GUILD_ROLE_MEMBER_IDS(", "requestMembersById");
18
19const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!;
20
21function PencilIcon() {
22 return (
23 <svg
24 role="img"
25 width="18"
26 height="18"
27 fill="none"
28 viewBox="0 0 24 24"
29 >
30 <path fill="currentColor" d="m13.96 5.46 4.58 4.58a1 1 0 0 0 1.42 0l1.38-1.38a2 2 0 0 0 0-2.82l-3.18-3.18a2 2 0 0 0-2.82 0l-1.38 1.38a1 1 0 0 0 0 1.42ZM2.11 20.16l.73-4.22a3 3 0 0 1 .83-1.61l7.87-7.87a1 1 0 0 1 1.42 0l4.58 4.58a1 1 0 0 1 0 1.42l-7.87 7.87a3 3 0 0 1-1.6.83l-4.23.73a1.5 1.5 0 0 1-1.73-1.73Z" />
31 </svg>
32 );
33}
34
35function AppearanceIcon() {
36 return (
37 <svg width="18" height="18" viewBox="0 0 24 24">
38 <path fill="currentColor" d="M 12,0 C 5.3733333,0 0,5.3733333 0,12 c 0,6.626667 5.3733333,12 12,12 1.106667,0 2,-0.893333 2,-2 0,-0.52 -0.2,-0.986667 -0.52,-1.346667 -0.306667,-0.346666 -0.506667,-0.813333 -0.506667,-1.32 0,-1.106666 0.893334,-2 2,-2 h 2.36 C 21.013333,17.333333 24,14.346667 24,10.666667 24,4.7733333 18.626667,0 12,0 Z M 4.6666667,12 c -1.1066667,0 -2,-0.893333 -2,-2 0,-1.1066667 0.8933333,-2 2,-2 1.1066666,0 2,0.8933333 2,2 0,1.106667 -0.8933334,2 -2,2 z M 8.666667,6.6666667 c -1.106667,0 -2.0000003,-0.8933334 -2.0000003,-2 0,-1.1066667 0.8933333,-2 2.0000003,-2 1.106666,0 2,0.8933333 2,2 0,1.1066666 -0.893334,2 -2,2 z m 6.666666,0 c -1.106666,0 -2,-0.8933334 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.1066666 -0.893333,2 -2,2 z m 4,5.3333333 c -1.106666,0 -2,-0.893333 -2,-2 0,-1.1066667 0.893334,-2 2,-2 1.106667,0 2,0.8933333 2,2 0,1.106667 -0.893333,2 -2,2 z" />
39 </svg>
40 );
41}
42
43function RoleMembersIcon() {
44 return (
45 <svg width="18" height="18" viewBox="0 0 24 24">
46 <path fill="currentColor" d="M12 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM11.53 11A9.53 9.53 0 0 0 2 20.53c0 .81.66 1.47 1.47 1.47h.22c.24 0 .44-.17.5-.4.29-1.12.84-2.17 1.32-2.91.14-.21.43-.1.4.15l-.26 2.61c-.02.3.2.55.5.55h11.7a.5.5 0 0 0 .5-.55l-.27-2.6c-.02-.26.27-.37.41-.16.48.74 1.03 1.8 1.32 2.9.06.24.26.41.5.41h.22c.81 0 1.47-.66 1.47-1.47A9.53 9.53 0 0 0 12.47 11h-.94Z" />
47 </svg>
48 );
49}
50
51const settings = definePluginSettings({
52 roleIconFileFormat: {
53 type: OptionType.SELECT,
54 description: "File format to use when viewing role icons",
55 options: [
56 {
57 label: "png",
58 value: "png",
59 default: true
60 },
61 {
62 label: "webp",
63 value: "webp",
64 },
65 {
66 label: "jpg",
67 value: "jpg"
68 }
69 ]
70 }
71});
72
73interface RoleMemberPopoutProps {
74 popoutProps: PopoutProps;
75 guildId: string;
76 channelId: string;
77 roleId: string;
78}
79type RoleMemberPopout = ComponentType<RoleMemberPopoutProps>;
80
81let RoleMemberPopout: RoleMemberPopout = () => null;
82
83export function buildExtraRoleContextMenuItems(role: Role, guild: Guild, popoutRef?: React.RefObject<any>) {
84 if (!role) return { before: [], after: [] };
85
86 const before = [
87 PermissionStore.getGuildPermissionProps(guild).canManageRoles && (
88 <Menu.MenuItem
89 key="vc-edit-role"
90 id="vc-edit-role"
91 label="Edit Role"
92 action={async () => {
93 await GuildSettingsActions.open(guild.id, "ROLES");
94 GuildSettingsActions.selectRole(role.id);
95 }}
96 icon={PencilIcon}
97 />
98 ),
99 role.colorString && (
100 <Menu.MenuItem
101 key="vc-copy-role-color"
102 id="vc-copy-role-color"
103 label="Copy Role Color"
104 action={() => copyToClipboard(role.colorString!)}
105 icon={AppearanceIcon}
106 />
107 )
108 ].filter(isTruthy);
109
110 const after = [
111 role.icon && (
112 <Menu.MenuItem
113 key="vc-view-role-icon"
114 id="vc-view-role-icon"
115 label="View Role Icon"
116 action={() => {
117 openImageModal({
118 url: `${location.protocol}class="ts-cmt">//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`,
119 height: 128,
120 width: 128
121 });
122 }}
123 icon={ImageIcon}
124 />
125 ),
126 popoutRef && (
127 <Menu.MenuItem
128 key="vc-view-role-members"
129 id="vc-view-role-members"
130 label="View Role Members"
131 render={() => (
132 <Popout
133 position="right"
134 align="center"
135 targetElementRef={popoutRef}
136 preload={() => loadRoleMembers(guild.id, role.id)}
137 renderPopout={popoutProps => (
138 <RoleMemberPopout
139 popoutProps={popoutProps}
140 guildId={guild.id}
141 channelId={getCurrentChannel()!.id}
142 roleId={role.id}
143 />
144 )}
145 >
146 {popoutProps => (
147 <div
148 className={classes(MenuItemClasses.item, MenuItemClasses.labelContainer, MenuItemClasses.colorDefault)}
149 ref={popoutRef}
150 role="menuitem"
151 {...popoutProps}
152 >
153 <div className={MenuItemClasses.label}>View Role Members</div>
154 <div className={MenuItemClasses.iconContainer}>
155 <RoleMembersIcon />
156 </div>
157 </div>
158 )}
159 </Popout>
160 )}
161 />
162 )
163 ].filter(isTruthy);
164
165 return { before, after };
166}
167
168export function openRoleContextMenu(event: React.MouseEvent<HTMLElement>, { guildId, id: roleId }: { guildId: string; id: string; }) {
169 const guild = getCurrentGuild();
170 if (!guild || guild.id !== guildId) return;
171
172 const role = GuildRoleStore.getRole(guildId, roleId);
173 if (!role) return;
174
175 ContextMenuApi.openContextMenu(event, () => {
176 const popoutRef = useRef(null);
177 const { before, after } = buildExtraRoleContextMenuItems(role, guild, popoutRef);
178
179 return (
180 <Menu.Menu
181 navId="vc-better-role-context-member-list"
182 onClose={ContextMenuApi.closeContextMenu}
183 aria-label="Role Actions"
184 >
185 {before}
186 {after}
187 <Menu.MenuItem
188 key="vc-better-role-context-copy-role-id"
189 id="vc-better-role-context-copy-role-id"
190 label={getIntlMessage("COPY_ID_ROLE")}
191 icon={CopyIdIcon}
192 action={() => copyToClipboard(role.id)}
193 />
194 </Menu.Menu>
195 );
196 });
197}
198
199export default definePlugin({
200 name: "BetterRoleContext",
201 description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile or in the member list",
202 tags: ["Roles", "Appearance"],
203 authors: [Devs.Ven, Devs.goodbee, Devs.nightmaresan],
204 dependencies: ["UserSettingsAPI"],
205 settings,
206 openRoleContextMenu,
207 patches: [
208 {
209 find: ".ROLE_MENTION)",
210 replacement: {
211 match: /function (\i)(?=.+?renderPopout:.{0,20}\1,\{guildId:\i,channelId:\i)/,
212 replace: "$self.RoleMembers=$1;$&"
213 }
214 },
215 // Conflicts with RoleColorEverywhere which changes the code at the end of our match. (and also uses same find & similar match)
216 // However, BetterRoleContext applies first (alphabetic order), so it's not an issue
217 {
218 find: &#039;tutorialId:"whos-online&#039;,
219 replacement: {
220 match: /(?<=#{intl::CHANNEL_MEMBERS_A11Y_LABEL}.{0,200}?"aria-hidden":!0,)children:.{0,200}?(?:—|\\u2014) ",\i\]\}\)\]/,
221 replace: "onContextMenu:e=>$self.openRoleContextMenu(e,arguments[0]),$&"
222 }
223 }
224 ],
225
226 start() {
227 // DeveloperMode needs to be enabled for the context menu to be shown
228 DeveloperMode.updateSetting(true);
229 },
230
231 set RoleMembers(component: RoleMemberPopout) {
232 RoleMemberPopout = component;
233 },
234
235 contextMenus: {
236 "dev-context"(children, { id }: { id: string; }) {
237 const popoutRef = useRef(null);
238
239 const guild = getCurrentGuild();
240 if (!guild) return;
241
242 const role = GuildRoleStore.getRole(guild.id, id);
243 if (!role) return;
244
245 const { before, after } = buildExtraRoleContextMenuItems(role, guild, popoutRef);
246 children.unshift(...before);
247 children.push(...after);
248 }
249 }
250});
251