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(/^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(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
...commonOpts.plugins
];

View file

@ -83,7 +83,16 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<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>
<div>
{themeLinks.map(link => (
{themeLinks.map(rawLink => {
const { label, link } = (() => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return { label: rawLink, link: rawLink };
const [, mode, link] = match;
return { label: `[${mode} mode only] ${link}`, link };
})();
return (
<Card
style={{
padding: ".5em",
@ -96,11 +105,12 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
tag="h5"
style={{ overflowWrap: "break-word" }}
>
{link}
{label}
</Forms.FormTitle>
<Validator link={link} />
<Validator link={link!} />
</Card>
))}
);
})}
</div>
</>
);
@ -297,6 +307,7 @@ function ThemesTab() {
<Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
<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>
</Card>

View file

@ -14,7 +14,7 @@ import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import type { PersistedStore } from "@vencord/discord-types";
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)");
@ -38,7 +38,6 @@ function setTheme(theme: string) {
saveClientTheme({ theme });
}
const ThemeStore: PersistedStore & Record<string, any> = findStoreLazy("ThemeStore");
const ClientThemesBackgroundStore: PersistedStore & Record<string, any> = findStoreLazy("ClientThemesBackgroundStore");
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({
name: "IgnoreActivities",
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"],
settings,
@ -275,6 +275,7 @@ export default definePlugin({
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",
replacement: {
@ -288,6 +289,13 @@ export default definePlugin({
match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/,
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 definePlugin from "@utils/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 type { ReactNode } from "react";
const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
const ExpandableList = findComponentByCodeLazy(".mutualFriendItem]");
const ProfileListClasses: Record<string, string> = findByPropsLazy("emptyIconFriends", "emptyIconGuilds");
const GuildLabelClasses: Record<string, string> = findByPropsLazy("guildNick", "guildAvatarWithoutIcon");
@ -54,9 +56,30 @@ function getMutualGDMCountText(user: UserRecord) {
for (const channel of Object.values(ChannelStore.getMutablePrivateChannels()))
if (channel.isGroupDM() && channel.recipients.includes(user.id))
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");
export default definePlugin({
@ -77,6 +100,13 @@ export default definePlugin({
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; }) => {
const mutualGroupDMs = useMutualGroupDMs(user.id);
const entries = mutualGroupDMs.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 entries = renderClickableGDMs(mutualGroupDMs, onClose);
return (
<ScrollerThin
@ -130,5 +142,25 @@ export default definePlugin({
}
</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 { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, type ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import type { GuildRecord } from "@vencord/discord-types";
import { ClipboardUtils, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, Permissions, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { GuildRecord, Role, UnicodeEmoji } from "@vencord/discord-types";
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 { cl, getPermissionDescription, getPermissionString } from "../utils";
@ -42,15 +43,17 @@ export interface RoleOrUserPermission {
overwriteDeny?: bigint;
}
function openRolesAndUsersPermissionsModal(permissions: RoleOrUserPermission[], guild: GuildRecord, header: string) {
return openModal(modalProps => (
<RolesAndUsersPermissions
modalProps={modalProps}
permissions={permissions}
guild={guild}
header={header}
/>
));
const getRoleIconData: {
(role: Role, size?: number | null): {
customIconSrc: string | undefined;
unicodeEmoji: UnicodeEmoji | undefined;
};
(role?: null, size?: number | null): null;
} = 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; }) {
@ -86,31 +89,34 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
size={ModalSize.LARGE}
>
<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} />
</ModalHeader>
<ModalContent>
<ModalContent className={cl("modal-content")}>
{!selectedItem && (
<div className={cl("perms-no-perms")}>
<div className={cl("modal-no-perms")}>
<Text variant="heading-lg/normal">No permissions to display!</Text>
</div>
)}
{selectedItem && (
<div className={cl("perms-container")}>
<div className={cl("perms-list")}>
<div className={cl("modal-container")}>
<ScrollerThin className={cl("modal-list")} orientation="auto">
{permissions.map((permission, index) => {
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 (
<button
className={cl("perms-list-item-btn")}
<div
className={cl("modal-list-item-btn")}
onClick={() => { selectItem(index); }}
role="button"
tabIndex={0}
>
<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 => {
if (permission.type === PermissionType.Role)
ContextMenuApi.openContextMenu(e, () => (
@ -129,13 +135,19 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
>
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
<span
className={cl("perms-role-circle")}
className={cl("modal-role-circle")}
style={{ backgroundColor: role?.colorString ?? "var(--primary-300)" }}
/>
)}
{permission.type === PermissionType.User && user !== undefined && (
{permission.type === PermissionType.Role && roleIconSrc != null && (
<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()}
/>
)}
@ -147,24 +159,21 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
: (
<Flex style={{ gap: "0.2em", justifyItems: "center" }}>
@owner
<OwnerCrownIcon
height={18}
width={18}
aria-hidden="true"
/>
<OwnerCrownIcon height={18} width={18} aria-hidden="true" />
</Flex>
)
}
</Text>
</div>
</button>
</div>
);
})}
</div>
<div className={cl("perms-perms")}>
</ScrollerThin>
<div className={cl("modal-divider")} />
<ScrollerThin className={cl("modal-perms")} orientation="auto">
{Object.entries(Permissions).map(([permissionName, flag]) => (
<div className={cl("perms-perms-item")}>
<div className={cl("perms-perms-item-icon")}>
<div className={cl("modal-perms-item")}>
<div className={cl("modal-perms-item-icon")}>
{(() => {
const { permissions, overwriteAllow, overwriteDeny } = selectedItem;
@ -188,7 +197,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
</Tooltip>
</div>
))}
</div>
</ScrollerThin>
</div>
)}
</ModalContent>
@ -204,7 +213,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: GuildRecord; roleI
aria-label="Role Options"
>
<Menu.MenuItem
id="vc-copy-role-id"
id={cl("copy-role-id")}
label={i18n.Messages.COPY_ID_ROLE}
action={() => {
ClipboardUtils.copy(roleId);
@ -213,14 +222,13 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: GuildRecord; roleI
{(settings.store as any).unsafeViewAsRole && (
<Menu.MenuItem
id="vc-pw-view-as-role"
id={cl("view-as-role")}
label={i18n.Messages.VIEW_AS_ROLE}
action={() => {
const role = GuildStore.getRole(guild.id, roleId);
if (!role) return;
onClose();
FluxDispatcher.dispatch({
type: "IMPERSONATE_UPDATE",
guildId: guild.id,
@ -231,8 +239,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: GuildRecord; roleI
}
}
});
}
}
}}
/>
)}
</Menu.Menu>
@ -247,7 +254,7 @@ function UserContextMenu({ userId }: { userId: string; }) {
aria-label="User Options"
>
<Menu.MenuItem
id="vc-copy-user-id"
id={cl("copy-user-id")}
label={i18n.Messages.COPY_ID_USER}
action={() => {
ClipboardUtils.copy(userId);
@ -259,4 +266,13 @@ function UserContextMenu({ userId }: { userId: string; }) {
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 { filters, findBulk, proxyLazyWebpack } from "@webpack";
import { i18n, Permissions, Text, Tooltip, useMemo, UserStore } from "@webpack/common";
import type { HTMLAttributes } from "react";
import type { BuildTuple } from "type-fest/source/internal";
import { PermissionsSortOrder, settings } from "..";
@ -30,6 +31,7 @@ import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermi
interface UserPermission {
permission: string;
roleName: string;
roleColor: string;
rolePosition: number;
}
@ -53,6 +55,42 @@ const { RoleRootClasses, RoleClasses, RoleBorderClasses } = proxyLazyWebpack(()
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 {
forceOpen?: boolean;
guild: GuildRecord;
@ -60,7 +98,7 @@ interface UserPermissionsComponentProps {
}
function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: UserPermissionsComponentProps) {
const stns = settings.use(["permissionsSortOrder"]);
const { permissionsSortOrder } = settings.use(["permissionsSortOrder"]);
const [rolePermissions, userPermissions] = useMemo(() => {
const userPermissions: UserPermission[] = [];
@ -81,6 +119,7 @@ function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: Use
const OWNER = i18n.Messages.GUILD_OWNER || "Server Owner";
userPermissions.push({
permission: OWNER,
roleName: "Owner",
roleColor: "var(--primary-300)",
rolePosition: Infinity
});
@ -88,24 +127,20 @@ function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: Use
sortUserRoles(userRoles);
for (const [permission, flag] of Object.entries(Permissions)) {
for (const { permissions, colorString, position } of userRoles) {
if ((permissions & flag) === flag) {
for (const [permission, flag] of Object.entries(Permissions))
for (const { permissions, colorString, position, name } of userRoles)
if ((permissions & flag) === flag)
userPermissions.push({
permission: getPermissionString(permission),
roleName: name,
roleColor: colorString || "var(--primary-300)",
rolePosition: position
});
break;
}
}
}
userPermissions.sort((a, b) => b.rolePosition - a.rolePosition);
return [rolePermissions, userPermissions];
}, [stns.permissionsSortOrder]);
}, [permissionsSortOrder]);
return (
<ExpandableHeader
@ -122,13 +157,15 @@ function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: Use
onDropDownClick={state => { settings.store.defaultPermissionsDropdownState = !state; }}
defaultState={settings.store.defaultPermissionsDropdownState}
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 => (
<button
<div
{...tooltipProps}
className={cl("userperms-sortorder-btn")}
className={cl("user-sortorder-btn")}
role="button"
tabIndex={0}
onClick={() => {
stns.permissionsSortOrder = stns.permissionsSortOrder === PermissionsSortOrder.HighestRole
settings.store.permissionsSortOrder = permissionsSortOrder === PermissionsSortOrder.HighestRole
? PermissionsSortOrder.LowestRole
: PermissionsSortOrder.HighestRole;
}}
@ -138,34 +175,27 @@ function UserPermissionsComponent({ forceOpen = false, guild, guildMember }: Use
height="20"
viewBox="0 96 960 960"
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" />
</svg>
</button>
</div>
)}
</Tooltip>
]}
>
{userPermissions.length > 0 && (
<div className={classes(RoleRootClasses.root)}>
{userPermissions.map(({ permission, roleColor }) => (
<div className={classes(RoleClasses.role)}>
<div className={RoleClasses.roleRemoveButton}>
<span
className={classes(RoleBorderClasses.roleCircle, RoleClasses.roleCircle)}
style={{ backgroundColor: roleColor }}
/>
</div>
<div className={RoleClasses.roleName}>
<Text
className={RoleClasses.roleNameOverflow}
variant="text-xs/medium"
{userPermissions.map(({ permission, roleColor, roleName }) => (
<Tooltip
text={<GrantedByTooltip roleName={roleName} roleColor={roleColor} />}
tooltipClassName={cl("granted-by-container")}
tooltipContentClassName={cl("granted-by-content")}
>
{permission}
</Text>
</div>
</div>
{tooltipProps => (
<FakeRole {...tooltipProps} text={permission} color={roleColor} />
)}
</Tooltip>
))}
</div>
)}

View file

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

View file

@ -1,20 +1,6 @@
/* User Permissions Component */
.vc-permviewer-userperms-title-container {
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;
.vc-permviewer-user-sortorder-btn {
cursor: pointer;
display: flex;
align-items: center;
@ -23,27 +9,17 @@
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 */
.vc-permviewer-perms-title {
.vc-permviewer-modal-content {
padding: 16px 4px 16px 16px;
}
.vc-permviewer-modal-title {
flex-grow: 1;
}
.vc-permviewer-perms-no-perms {
.vc-permviewer-modal-no-perms {
width: 100%;
height: 100%;
display: flex;
@ -52,101 +28,103 @@
text-align: center;
}
.vc-permviewer-perms-container {
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-areas: "list permissions";
padding: 16px 0;
.vc-permviewer-modal-container {
width: 100%;
height: 100%;
display: flex;
gap: 8px;
}
.vc-permviewer-perms-list {
grid-area: list;
.vc-permviewer-modal-list {
display: flex;
flex-direction: column;
gap: 2px;
border-right: 2px solid var(--background-modifier-active);
padding-right: 8px;
width: 200px;
}
.vc-permviewer-perms-list-item-btn {
all: unset;
.vc-permviewer-modal-list-item-btn {
cursor: pointer;
}
.vc-permviewer-perms-list-item {
.vc-permviewer-modal-list-item {
display: flex;
align-items: center;
padding: 8px 5px;
cursor: pointer;
width: 230px;
gap: 8px;
padding: 8px;
border-radius: 5px;
}
.vc-permviewer-perms-list-item:hover {
.vc-permviewer-modal-list-item: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);
}
.vc-permviewer-perms-list-item > div {
.vc-permviewer-modal-list-item > div {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.vc-permviewer-perms-role-circle {
.vc-permviewer-modal-role-circle {
border-radius: 50%;
width: 12px;
height: 12px;
margin-left: 3px;
margin-right: 11px;
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%;
width: 20px;
height: 20px;
margin-right: 6px;
}
.vc-permviewer-perms-perms {
grid-area: permissions;
.vc-permviewer-modal-divider {
width: 2px;
background-color: var(--background-modifier-active);
}
.vc-permviewer-modal-perms {
display: flex;
flex-direction: column;
margin-left: 5px;
padding-right: 8px;
}
.vc-permviewer-perms-perms-item {
position: relative;
.vc-permviewer-modal-perms-item {
display: flex;
align-items: center;
padding: 10px;
gap: 5px;
padding: 10px 2px 10px 10px;
border-bottom: 2px solid var(--background-modifier-active);
}
.vc-permviewer-perms-perms-item:last-child {
.vc-permviewer-modal-perms-item:last-child {
border: 0;
}
.vc-permviewer-perms-perms-item-icon {
.vc-permviewer-modal-perms-item-icon {
border: 1px solid var(--background-modifier-selected);
width: 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);
margin-left: auto;
cursor: pointer;
position: absolute;
right: 0;
scale: 0.9;
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);
}
@ -167,3 +145,14 @@
background: rgb(var(--bg-overlay-color) / var(--bg-overlay-opacity-6));
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/,
replace: '$&+vcHasPendingPronouns?"":` (${vcPronounSource})`'
replace: '$&+(vcHasPendingPronouns?"":` (${vcPronounSource})`)'
},
{
match: /(\.pronounsText.+?children:)(\i)/,

View file

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

View file

@ -22,7 +22,7 @@ import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpa
import { Forms, RelationshipStore, useRef, UserStore } from "@webpack/common";
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 { settings } from "../settings";
import { cl, showToast } from "../utils";
@ -46,6 +46,7 @@ interface ReviewsViewProps extends UserProps {
refetchSignal?: unknown;
scrollToTop?: () => void;
showInput?: boolean;
type: ReviewType;
}
export default function ReviewsView({
@ -57,6 +58,7 @@ export default function ReviewsView({
page = 1,
showInput = false,
hideOwnReview = false,
type,
}: ReviewsViewProps) {
const [signal, refetch] = useForceUpdater(true);
@ -81,6 +83,7 @@ export default function ReviewsView({
reviews={reviewData.reviews}
hideOwnReview={hideOwnReview}
profileId={discordId}
type={type}
/>
{showInput && (
@ -100,9 +103,10 @@ interface ReviewListProps {
profileId: string;
refetch: () => void;
reviews: Review[];
type: ReviewType;
}
function ReviewList({ hideOwnReview, profileId, refetch, reviews }: ReviewListProps) {
function ReviewList({ hideOwnReview, profileId, refetch, reviews, type }: ReviewListProps) {
const meId = UserStore.getCurrentUser()!.id;
return (
@ -120,7 +124,7 @@ function ReviewList({ hideOwnReview, profileId, refetch, reviews }: ReviewListPr
{reviews.length === 0 && (
<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>
)}
</div>

View file

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

View file

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

View file

@ -17,6 +17,7 @@
*/
import { Settings, SettingsStore } from "@api/Settings";
import { ThemeStore } from "@webpack/common";
let style: HTMLStyleElement | undefined;
@ -32,11 +33,8 @@ function createStyle(id: string) {
async function initSystemValues() {
const values = await VencordNative.themes.getSystemValues();
let variables = "";
for (const k in values) {
const v = values[k];
if (v !== "#")
variables += `--${k}: ${v};`;
}
for (const [k, v] of Object.entries(values))
if (v !== "#") variables += `--${k}: ${v};`;
createStyle("vencord-os-theme-values").textContent = `:root{${variables}}`;
}
@ -61,7 +59,17 @@ async function initThemes() {
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) {
for (const theme of enabledThemes) {
@ -87,6 +95,7 @@ document.addEventListener("DOMContentLoaded", () => {
SettingsStore.addChangeListener("themeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes);
ThemeStore.addChangeListener(initThemes);
if (!IS_WEB)
VencordNative.quickCss.addThemeChangeListener(initThemes);

View file

@ -16,7 +16,7 @@
* 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";
// eslint-disable-next-line path-alias/no-relative
@ -103,6 +103,9 @@ waitForStore("SelectedChannelStore", m => { SelectedChannelStore = m; });
export let SelectedGuildStore: Stores.SelectedGuildStore;
waitForStore("SelectedGuildStore", m => { SelectedGuildStore = m; });
export let ThemeStore: PersistedStore & Record<string, any>;
waitForStore("ThemeStore", m => { ThemeStore = m; });
export let UserProfileStore: Stores.UserProfileStore;
waitForStore("UserProfileStore", m => { UserProfileStore = m; });

View file

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