Merge branch 'dev' of https://github.com/Vendicated/Vencord into discord-types

This commit is contained in:
ryan-0324 2024-09-02 14:53:38 -04:00
commit de815b7586
21 changed files with 373 additions and 249 deletions

View file

@ -344,5 +344,6 @@ export const commonRendererPlugins = [
banImportPlugin(builtinModuleRegex, "Cannot import node inbuilt modules in browser code. You need to use a native.ts file"), banImportPlugin(builtinModuleRegex, "Cannot import node inbuilt modules in browser code. You need to use a native.ts file"),
banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"), banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"),
banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"), banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"),
banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
...commonOpts.plugins ...commonOpts.plugins
]; ];

View file

@ -83,24 +83,34 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle> <Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText> <Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div> <div>
{themeLinks.map(link => ( {themeLinks.map(rawLink => {
<Card const { label, link } = (() => {
style={{ const match = /^@(light|dark) (.*)/.exec(rawLink);
padding: ".5em", if (!match) return { label: rawLink, link: rawLink };
marginBottom: ".5em",
marginTop: ".5em" const [, mode, link] = match;
}} return { label: `[${mode} mode only] ${link}`, link };
key={link} })();
>
<Forms.FormTitle return (
tag="h5" <Card
style={{ overflowWrap: "break-word" }} style={{
padding: ".5em",
marginBottom: ".5em",
marginTop: ".5em"
}}
key={link}
> >
{link} <Forms.FormTitle
</Forms.FormTitle> tag="h5"
<Validator link={link} /> style={{ overflowWrap: "break-word" }}
</Card> >
))} {label}
</Forms.FormTitle>
<Validator link={link!} />
</Card>
);
})}
</div> </div>
</> </>
); );
@ -297,6 +307,7 @@ function ThemesTab() {
<Card className="vc-settings-card vc-text-selectable"> <Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle> <Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText> <Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText> <Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
</Card> </Card>

View file

@ -14,7 +14,7 @@ import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types"; import definePlugin, { OptionType, StartAt } from "@utils/types";
import type { PersistedStore } from "@vencord/discord-types"; import type { PersistedStore } from "@vencord/discord-types";
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, useStateFromStores } from "@webpack/common"; import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
@ -38,7 +38,6 @@ function setTheme(theme: string) {
saveClientTheme({ theme }); saveClientTheme({ theme });
} }
const ThemeStore: PersistedStore & Record<string, any> = findStoreLazy("ThemeStore");
const ClientThemesBackgroundStore: PersistedStore & Record<string, any> = findStoreLazy("ClientThemesBackgroundStore"); const ClientThemesBackgroundStore: PersistedStore & Record<string, any> = findStoreLazy("ClientThemesBackgroundStore");
function ThemeSettings() { function ThemeSettings() {

View file

@ -0,0 +1,13 @@
# IgnoreActivities
Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings.
![](https://github.com/user-attachments/assets/f0c19060-0ecf-4f1c-8165-a5aa40143c82)
![](https://github.com/user-attachments/assets/73c3fa7a-5b90-41ee-a4d6-91fa76458b74)
![](https://github.com/user-attachments/assets/1ab3fe73-3911-48d1-8a08-e976af614b41)
The activity stays showing as a detected game even if ignored, differently from the stock Toggle Detection button from Discord:
![](https://github.com/user-attachments/assets/08ea60c3-3a31-42de-ae4c-7535fbf1b45a)

View file

@ -246,7 +246,7 @@ function isActivityTypeIgnored(type: ActivityType, id?: string) {
export default definePlugin({ export default definePlugin({
name: "IgnoreActivities", name: "IgnoreActivities",
authors: [Devs.Nuckyz, Devs.Kylie], authors: [Devs.Nuckyz, Devs.Kylie],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.", description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below",
dependencies: ["UserSettingsAPI"], dependencies: ["UserSettingsAPI"],
settings, settings,
@ -275,6 +275,7 @@ export default definePlugin({
replace: (m, props, nowPlaying) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),` replace: (m, props, nowPlaying) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),`
} }
}, },
// Discord has 3 different components for activities. Currently, the last is the one being used
{ {
find: ".activityTitleText,variant", find: ".activityTitleText,variant",
replacement: { replacement: {
@ -288,6 +289,13 @@ export default definePlugin({
match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/, match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/,
replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),` replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
} }
},
{
find: ".promotedLabelWrapperNonBanner,children",
replacement: {
match: /\.appDetailsHeaderContainer.+?children:\i.*?}\),(?<=application:(\i).+?)/,
replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),`
}
} }
], ],

View file

@ -20,11 +20,13 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import type { GroupDMChannelRecord, UserRecord } from "@vencord/discord-types"; import type { GroupDMChannelRecord, UserRecord } from "@vencord/discord-types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore, UserUtils } from "@webpack/common"; import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore, UserUtils } from "@webpack/common";
import type { ReactNode } from "react";
const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel"); const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
const ExpandableList = findComponentByCodeLazy(".mutualFriendItem]");
const ProfileListClasses: Record<string, string> = findByPropsLazy("emptyIconFriends", "emptyIconGuilds"); const ProfileListClasses: Record<string, string> = findByPropsLazy("emptyIconFriends", "emptyIconGuilds");
const GuildLabelClasses: Record<string, string> = findByPropsLazy("guildNick", "guildAvatarWithoutIcon"); const GuildLabelClasses: Record<string, string> = findByPropsLazy("guildNick", "guildAvatarWithoutIcon");
@ -54,9 +56,30 @@ function getMutualGDMCountText(user: UserRecord) {
for (const channel of Object.values(ChannelStore.getMutablePrivateChannels())) for (const channel of Object.values(ChannelStore.getMutablePrivateChannels()))
if (channel.isGroupDM() && channel.recipients.includes(user.id)) if (channel.isGroupDM() && channel.recipients.includes(user.id))
count++; count++;
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`; return `${count === 0 ? "No" : count} Mutual Group${count === 1 ? "" : "s"}`;
} }
const renderClickableGDMs = (mutualDms: GroupDMChannelRecord[], onClose: () => void) =>
mutualDms.map(channel => (
<Clickable
className={ProfileListClasses.listRow}
onClick={() => {
onClose();
SelectedChannelActionCreators.selectPrivateChannel(channel.id);
}}
>
<Avatar
src={IconUtils.getChannelIconURL({ id: channel.id, icon: channel.icon, size: 32 })}
size="SIZE_40"
className={ProfileListClasses.listAvatar}
/>
<div className={ProfileListClasses.listRowContent}>
<div className={ProfileListClasses.listName}>{getGroupDMName(channel)}</div>
<div className={GuildLabelClasses.guildNick}>{channel.recipients.length + 1} Members</div>
</div>
</Clickable>
));
const IS_PATCHED = Symbol("MutualGroupDMs.Patched"); const IS_PATCHED = Symbol("MutualGroupDMs.Patched");
export default definePlugin({ export default definePlugin({
@ -77,6 +100,13 @@ export default definePlugin({
replace: "$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):$&" replace: "$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):$&"
} }
] ]
},
{
find: 'section:"MUTUAL_FRIENDS"',
replacement: {
match: /\.openUserProfileModal.+?\)}\)}\)(?<=(\(0,\i\.jsxs?\)\(\i\.\i,{className:(\i)\.divider}\)).+?)/,
replace: "$&,$self.renderDMPageList({user: arguments[0].user, Divider: $1, listStyle: $2.list})"
}
} }
], ],
@ -93,25 +123,7 @@ export default definePlugin({
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: UserRecord; onClose: () => void; }) => { renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: UserRecord; onClose: () => void; }) => {
const mutualGroupDMs = useMutualGroupDMs(user.id); const mutualGroupDMs = useMutualGroupDMs(user.id);
const entries = mutualGroupDMs.map(channel => ( const entries = renderClickableGDMs(mutualGroupDMs, onClose);
<Clickable
className={ProfileListClasses.listRow}
onClick={() => {
onClose();
SelectedChannelActionCreators.selectPrivateChannel(channel.id);
}}
>
<Avatar
src={IconUtils.getChannelIconURL({ id: channel.id, icon: channel.icon, size: 32 })}
size="SIZE_40"
className={ProfileListClasses.listAvatar}
/>
<div className={ProfileListClasses.listRowContent}>
<div className={ProfileListClasses.listName}>{getGroupDMName(channel)}</div>
<div className={GuildLabelClasses.guildNick}>{channel.recipients.length + 1} Members</div>
</div>
</Clickable>
));
return ( return (
<ScrollerThin <ScrollerThin
@ -130,5 +142,25 @@ export default definePlugin({
} }
</ScrollerThin> </ScrollerThin>
); );
}),
renderDMPageList: ErrorBoundary.wrap(({ user, Divider, listStyle }: { user: UserRecord; Divider: ReactNode; listStyle: string; }) => {
const mutualGDms = useMutualGroupDMs(user.id);
if (mutualGDms.length === 0) return null;
const header = getMutualGDMCountText(user);
return (
<>
{Divider}
<ExpandableList
className={listStyle}
header={header}
isLoadingHeader={false}
>
{renderClickableGDMs(mutualGDms, () => {})}
</ExpandableList>
</>
);
}) })
}); });

View file

@ -21,8 +21,9 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons"; import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord"; import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, type ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ModalCloseButton, ModalContent, ModalHeader, type ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import type { GuildRecord } from "@vencord/discord-types"; import type { GuildRecord, Role, UnicodeEmoji } from "@vencord/discord-types";
import { ClipboardUtils, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, Permissions, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common"; import { findByCodeLazy } from "@webpack";
import { ClipboardUtils, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, Permissions, ScrollerThin, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import { settings } from ".."; import { settings } from "..";
import { cl, getPermissionDescription, getPermissionString } from "../utils"; import { cl, getPermissionDescription, getPermissionString } from "../utils";
@ -42,15 +43,17 @@ export interface RoleOrUserPermission {
overwriteDeny?: bigint; overwriteDeny?: bigint;
} }
function openRolesAndUsersPermissionsModal(permissions: RoleOrUserPermission[], guild: GuildRecord, header: string) { const getRoleIconData: {
return openModal(modalProps => ( (role: Role, size?: number | null): {
<RolesAndUsersPermissions customIconSrc: string | undefined;
modalProps={modalProps} unicodeEmoji: UnicodeEmoji | undefined;
permissions={permissions} };
guild={guild} (role?: null, size?: number | null): null;
header={header} } = findByCodeLazy("convertSurrogateToName", "customIconSrc", "unicodeEmoji");
/>
)); function getRoleIconSrc(role: Role) {
const icon = getRoleIconData(role, 20);
return icon.customIconSrc ?? icon.unicodeEmoji?.url;
} }
function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: RoleOrUserPermission[]; guild: GuildRecord; modalProps: ModalProps; header: string; }) { function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: RoleOrUserPermission[]; guild: GuildRecord; modalProps: ModalProps; header: string; }) {
@ -86,31 +89,34 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
size={ModalSize.LARGE} size={ModalSize.LARGE}
> >
<ModalHeader> <ModalHeader>
<Text className={cl("perms-title")} variant="heading-lg/semibold">{header} permissions:</Text> <Text className={cl("modal-title")} variant="heading-lg/semibold">{header} permissions:</Text>
<ModalCloseButton onClick={modalProps.onClose} /> <ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader> </ModalHeader>
<ModalContent> <ModalContent className={cl("modal-content")}>
{!selectedItem && ( {!selectedItem && (
<div className={cl("perms-no-perms")}> <div className={cl("modal-no-perms")}>
<Text variant="heading-lg/normal">No permissions to display!</Text> <Text variant="heading-lg/normal">No permissions to display!</Text>
</div> </div>
)} )}
{selectedItem && ( {selectedItem && (
<div className={cl("perms-container")}> <div className={cl("modal-container")}>
<div className={cl("perms-list")}> <ScrollerThin className={cl("modal-list")} orientation="auto">
{permissions.map((permission, index) => { {permissions.map((permission, index) => {
const user = UserStore.getUser(permission.id); const user = UserStore.getUser(permission.id);
const role = roles[permission.id ?? ""]; const role = permission.id ? roles[permission.id] : undefined;
const roleIconSrc = role ? getRoleIconSrc(role) : undefined;
return ( return (
<button <div
className={cl("perms-list-item-btn")} className={cl("modal-list-item-btn")}
onClick={() => { selectItem(index); }} onClick={() => { selectItem(index); }}
role="button"
tabIndex={0}
> >
<div <div
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })} className={cl("modal-list-item", { "modal-list-item-active": selectedItemIndex === index })}
onContextMenu={e => { onContextMenu={e => {
if (permission.type === PermissionType.Role) if (permission.type === PermissionType.Role)
ContextMenuApi.openContextMenu(e, () => ( ContextMenuApi.openContextMenu(e, () => (
@ -129,13 +135,19 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
> >
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && ( {(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
<span <span
className={cl("perms-role-circle")} className={cl("modal-role-circle")}
style={{ backgroundColor: role?.colorString ?? "var(--primary-300)" }} style={{ backgroundColor: role?.colorString ?? "var(--primary-300)" }}
/> />
)} )}
{permission.type === PermissionType.User && user !== undefined && ( {permission.type === PermissionType.Role && roleIconSrc != null && (
<img <img
className={cl("perms-user-img")} className={cl("modal-role-image")}
src={roleIconSrc}
/>
)}
{permission.type === PermissionType.User && user && (
<img
className={cl("modal-user-img")}
src={user.getAvatarURL()} src={user.getAvatarURL()}
/> />
)} )}
@ -147,24 +159,21 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
: ( : (
<Flex style={{ gap: "0.2em", justifyItems: "center" }}> <Flex style={{ gap: "0.2em", justifyItems: "center" }}>
@owner @owner
<OwnerCrownIcon <OwnerCrownIcon height={18} width={18} aria-hidden="true" />
height={18}
width={18}
aria-hidden="true"
/>
</Flex> </Flex>
) )
} }
</Text> </Text>
</div> </div>
</button> </div>
); );
})} })}
</div> </ScrollerThin>
<div className={cl("perms-perms")}> <div className={cl("modal-divider")} />
<ScrollerThin className={cl("modal-perms")} orientation="auto">
{Object.entries(Permissions).map(([permissionName, flag]) => ( {Object.entries(Permissions).map(([permissionName, flag]) => (
<div className={cl("perms-perms-item")}> <div className={cl("modal-perms-item")}>
<div className={cl("perms-perms-item-icon")}> <div className={cl("modal-perms-item-icon")}>
{(() => { {(() => {
const { permissions, overwriteAllow, overwriteDeny } = selectedItem; const { permissions, overwriteAllow, overwriteDeny } = selectedItem;
@ -188,7 +197,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
</Tooltip> </Tooltip>
</div> </div>
))} ))}
</div> </ScrollerThin>
</div> </div>
)} )}
</ModalContent> </ModalContent>
@ -204,7 +213,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: GuildRecord; roleI
aria-label="Role Options" aria-label="Role Options"
> >
<Menu.MenuItem <Menu.MenuItem
id="vc-copy-role-id" id={cl("copy-role-id")}
label={i18n.Messages.COPY_ID_ROLE} label={i18n.Messages.COPY_ID_ROLE}
action={() => { action={() => {
ClipboardUtils.copy(roleId); ClipboardUtils.copy(roleId);
@ -213,14 +222,13 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: GuildRecord; roleI
{(settings.store as any).unsafeViewAsRole && ( {(settings.store as any).unsafeViewAsRole && (
<Menu.MenuItem <Menu.MenuItem
id="vc-pw-view-as-role" id={cl("view-as-role")}
label={i18n.Messages.VIEW_AS_ROLE} label={i18n.Messages.VIEW_AS_ROLE}
action={() => { action={() => {
const role = GuildStore.getRole(guild.id, roleId); const role = GuildStore.getRole(guild.id, roleId);
if (!role) return; if (!role) return;
onClose(); onClose();
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({
type: "IMPERSONATE_UPDATE", type: "IMPERSONATE_UPDATE",
guildId: guild.id, guildId: guild.id,
@ -231,8 +239,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: GuildRecord; roleI
} }
} }
}); });
} }}
}
/> />
)} )}
</Menu.Menu> </Menu.Menu>
@ -247,7 +254,7 @@ function UserContextMenu({ userId }: { userId: string; }) {
aria-label="User Options" aria-label="User Options"
> >
<Menu.MenuItem <Menu.MenuItem
id="vc-copy-user-id" id={cl("copy-user-id")}
label={i18n.Messages.COPY_ID_USER} label={i18n.Messages.COPY_ID_USER}
action={() => { action={() => {
ClipboardUtils.copy(userId); ClipboardUtils.copy(userId);
@ -259,4 +266,13 @@ function UserContextMenu({ userId }: { userId: string; }) {
const RolesAndUsersPermissions = ErrorBoundary.wrap(RolesAndUsersPermissionsComponent); const RolesAndUsersPermissions = ErrorBoundary.wrap(RolesAndUsersPermissionsComponent);
export default openRolesAndUsersPermissionsModal; export default function openRolesAndUsersPermissionsModal(permissions: RoleOrUserPermission[], guild: GuildRecord, header: string) {
return openModal(modalProps => (
<RolesAndUsersPermissions
modalProps={modalProps}
permissions={permissions}
guild={guild}
header={header}
/>
));
}

View file

@ -22,6 +22,7 @@ import { classes } from "@utils/misc";
import type { GuildMember, GuildRecord } from "@vencord/discord-types"; import type { GuildMember, GuildRecord } from "@vencord/discord-types";
import { filters, findBulk, proxyLazyWebpack } from "@webpack"; import { filters, findBulk, proxyLazyWebpack } from "@webpack";
import { i18n, Permissions, Text, Tooltip, useMemo, UserStore } from "@webpack/common"; import { i18n, Permissions, Text, Tooltip, useMemo, UserStore } from "@webpack/common";
import type { HTMLAttributes } from "react";
import type { BuildTuple } from "type-fest/source/internal"; import type { BuildTuple } from "type-fest/source/internal";
import { PermissionsSortOrder, settings } from ".."; import { PermissionsSortOrder, settings } from "..";
@ -30,6 +31,7 @@ import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermi
interface UserPermission { interface UserPermission {
permission: string; permission: string;
roleName: string;
roleColor: string; roleColor: string;
rolePosition: number; rolePosition: number;
} }
@ -53,6 +55,42 @@ const { RoleRootClasses, RoleClasses, RoleBorderClasses } = proxyLazyWebpack(()
return { RoleRootClasses, RoleClasses, RoleBorderClasses }; return { RoleRootClasses, RoleClasses, RoleBorderClasses };
}); });
interface FakeRoleProps extends HTMLAttributes<HTMLDivElement> {
text: string;
color: string;
}
const FakeRole = ({ text, color, ...props }: FakeRoleProps) => (
<div {...props} className={classes(RoleClasses.role)}>
<div className={RoleClasses.roleRemoveButton}>
<span
className={classes(RoleBorderClasses.roleCircle, RoleClasses.roleCircle)}
style={{ backgroundColor: color }}
/>
</div>
<div className={RoleClasses.roleName}>
<Text
className={RoleClasses.roleNameOverflow}
variant="text-xs/medium"
>
{text}
</Text>
</div>
</div>
);
interface GrantedByTooltipProps {
roleName: string;
roleColor: string;
}
const GrantedByTooltip = ({ roleName, roleColor }: GrantedByTooltipProps) => (
<>
<Text variant="text-sm/medium">Granted By</Text>
<FakeRole text={roleName} color={roleColor} />
</>
);
interface UserPermissionsComponentProps { interface UserPermissionsComponentProps {
forceOpen?: boolean; forceOpen?: boolean;
guild: GuildRecord; guild: GuildRecord;
@ -60,7 +98,7 @@ interface UserPermissionsComponentProps {
} }
function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: UserPermissionsComponentProps) { function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: UserPermissionsComponentProps) {
const stns = settings.use(["permissionsSortOrder"]); const { permissionsSortOrder } = settings.use(["permissionsSortOrder"]);
const [rolePermissions, userPermissions] = useMemo(() => { const [rolePermissions, userPermissions] = useMemo(() => {
const userPermissions: UserPermission[] = []; const userPermissions: UserPermission[] = [];
@ -81,6 +119,7 @@ function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: Use
const OWNER = i18n.Messages.GUILD_OWNER || "Server Owner"; const OWNER = i18n.Messages.GUILD_OWNER || "Server Owner";
userPermissions.push({ userPermissions.push({
permission: OWNER, permission: OWNER,
roleName: "Owner",
roleColor: "var(--primary-300)", roleColor: "var(--primary-300)",
rolePosition: Infinity rolePosition: Infinity
}); });
@ -88,24 +127,20 @@ function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: Use
sortUserRoles(userRoles); sortUserRoles(userRoles);
for (const [permission, flag] of Object.entries(Permissions)) { for (const [permission, flag] of Object.entries(Permissions))
for (const { permissions, colorString, position } of userRoles) { for (const { permissions, colorString, position, name } of userRoles)
if ((permissions & flag) === flag) { if ((permissions & flag) === flag)
userPermissions.push({ userPermissions.push({
permission: getPermissionString(permission), permission: getPermissionString(permission),
roleName: name,
roleColor: colorString || "var(--primary-300)", roleColor: colorString || "var(--primary-300)",
rolePosition: position rolePosition: position
}); });
break;
}
}
}
userPermissions.sort((a, b) => b.rolePosition - a.rolePosition); userPermissions.sort((a, b) => b.rolePosition - a.rolePosition);
return [rolePermissions, userPermissions]; return [rolePermissions, userPermissions];
}, [stns.permissionsSortOrder]); }, [permissionsSortOrder]);
return ( return (
<ExpandableHeader <ExpandableHeader
@ -122,13 +157,15 @@ function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: Use
onDropDownClick={state => { settings.store.defaultPermissionsDropdownState = !state; }} onDropDownClick={state => { settings.store.defaultPermissionsDropdownState = !state; }}
defaultState={settings.store.defaultPermissionsDropdownState} defaultState={settings.store.defaultPermissionsDropdownState}
buttons={[ buttons={[
<Tooltip text={`Sorting by ${stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}> <Tooltip text={`Sorting by ${permissionsSortOrder === PermissionsSortOrder.HighestRole ? "Highest Role" : "Lowest Role"}`}>
{tooltipProps => ( {tooltipProps => (
<button <div
{...tooltipProps} {...tooltipProps}
className={cl("userperms-sortorder-btn")} className={cl("user-sortorder-btn")}
role="button"
tabIndex={0}
onClick={() => { onClick={() => {
stns.permissionsSortOrder = stns.permissionsSortOrder === PermissionsSortOrder.HighestRole settings.store.permissionsSortOrder = permissionsSortOrder === PermissionsSortOrder.HighestRole
? PermissionsSortOrder.LowestRole ? PermissionsSortOrder.LowestRole
: PermissionsSortOrder.HighestRole; : PermissionsSortOrder.HighestRole;
}} }}
@ -138,34 +175,27 @@ function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: Use
height="20" height="20"
viewBox="0 96 960 960" viewBox="0 96 960 960"
fill="var(--text-normal)" fill="var(--text-normal)"
transform={stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"} transform={permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
> >
<path d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" /> <path d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" />
</svg> </svg>
</button> </div>
)} )}
</Tooltip> </Tooltip>
]} ]}
> >
{userPermissions.length > 0 && ( {userPermissions.length > 0 && (
<div className={classes(RoleRootClasses.root)}> <div className={classes(RoleRootClasses.root)}>
{userPermissions.map(({ permission, roleColor }) => ( {userPermissions.map(({ permission, roleColor, roleName }) => (
<div className={classes(RoleClasses.role)}> <Tooltip
<div className={RoleClasses.roleRemoveButton}> text={<GrantedByTooltip roleName={roleName} roleColor={roleColor} />}
<span tooltipClassName={cl("granted-by-container")}
className={classes(RoleBorderClasses.roleCircle, RoleClasses.roleCircle)} tooltipContentClassName={cl("granted-by-content")}
style={{ backgroundColor: roleColor }} >
/> {tooltipProps => (
</div> <FakeRole {...tooltipProps} text={permission} color={roleColor} />
<div className={RoleClasses.roleName}> )}
<Text </Tooltip>
className={RoleClasses.roleNameOverflow}
variant="text-xs/medium"
>
{permission}
</Text>
</div>
</div>
))} ))}
</div> </div>
)} )}

View file

@ -28,6 +28,7 @@ import definePlugin, { OptionType } from "@utils/types";
import type { GuildMember, GuildRecord } from "@vencord/discord-types"; import type { GuildMember, GuildRecord } from "@vencord/discord-types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, Menu, Permissions, Popout, TooltipContainer, UserStore } from "@webpack/common"; import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, Menu, Permissions, Popout, TooltipContainer, UserStore } from "@webpack/common";
import type { ReactElement } from "react";
import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermission } from "./components/RolesAndUsersPermissions"; import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermission } from "./components/RolesAndUsersPermissions";
import UserPermissions from "./components/UserPermissions"; import UserPermissions from "./components/UserPermissions";
@ -54,12 +55,12 @@ export const settings = definePluginSettings({
options: [ options: [
{ label: "Highest Role", value: PermissionsSortOrder.HighestRole, default: true }, { label: "Highest Role", value: PermissionsSortOrder.HighestRole, default: true },
{ label: "Lowest Role", value: PermissionsSortOrder.LowestRole } { label: "Lowest Role", value: PermissionsSortOrder.LowestRole }
], ]
}, },
defaultPermissionsDropdownState: { defaultPermissionsDropdownState: {
description: "Whether the permissions dropdown on user popouts should be open by default", description: "Whether the permissions dropdown on user popouts should be open by default",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: false, default: false
} }
}); });
@ -80,11 +81,10 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
case MenuItemParentType.User: { case MenuItemParentType.User: {
const member = GuildMemberStore.getMember(guildId, id!)!; const member = GuildMemberStore.getMember(guildId, id!)!;
permissions = getSortedRoles(guild, member) permissions = getSortedRoles(guild, member).map(role => ({
.map(role => ({ type: PermissionType.Role,
type: PermissionType.Role, ...role
...role }));
}));
if (guild.ownerId === id) { if (guild.ownerId === id) {
permissions.push({ permissions.push({
@ -97,32 +97,30 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
break; break;
} }
case MenuItemParentType.Channel: { case MenuItemParentType.Channel: {
const channel = ChannelStore.getChannel(id)!; const channel = ChannelStore.getChannel(id)!;
permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({ permissions = sortPermissionOverwrites(
type: type as number as PermissionType, Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({
id, type: type as number as PermissionType,
overwriteAllow: allow, id,
overwriteDeny: deny overwriteAllow: allow,
})), guildId); overwriteDeny: deny
})),
guildId
);
header = channel.name; header = channel.name;
break; break;
} }
default:
default: {
permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({ permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
type: PermissionType.Role, type: PermissionType.Role,
...role ...role
})); }));
header = guild.name; header = guild.name;
break;
}
} }
openRolesAndUsersPermissionsModal(permissions, guild, header); openRolesAndUsersPermissionsModal(permissions, guild, header);
@ -133,31 +131,39 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
const makeContextMenuPatch = (childId: string | string[], type?: MenuItemParentType) => const makeContextMenuPatch = (childId: string | string[], type?: MenuItemParentType) =>
((children, props) => { ((children, props) => {
if (!props) return; if (
if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild))) !props ||
return; (type === MenuItemParentType.User && !props.user) ||
(type === MenuItemParentType.Guild && !props.guild) ||
(type === MenuItemParentType.Channel && (!props.channel || !props.guild))
) return;
const group = findGroupChildrenByChildId(childId, children); const group = findGroupChildrenByChildId(childId, children);
const item = (() => { let item: ReactElement | null;
switch (type) { switch (type) {
case MenuItemParentType.User: case MenuItemParentType.User:
return MenuItem(props.guildId, props.user.id, type); item = MenuItem(props.guildId, props.user.id, type);
case MenuItemParentType.Channel: break;
return MenuItem(props.guild.id, props.channel.id, type); case MenuItemParentType.Channel:
case MenuItemParentType.Guild: item = MenuItem(props.guild.id, props.channel.id, type);
return MenuItem(props.guild.id); break;
default: case MenuItemParentType.Guild:
return null; item = MenuItem(props.guild.id);
} break;
})(); default:
return;
}
if (item == null) return; if (item === null) return;
if (group) if (group) {
group.push(item); group.push(item);
else if (childId === "roles" && props.guildId) return;
// "roles" may not be present due to the member not having any roles. In that case, add it above "Copy ID" }
// "roles" may not be present due to the member not having any roles. In that case, add it above "Copy ID"
if (childId === "roles" && props.guildId)
children.splice(-1, 0, <Menu.MenuGroup>{item}</Menu.MenuGroup>); children.splice(-1, 0, <Menu.MenuGroup>{item}</Menu.MenuGroup>);
}) satisfies NavContextMenuPatchCallback; }) satisfies NavContextMenuPatchCallback;

