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
1
import { definePluginSettings } from "@api/Settings";2
import { getUserSettingLazy } from "@api/UserSettings";3
import { CopyIdIcon, ImageIcon } from "@components/Icons";4
import { copyToClipboard } from "@utils/clipboard";5
import { Devs } from "@utils/constants";6
import { getCurrentChannel, getCurrentGuild, getIntlMessage, openImageModal } from "@utils/discord";7
import { isTruthy } from "@utils/guards";8
import { classes } from "@utils/misc";9
import definePlugin, { OptionType } from "@utils/types";10
import { Guild, PopoutProps, Role } from "@vencord/discord-types";11
import { findByCodeLazy, findByPropsLazy, findCssClassesLazy } from "@webpack";12
import { ContextMenuApi, GuildRoleStore, Menu, PermissionStore, Popout, useRef } from "@webpack/common";13
import { ComponentType } from "react";14
15
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");16
const MenuItemClasses = findCssClassesLazy("item", "labelContainer", "colorDefault", "label", "iconContainer");17
const loadRoleMembers = findByCodeLazy(".GUILD_ROLE_MEMBER_IDS(", "requestMembersById");18
19
const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!;20
21
function PencilIcon() {22
return (23
<svg24
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
35
function 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
43
function 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
51
const 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: true60
},61
{62
label: "webp",63
value: "webp",64
},65
{66
label: "jpg",67
value: "jpg"68
}69
]70
}71
});72
73
interface RoleMemberPopoutProps {74
popoutProps: PopoutProps;75
guildId: string;76
channelId: string;77
roleId: string;78
}79
type RoleMemberPopout = ComponentType<RoleMemberPopoutProps>;80
81
let RoleMemberPopout: RoleMemberPopout = () => null;82
83
export 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.MenuItem89
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.MenuItem101
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.MenuItem113
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: 128121
});122
}}123
icon={ImageIcon}124
/>125
),126
popoutRef && (127
<Menu.MenuItem128
key="vc-view-role-members"129
id="vc-view-role-members"130
label="View Role Members"131
render={() => (132
<Popout133
position="right"134
align="center"135
targetElementRef={popoutRef}136
preload={() => loadRoleMembers(guild.id, role.id)}137
renderPopout={popoutProps => (138
<RoleMemberPopout139
popoutProps={popoutProps}140
guildId={guild.id}141
channelId={getCurrentChannel()!.id}142
roleId={role.id}143
/>144
)}145
>146
{popoutProps => (147
<div148
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
168
export 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.Menu181
navId="vc-better-role-context-member-list"182
onClose={ContextMenuApi.closeContextMenu}183
aria-label="Role Actions"184
>185
{before}186
{after}187
<Menu.MenuItem188
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
199
export 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 issue217
{218
find: 039;tutorialId:"whos-online039;,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 shown228
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