View file

@ -1,20 +1,6 @@
/* User Permissions Component */ /* User Permissions Component */
.vc-permviewer-userperms-title-container { .vc-permviewer-user-sortorder-btn {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
margin-bottom: 6px;
}
.vc-permviewer-userperms-btns-container {
display: flex;
align-items: center;
}
.vc-permviewer-userperms-sortorder-btn {
all: unset;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@ -23,27 +9,17 @@
height: 24px; height: 24px;
} }
.vc-permviewer-userperms-permdetails-btn {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
}
.vc-permviewer-userperms-toggleperms-btn {
all: unset;
cursor: pointer;
display: flex;
align-items: center;
}
/* RolesAndUsersPermissions Component */ /* RolesAndUsersPermissions Component */
.vc-permviewer-perms-title { .vc-permviewer-modal-content {
padding: 16px 4px 16px 16px;
}
.vc-permviewer-modal-title {
flex-grow: 1; flex-grow: 1;
} }
.vc-permviewer-perms-no-perms { .vc-permviewer-modal-no-perms {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
@ -52,101 +28,103 @@
text-align: center; text-align: center;
} }
.vc-permviewer-perms-container { .vc-permviewer-modal-container {
display: grid; width: 100%;
grid-template-columns: 1fr 2fr; height: 100%;
grid-template-areas: "list permissions"; display: flex;
padding: 16px 0; gap: 8px;
} }
.vc-permviewer-perms-list { .vc-permviewer-modal-list {
grid-area: list;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
border-right: 2px solid var(--background-modifier-active); padding-right: 8px;
width: 200px;
} }
.vc-permviewer-perms-list-item-btn { .vc-permviewer-modal-list-item-btn {
all: unset;
cursor: pointer; cursor: pointer;
} }
.vc-permviewer-perms-list-item { .vc-permviewer-modal-list-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8px 5px; gap: 8px;
cursor: pointer; padding: 8px;
width: 230px;
border-radius: 5px; border-radius: 5px;
} }
.vc-permviewer-perms-list-item:hover { .vc-permviewer-modal-list-item:hover {
background-color: var(--background-modifier-hover); background-color: var(--background-modifier-hover);
} }
.vc-permviewer-perms-list-item-active { .vc-permviewer-modal-list-item-active {
background-color: var(--background-modifier-selected); background-color: var(--background-modifier-selected);
} }
.vc-permviewer-perms-list-item > div { .vc-permviewer-modal-list-item > div {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.vc-permviewer-perms-role-circle { .vc-permviewer-modal-role-circle {
border-radius: 50%; border-radius: 50%;
width: 12px; width: 12px;
height: 12px; height: 12px;
margin-left: 3px;
margin-right: 11px;
flex-shrink: 0; flex-shrink: 0;
} }
.vc-permviewer-perms-user-img { .vc-permviewer-modal-role-image {
width: 20px;
height: 20px;
object-fit: contain;
}
.vc-permviewer-modal-user-img {
border-radius: 50%; border-radius: 50%;
width: 20px; width: 20px;
height: 20px; height: 20px;
margin-right: 6px;
} }
.vc-permviewer-perms-perms { .vc-permviewer-modal-divider {
grid-area: permissions; width: 2px;
background-color: var(--background-modifier-active);
}
.vc-permviewer-modal-perms {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-left: 5px; padding-right: 8px;
} }
.vc-permviewer-perms-perms-item { .vc-permviewer-modal-perms-item {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 10px; gap: 5px;
padding: 10px 2px 10px 10px;
border-bottom: 2px solid var(--background-modifier-active); border-bottom: 2px solid var(--background-modifier-active);
} }
.vc-permviewer-perms-perms-item:last-child { .vc-permviewer-modal-perms-item:last-child {
border: 0; border: 0;
} }
.vc-permviewer-perms-perms-item-icon { .vc-permviewer-modal-perms-item-icon {
border: 1px solid var(--background-modifier-selected); border: 1px solid var(--background-modifier-selected);
width: 24px; width: 24px;
height: 24px; height: 24px;
margin-right: 5px;
} }
.vc-permviewer-perms-perms-item .vc-info-icon { .vc-permviewer-modal-perms-item .vc-info-icon {
color: var(--interactive-muted); color: var(--interactive-muted);
margin-left: auto;
cursor: pointer; cursor: pointer;
position: absolute;
right: 0;
scale: 0.9;
transition: color ease-in 0.1s; transition: color ease-in 0.1s;
} }
.vc-permviewer-perms-perms-item .vc-info-icon:hover { .vc-permviewer-modal-perms-item .vc-info-icon:hover {
color: var(--interactive-active); color: var(--interactive-active);
} }
@ -167,3 +145,14 @@
background: rgb(var(--bg-overlay-color) / var(--bg-overlay-opacity-6)); background: rgb(var(--bg-overlay-color) / var(--bg-overlay-opacity-6));
border-color: var(--profile-body-border-color); border-color: var(--profile-body-border-color);
} }
.vc-permviewer-granted-by-container {
max-width: 300px;
width: auto;
}
.vc-permviewer-granted-by-content {
display: flex;
align-items: center;
gap: 4px;
}

View file

@ -57,7 +57,7 @@ export default definePlugin({
}, },
{ {
match: /text:\i\.\i.Messages.USER_PROFILE_PRONOUNS/, match: /text:\i\.\i.Messages.USER_PROFILE_PRONOUNS/,
replace: '$&+vcHasPendingPronouns?"":` (${vcPronounSource})`' replace: '$&+(vcHasPendingPronouns?"":` (${vcPronounSource})`)'
}, },
{ {
match: /(\.pronounsText.+?children:)(\i)/, match: /(\.pronounsText.+?children:)(\i)/,

View file

@ -22,12 +22,13 @@ import { useForceUpdater } from "@utils/react";
import { Paginator, Text, useRef, useState } from "@webpack/common"; import { Paginator, Text, useRef, useState } from "@webpack/common";
import { Auth } from "../auth"; import { Auth } from "../auth";
import type { ReviewType } from "../entities";
import { type Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; import { type Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { cl } from "../utils"; import { cl } from "../utils";
import ReviewComponent from "./ReviewComponent"; import ReviewComponent from "./ReviewComponent";
import ReviewsView, { ReviewsInputComponent } from "./ReviewsView"; import ReviewsView, { ReviewsInputComponent } from "./ReviewsView";
function Modal({ discordId, modalKey, modalProps, name }: { discordId: string; modalKey: string; modalProps: ModalProps; name: string; }) { function Modal({ discordId, modalKey, modalProps, name, type }: { discordId: string; modalKey: string; modalProps: ModalProps; name: string; type: ReviewType; }) {
const [data, setData] = useState<Response>(); const [data, setData] = useState<Response>();
const [signal, refetch] = useForceUpdater(true); const [signal, refetch] = useForceUpdater(true);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@ -58,6 +59,7 @@ function Modal({ discordId, modalKey, modalProps, name }: { discordId: string; m
onFetchReviews={setData} onFetchReviews={setData}
scrollToTop={() => { ref.current?.scrollTo({ top: 0, behavior: "smooth" }); }} scrollToTop={() => { ref.current?.scrollTo({ top: 0, behavior: "smooth" }); }}
hideOwnReview hideOwnReview
type={type}
/> />
</div> </div>
</ModalContent> </ModalContent>
@ -95,7 +97,7 @@ function Modal({ discordId, modalKey, modalProps, name }: { discordId: string; m
); );
} }
export function openReviewsModal(discordId: string, name: string) { export function openReviewsModal(discordId: string, name: string, type: ReviewType) {
const modalKey = "vc-rdb-modal-" + Date.now(); const modalKey = "vc-rdb-modal-" + Date.now();
openModal(props => ( openModal(props => (
@ -104,6 +106,7 @@ export function openReviewsModal(discordId: string, name: string) {
modalProps={props} modalProps={props}
discordId={discordId} discordId={discordId}
name={name} name={name}
type={type}
/> />
), { modalKey }); ), { modalKey });
} }

View file

@ -22,7 +22,7 @@ import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpa
import { Forms, RelationshipStore, useRef, UserStore } from "@webpack/common"; import { Forms, RelationshipStore, useRef, UserStore } from "@webpack/common";
import { Auth, authorize } from "../auth"; import { Auth, authorize } from "../auth";
import type { Review } from "../entities"; import { type Review, ReviewType } from "../entities";
import { addReview, getReviews, type Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; import { addReview, getReviews, type Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { settings } from "../settings"; import { settings } from "../settings";
import { cl, showToast } from "../utils"; import { cl, showToast } from "../utils";
@ -46,6 +46,7 @@ interface ReviewsViewProps extends UserProps {
refetchSignal?: unknown; refetchSignal?: unknown;
scrollToTop?: () => void; scrollToTop?: () => void;
showInput?: boolean; showInput?: boolean;
type: ReviewType;
} }
export default function ReviewsView({ export default function ReviewsView({
@ -57,6 +58,7 @@ export default function ReviewsView({
page = 1, page = 1,
showInput = false, showInput = false,
hideOwnReview = false, hideOwnReview = false,
type,
}: ReviewsViewProps) { }: ReviewsViewProps) {
const [signal, refetch] = useForceUpdater(true); const [signal, refetch] = useForceUpdater(true);
@ -81,6 +83,7 @@ export default function ReviewsView({
reviews={reviewData.reviews} reviews={reviewData.reviews}
hideOwnReview={hideOwnReview} hideOwnReview={hideOwnReview}
profileId={discordId} profileId={discordId}
type={type}
/> />
{showInput && ( {showInput && (
@ -100,9 +103,10 @@ interface ReviewListProps {
profileId: string; profileId: string;
refetch: () => void; refetch: () => void;
reviews: Review[]; reviews: Review[];
type: ReviewType;
} }
function ReviewList({ hideOwnReview, profileId, refetch, reviews }: ReviewListProps) { function ReviewList({ hideOwnReview, profileId, refetch, reviews, type }: ReviewListProps) {
const meId = UserStore.getCurrentUser()!.id; const meId = UserStore.getCurrentUser()!.id;
return ( return (
@ -120,7 +124,7 @@ function ReviewList({ hideOwnReview, profileId, refetch, reviews }: ReviewListPr
{reviews.length === 0 && ( {reviews.length === 0 && (
<Forms.FormText className={cl("placeholder")}> <Forms.FormText className={cl("placeholder")}>
Looks like nobody reviewed this user yet. You could be the first! Looks like nobody reviewed this {type === ReviewType.User ? "user" : "server"} yet. You could be the first!
</Forms.FormText> </Forms.FormText>
)} )}
</div> </div>

View file

@ -30,7 +30,7 @@ import { AlertActionCreators, Button, MarkupUtils, Menu, TooltipContainer } from
import { Auth, initAuth, updateAuth } from "./auth"; import { Auth, initAuth, updateAuth } from "./auth";
import { openReviewsModal } from "./components/ReviewModal"; import { openReviewsModal } from "./components/ReviewModal";
import { NotificationType } from "./entities"; import { NotificationType, ReviewType } from "./entities";
import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings"; import { settings } from "./settings";
import { showToast } from "./utils"; import { showToast } from "./utils";
@ -38,28 +38,28 @@ import { showToast } from "./utils";
const RoleButtonClasses: Record<string, string> = findByPropsLazy("button", "buttonInner", "icon", "banner"); const RoleButtonClasses: Record<string, string> = findByPropsLazy("button", "buttonInner", "icon", "banner");
const guildPopoutPatch = ((children, { guild }: { guild?: GuildRecord; onClose: () => void; }) => { const guildPopoutPatch = ((children, { guild }: { guild?: GuildRecord; onClose: () => void; }) => {
if (!guild) return; if (guild)
children.push( children.push(
<Menu.MenuItem <Menu.MenuItem
label="View Reviews" label="View Reviews"
id="vc-rdb-server-reviews" id="vc-rdb-server-reviews"
icon={OpenExternalIcon} icon={OpenExternalIcon}
action={() => { openReviewsModal(guild.id, guild.name); }} action={() => { openReviewsModal(guild.id, guild.name, ReviewType.Server); }}
/> />
); );
}) satisfies NavContextMenuPatchCallback; }) satisfies NavContextMenuPatchCallback;
const userContextPatch = ((children, { user }: { user?: UserRecord; onClose: () => void; }) => { const userContextPatch: NavContextMenuPatchCallback = (children, { user }: { user?: UserRecord; onClose: () => void; }) => {
if (user) if (user)
children.push( children.push(
<Menu.MenuItem <Menu.MenuItem
label="View Reviews" label="View Reviews"
id="vc-rdb-user-reviews" id="vc-rdb-user-reviews"
icon={OpenExternalIcon} icon={OpenExternalIcon}
action={() => { openReviewsModal(user.id, user.username); }} action={() => { openReviewsModal(user.id, user.username, ReviewType.User); }}
/> />
); );
}) satisfies NavContextMenuPatchCallback; };
export default definePlugin({ export default definePlugin({
name: "ReviewDB", name: "ReviewDB",
@ -158,7 +158,7 @@ export default definePlugin({
return ( return (
<TooltipContainer text="View Reviews"> <TooltipContainer text="View Reviews">
<Button <Button
onClick={() => { openReviewsModal(user.id, user.username); }} onClick={() => { openReviewsModal(user.id, user.username, ReviewType.User); }}
look={Button.Looks.FILLED} look={Button.Looks.FILLED}
size={Button.Sizes.NONE} size={Button.Sizes.NONE}
color={RoleButtonClasses.bannerColor} color={RoleButtonClasses.bannerColor}

View file

@ -69,7 +69,7 @@ export default definePlugin({
// Patches needed for web/vesktop // Patches needed for web/vesktop
{ {
find: "streamSourceNode", find: "streamSourceNode",
predicate: () => IS_WEB, predicate: () => !IS_DISCORD_DESKTOP,
group: true, group: true,
replacement: [ replacement: [
// Remove rounding algorithm // Remove rounding algorithm

View file

@ -17,6 +17,7 @@
*/ */
import { Settings, SettingsStore } from "@api/Settings"; import { Settings, SettingsStore } from "@api/Settings";
import { ThemeStore } from "@webpack/common";
let style: HTMLStyleElement | undefined; let style: HTMLStyleElement | undefined;
@ -32,11 +33,8 @@ function createStyle(id: string) {
async function initSystemValues() { async function initSystemValues() {
const values = await VencordNative.themes.getSystemValues(); const values = await VencordNative.themes.getSystemValues();
let variables = ""; let variables = "";
for (const k in values) { for (const [k, v] of Object.entries(values))
const v = values[k]; if (v !== "#") variables += `--${k}: ${v};`;
if (v !== "#")
variables += `--${k}: ${v};`;
}
createStyle("vencord-os-theme-values").textContent = `:root{${variables}}`; createStyle("vencord-os-theme-values").textContent = `:root{${variables}}`;
} }
@ -61,7 +59,17 @@ async function initThemes() {
const { themeLinks, enabledThemes } = Settings; const { themeLinks, enabledThemes } = Settings;
const links = [...themeLinks]; // "darker" and "midnight" both count as dark
const activeTheme = ThemeStore.theme === "light" ? "light" : "dark";
const links: string[] = [];
for (const rawLink of themeLinks) {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (match) {
const [, mode, link] = match;
if (mode === activeTheme) links.push(link!);
} else links.push(rawLink);
}
if (IS_WEB) { if (IS_WEB) {
for (const theme of enabledThemes) { for (const theme of enabledThemes) {
@ -87,6 +95,7 @@ document.addEventListener("DOMContentLoaded", () => {
SettingsStore.addChangeListener("themeLinks", initThemes); SettingsStore.addChangeListener("themeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes); SettingsStore.addChangeListener("enabledThemes", initThemes);
ThemeStore.addChangeListener(initThemes);
if (!IS_WEB) if (!IS_WEB)
VencordNative.quickCss.addThemeChangeListener(initThemes); VencordNative.quickCss.addThemeChangeListener(initThemes);

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type { FetchStoreFactory, Flux as $Flux, StoreArrayStateHook, StoreObjectStateHook, StoreStateHook, UnequatableStateComparator } from "@vencord/discord-types"; import type { FetchStoreFactory, Flux as $Flux, PersistedStore, StoreArrayStateHook, StoreObjectStateHook, StoreStateHook, UnequatableStateComparator } from "@vencord/discord-types";
import type * as Stores from "@vencord/discord-types/src/stores"; import type * as Stores from "@vencord/discord-types/src/stores";
// eslint-disable-next-line path-alias/no-relative // eslint-disable-next-line path-alias/no-relative
@ -103,6 +103,9 @@ waitForStore("SelectedChannelStore", m => { SelectedChannelStore = m; });
export let SelectedGuildStore: Stores.SelectedGuildStore; export let SelectedGuildStore: Stores.SelectedGuildStore;
waitForStore("SelectedGuildStore", m => { SelectedGuildStore = m; }); waitForStore("SelectedGuildStore", m => { SelectedGuildStore = m; });
export let ThemeStore: PersistedStore & Record<string, any>;
waitForStore("ThemeStore", m => { ThemeStore = m; });
export let UserProfileStore: Stores.UserProfileStore; export let UserProfileStore: Stores.UserProfileStore;
waitForStore("UserProfileStore", m => { UserProfileStore = m; }); waitForStore("UserProfileStore", m => { UserProfileStore = m; });

View file

@ -459,7 +459,7 @@ export type ScrollerThin = ComponentType<PropsWithChildren<{
style?: CSSProperties; style?: CSSProperties;
dir?: "ltr"; dir?: "ltr";
orientation?: "horizontal" | "vertical"; orientation?: "horizontal" | "vertical" | "auto";
paddingFix?: boolean; paddingFix?: boolean;
fade?: boolean; fade?: boolean;