Merge branch 'dev' into newDevTools

This commit is contained in:
sadan4 2024-09-04 20:40:00 -04:00 committed by GitHub
commit 8bcfcf7ccd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 488 additions and 323 deletions

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.9.9",
"version": "1.10.1",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {

View file

@ -337,5 +337,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

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Logger } from "@utils/Logger";
import { makeCodeblock } from "@utils/text";
import { sendBotMessage } from "./commandHelpers";
@ -46,10 +47,10 @@ export let RequiredMessageOption: Option = ReqPlaceholder;
export const _init = function (cmds: Command[]) {
try {
BUILT_IN = cmds;
OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "me")!.options![0];
} catch (e) {
console.error("Failed to load CommandsApi");
new Logger("CommandsAPI").error("Failed to load CommandsApi", e, " - cmds is", cmds);
}
return cmds;
} as never;
@ -138,6 +139,8 @@ export function registerCommand<C extends Command>(command: C, plugin: string) {
throw new Error(`Command '${command.name}' already exists.`);
command.isVencordCommand = true;
command.untranslatedName ??= command.name;
command.untranslatedDescription ??= command.description;
command.id ??= `-${BUILT_IN.length + 1}`;
command.applicationId ??= "-1"; // BUILT_IN;
command.type ??= ApplicationCommandType.CHAT_INPUT;

View file

@ -93,8 +93,10 @@ export interface Command {
isVencordCommand?: boolean;
name: string;
untranslatedName?: string;
displayName?: string;
description: string;
untranslatedDescription?: string;
displayDescription?: string;
options?: Option[];

View file

@ -77,8 +77,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 => (
<Card style={{
{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",
marginBottom: ".5em",
marginTop: ".5em"
@ -86,11 +94,11 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
<Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word"
}}>
{link}
{label}
</Forms.FormTitle>
<Validator link={link} />
</Card>
))}
</Card>;
})}
</div>
</>
);
@ -296,6 +304,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

@ -1,3 +0,0 @@
[class*="profileBadges"] {
flex: none;
}

View file

@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./fixBadgeOverflow.css";
import { _getBadges, BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
@ -79,7 +77,7 @@ export default definePlugin({
replace: "...$1.props,$& $1.image??"
},
{
match: /(?<=text:(\i)\.description,.{0,50})children:/,
match: /(?<=text:(\i)\.description,.{0,200})children:/,
replace: "children:$1.component ? $self.renderBadgeComponent({ ...$1 }) :"
},
// conditionally override their onClick with badge.onClick if it exists

View file

@ -34,7 +34,7 @@ export default definePlugin({
{
find: "Messages.SERVERS,children",
replacement: {
match: /(?<=Messages\.SERVERS,children:).+?default:return null\}\}\)/,
match: /(?<=Messages\.SERVERS,children:)\i\.map\(\i\)/,
replace: "Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($&)"
}
}

View file

@ -132,8 +132,8 @@ export default definePlugin({
},
// Export the isBetterFolders variable to the folders component
{
match: /(?<=\.Messages\.SERVERS.+?switch\((\i)\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,)/,
replace: 'isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
match: /switch\(\i\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,/,
replace: '$&isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
}
]
},

View file

@ -12,7 +12,7 @@ import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import definePlugin, { OptionType, StartAt } from "@utils/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)");
@ -36,7 +36,6 @@ function setTheme(theme: string) {
saveClientTheme({ theme });
}
const ThemeStore = findStoreLazy("ThemeStore");
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
function ThemeSettings() {

View file

@ -46,7 +46,7 @@ const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
async function embedDidMount(this: Component<Props>) {
try {
const { embed } = this.props;
const { replaceElements } = settings.store;
const { replaceElements, dearrowByDefault } = settings.store;
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
@ -63,18 +63,22 @@ async function embedDidMount(this: Component<Props>) {
if (!hasTitle && !hasThumb) return;
embed.dearrow = {
enabled: true
enabled: dearrowByDefault
};
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");
}
const replacementTitle = titles[0].title.replace(/(^|\s)>(\S)/g, "$1$2");
embed.dearrow.oldTitle = dearrowByDefault ? embed.rawTitle : replacementTitle;
if (dearrowByDefault) embed.rawTitle = replacementTitle;
}
if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
const replacementProxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
embed.dearrow.oldThumb = dearrowByDefault ? embed.thumbnail.proxyURL : replacementProxyURL;
if (dearrowByDefault) embed.thumbnail.proxyURL = replacementProxyURL;
}
this.forceUpdate();
@ -96,6 +100,7 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
onClick={() => {
const { enabled, oldThumb, oldTitle } = embed.dearrow;
settings.store.dearrowByDefault = !enabled;
embed.dearrow.enabled = !enabled;
if (oldTitle) {
embed.dearrow.oldTitle = embed.rawTitle;
@ -153,6 +158,12 @@ const settings = definePluginSettings({
{ label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
{ label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
],
},
dearrowByDefault: {
description: "Dearrow videos automatically",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false
}
});

View file

@ -7,16 +7,15 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
import { Heading, RelationshipStore, Text } from "@webpack/common";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { RelationshipStore, Text } from "@webpack/common";
const containerWrapper = findByPropsLazy("memberSinceWrapper");
const container = findByPropsLazy("memberSince");
const getCreatedAtDate = findByCodeLazy('month:"short",day:"numeric"');
const locale = findByPropsLazy("getLocale");
const section = findLazy((m: any) => m.section !== void 0 && m.heading !== void 0 && Object.values(m).length === 2);
const Section = findComponentByCodeLazy('"auto":"smooth"', ".section");
export default definePlugin({
name: "FriendsSince",
@ -28,7 +27,7 @@ export default definePlugin({
find: ".PANEL}),nicknameIcons",
replacement: {
match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id)}\)}\)/,
replace: "$&,$self.friendsSinceNew({userId:$1,isSidebar:true})"
replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:true})"
}
},
// User Profile Modal
@ -36,34 +35,19 @@ export default definePlugin({
find: "action:\"PRESS_APP_CONNECTION\"",
replacement: {
match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id),.{0,100}}\)}\),/,
replace: "$&,$self.friendsSinceNew({userId:$1,isSidebar:false}),"
replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:false}),"
}
}
],
getFriendSince(userId: string) {
try {
if (!RelationshipStore.isFriend(userId)) return null;
return RelationshipStore.getSince(userId);
} catch (err) {
new Logger("FriendsSince").error(err);
return null;
}
},
friendsSinceNew: ErrorBoundary.wrap(({ userId, isSidebar }: { userId: string; isSidebar: boolean; }) => {
FriendsSinceComponent: ErrorBoundary.wrap(({ userId, isSidebar }: { userId: string; isSidebar: boolean; }) => {
if (!RelationshipStore.isFriend(userId)) return null;
const friendsSince = RelationshipStore.getSince(userId);
if (!friendsSince) return null;
return (
<section className={section.section}>
<Heading variant="text-xs/semibold" style={isSidebar ? {} : { color: "var(--header-secondary)" }}>
Friends Since
</Heading>
<Section heading="Friends Since">
{
isSidebar ? (
<Text variant="text-sm/normal">
@ -91,8 +75,7 @@ export default definePlugin({
</div>
)
}
</section>
</Section>
);
}, { noop: true }),
});

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

@ -237,7 +237,7 @@ function isActivityTypeIgnored(type: number, 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,
@ -266,6 +266,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: {
@ -279,6 +280,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

@ -14,7 +14,7 @@ import { OnlineMemberCountStore } from "./OnlineMemberCountStore";
export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) {
const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const guildId = isTooltip ? tooltipGuildId! : currentChannel.guild_id;
const guildId = isTooltip ? tooltipGuildId! : currentChannel?.guild_id;
const totalCount = useStateFromStores(
[GuildMemberCountStore],
@ -33,7 +33,7 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t
const threadGroups = useStateFromStores(
[ThreadMemberListStore],
() => ThreadMemberListStore.getMemberListSections(currentChannel.id)
() => ThreadMemberListStore.getMemberListSections(currentChannel?.id)
);
if (!isTooltip && (groups.length >= 1 || groups[0].id !== "unknown")) {

View file

@ -15,8 +15,8 @@ export const OnlineMemberCountStore = proxyLazy(() => {
const onlineMemberMap = new Map<string, number>();
class OnlineMemberCountStore extends Flux.Store {
getCount(guildId: string) {
return onlineMemberMap.get(guildId);
getCount(guildId?: string) {
return onlineMemberMap.get(guildId!);
}
async _ensureCount(guildId: string) {
@ -25,8 +25,8 @@ export const OnlineMemberCountStore = proxyLazy(() => {
await PrivateChannelsStore.preload(guildId, GuildChannelStore.getDefaultChannel(guildId).id);
}
ensureCount(guildId: string) {
if (onlineMemberMap.has(guildId)) return;
ensureCount(guildId?: string) {
if (!guildId || onlineMemberMap.has(guildId)) return;
preloadQueue.push(() =>
this._ensureCount(guildId)

View file

@ -28,12 +28,12 @@ import { FluxStore } from "@webpack/types";
import { MemberCount } from "./MemberCount";
export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; };
export const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId?: string): number | null; };
export const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
getProps(guildId?: string, channelId?: string): { groups: { count: number; id: string; }[]; };
};
export const ThreadMemberListStore = findStoreLazy("ThreadMemberListStore") as FluxStore & {
getMemberListSections(channelId: string): { [sectionId: string]: { sectionId: string; userIds: string[]; }; };
getMemberListSections(channelId?: string): { [sectionId: string]: { sectionId: string; userIds: string[]; }; };
};

View file

@ -9,3 +9,8 @@
.vc-mentionAvatars-role-icon {
margin: 0 2px 0.2rem 4px;
}
/** don't display inside the ServerInfo modal owner mention */
.vc-gp-owner .vc-mentionAvatars-icon {
display: none;
}

View file

@ -20,7 +20,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore } from "@webpack/common";
import { Channel, User } from "discord-types/general";
@ -28,6 +28,7 @@ const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
const UserUtils = findByPropsLazy("getGlobalName");
const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds");
const ExpandableList = findComponentByCodeLazy(".mutualFriendItem]");
const GuildLabelClasses = findByPropsLazy("guildNick", "guildAvatarWithoutIcon");
function getGroupDMName(channel: Channel) {
@ -50,6 +51,29 @@ function getMutualGDMCountText(user: User) {
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
}
function renderClickableGDMs(mutualDms: Channel[], onClose: () => void) {
return mutualDms.map(c => (
<Clickable
className={ProfileListClasses.listRow}
onClick={() => {
onClose();
SelectedChannelActionCreators.selectPrivateChannel(c.id);
}}
>
<Avatar
src={IconUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
size="SIZE_40"
className={ProfileListClasses.listAvatar}
>
</Avatar>
<div className={ProfileListClasses.listRowContent}>
<div className={ProfileListClasses.listName}>{getGroupDMName(c)}</div>
<div className={GuildLabelClasses.guildNick}>{c.recipients.length + 1} Members</div>
</div>
</Clickable>
));
}
const IS_PATCHED = Symbol("MutualGroupDMs.Patched");
export default definePlugin({
@ -70,6 +94,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})"
}
}
],
@ -84,28 +115,9 @@ export default definePlugin({
},
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const mutualDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);
const mutualGDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);
const entries = mutualDms.map(c => (
<Clickable
className={ProfileListClasses.listRow}
onClick={() => {
onClose();
SelectedChannelActionCreators.selectPrivateChannel(c.id);
}}
>
<Avatar
src={IconUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
size="SIZE_40"
className={ProfileListClasses.listAvatar}
>
</Avatar>
<div className={ProfileListClasses.listRowContent}>
<div className={ProfileListClasses.listName}>{getGroupDMName(c)}</div>
<div className={GuildLabelClasses.guildNick}>{c.recipients.length + 1} Members</div>
</div>
</Clickable>
));
const entries = renderClickableGDMs(mutualGDms, onClose);
return (
<ScrollerThin
@ -124,5 +136,24 @@ export default definePlugin({
}
</ScrollerThin>
);
}),
renderDMPageList: ErrorBoundary.wrap(({ user, Divider, listStyle }: { user: User, Divider: JSX.Element, listStyle: string; }) => {
const mutualGDms = getMutualGroupDms(user.id);
if (mutualGDms.length === 0) return null;
const header = getMutualGDMCountText(user);
return (
<>
{Divider}
<ExpandableList
className={listStyle}
header={header}
isLoadingHeader={false}
children={renderClickableGDMs(mutualGDms, () => { })}
/>
</>
);
})
});

View file

@ -36,7 +36,7 @@ export default definePlugin({
}
],
shouldSkip(guildId: string, emoji: any) {
if (emoji.type !== "GUILD_EMOJI") {
if (emoji.type !== 1) {
return false;
}
if (settings.store.shownEmojis === "onlyUnicode") {

View file

@ -21,8 +21,10 @@ import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import type { Guild } from "discord-types/general";
import { findByCodeLazy } from "@webpack";
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import { UnicodeEmoji } from "@webpack/types";
import type { Guild, Role, User } from "discord-types/general";
import { settings } from "..";
import { cl, getPermissionDescription, getPermissionString } from "../utils";
@ -42,15 +44,15 @@ export interface RoleOrUserPermission {
overwriteDeny?: bigint;
}
function openRolesAndUsersPermissionsModal(permissions: Array<RoleOrUserPermission>, guild: Guild, header: string) {
return openModal(modalProps => (
<RolesAndUsersPermissions
modalProps={modalProps}
permissions={permissions}
guild={guild}
header={header}
/>
));
type GetRoleIconData = (role: Role, size: number) => { customIconSrc?: string; unicodeEmoji?: UnicodeEmoji; };
const getRoleIconData: GetRoleIconData = findByCodeLazy("convertSurrogateToName", "customIconSrc", "unicodeEmoji");
function getRoleIconSrc(role: Role) {
const icon = getRoleIconData(role, 20);
if (!icon) return;
const { customIconSrc, unicodeEmoji } = icon;
return customIconSrc ?? unicodeEmoji?.url;
}
function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, header }: { permissions: Array<RoleOrUserPermission>; guild: Guild; modalProps: ModalProps; header: string; }) {
@ -86,31 +88,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 user: User | undefined = UserStore.getUser(permission.id ?? "");
const role: Role | undefined = roles[permission.id ?? ""];
const roleIconSrc = role != null ? 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, () => (
@ -124,7 +129,6 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
ContextMenuApi.openContextMenu(e, () => (
<UserContextMenu
userId={permission.id!}
onClose={modalProps.onClose}
/>
));
}
@ -132,13 +136,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 != null && (
<img
className={cl("modal-user-img")}
src={user.getAvatarURL(void 0, void 0, false)}
/>
)}
@ -147,28 +157,25 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
permission.type === PermissionType.Role
? role?.name ?? "Unknown Role"
: permission.type === PermissionType.User
? (user && getUniqueUsername(user)) ?? "Unknown User"
? (user != null && getUniqueUsername(user)) ?? "Unknown User"
: (
<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(PermissionsBits).map(([permissionName, bit]) => (
<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;
@ -192,11 +199,11 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
</Tooltip>
</div>
))}
</div>
</ScrollerThin>
</div>
)}
</ModalContent>
</ModalRoot >
</ModalRoot>
);
}
@ -208,7 +215,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
aria-label="Role Options"
>
<Menu.MenuItem
id="vc-copy-role-id"
id={cl("copy-role-id")}
label={i18n.Messages.COPY_ID_ROLE}
action={() => {
Clipboard.copy(roleId);
@ -217,14 +224,13 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
{(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,
@ -235,15 +241,14 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
}
}
});
}
}
}}
/>
)}
</Menu.Menu>
);
}
function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => void; }) {
function UserContextMenu({ userId }: { userId: string; }) {
return (
<Menu.Menu
navId={cl("user-context-menu")}
@ -251,7 +256,7 @@ function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => v
aria-label="User Options"
>
<Menu.MenuItem
id="vc-copy-user-id"
id={cl("copy-user-id")}
label={i18n.Messages.COPY_ID_USER}
action={() => {
Clipboard.copy(userId);
@ -263,4 +268,13 @@ function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => v
const RolesAndUsersPermissions = ErrorBoundary.wrap(RolesAndUsersPermissionsComponent);
export default openRolesAndUsersPermissionsModal;
export default function openRolesAndUsersPermissionsModal(permissions: Array<RoleOrUserPermission>, guild: Guild, header: string) {
return openModal(modalProps => (
<RolesAndUsersPermissions
modalProps={modalProps}
permissions={permissions}
guild={guild}
header={header}
/>
));
}

View file

@ -29,6 +29,7 @@ import openRolesAndUsersPermissionsModal, { PermissionType, type RoleOrUserPermi
interface UserPermission {
permission: string;
roleName: string;
roleColor: string;
rolePosition: number;
}
@ -45,8 +46,48 @@ const { RoleRootClasses, RoleClasses, RoleBorderClasses } = proxyLazyWebpack(()
return { RoleRootClasses, RoleClasses, RoleBorderClasses };
});
interface FakeRoleProps extends React.HTMLAttributes<HTMLDivElement> {
text: string;
color: string;
}
function FakeRole({ text, color, ...props }: FakeRoleProps) {
return (
<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;
}
function GrantedByTooltip({ roleName, roleColor }: GrantedByTooltipProps) {
return (
<>
<Text variant="text-sm/medium">Granted By</Text>
<FakeRole text={roleName} color={roleColor} />
</>
);
}
function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { guild: Guild; guildMember: GuildMember; forceOpen?: boolean; }) {
const stns = settings.use(["permissionsSortOrder"]);
const { permissionsSortOrder } = settings.use(["permissionsSortOrder"]);
const [rolePermissions, userPermissions] = useMemo(() => {
const userPermissions: UserPermissions = [];
@ -67,6 +108,7 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
const OWNER = i18n.Messages.GUILD_OWNER || "Server Owner";
userPermissions.push({
permission: OWNER,
roleName: "Owner",
roleColor: "var(--primary-300)",
rolePosition: Infinity
});
@ -75,10 +117,11 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
sortUserRoles(userRoles);
for (const [permission, bit] of Object.entries(PermissionsBits)) {
for (const { permissions, colorString, position } of userRoles) {
for (const { permissions, colorString, position, name } of userRoles) {
if ((permissions & bit) === bit) {
userPermissions.push({
permission: getPermissionString(permission),
roleName: name,
roleColor: colorString || "var(--primary-300)",
rolePosition: position
});
@ -91,7 +134,7 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
userPermissions.sort((a, b) => b.rolePosition - a.rolePosition);
return [rolePermissions, userPermissions];
}, [stns.permissionsSortOrder]);
}, [permissionsSortOrder]);
return (
<ExpandableHeader
@ -108,46 +151,41 @@ function UserPermissionsComponent({ guild, guildMember, forceOpen = false }: { g
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 ? PermissionsSortOrder.LowestRole : PermissionsSortOrder.HighestRole;
settings.store.permissionsSortOrder = permissionsSortOrder === PermissionsSortOrder.HighestRole ? PermissionsSortOrder.LowestRole : PermissionsSortOrder.HighestRole;
}}
>
<svg
width="20"
height="20"
viewBox="0 96 960 960"
transform={stns.permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
transform={permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
>
<path fill="var(--text-normal)" d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" />
</svg>
</button>
</div>
)}
</Tooltip>)
</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

@ -26,7 +26,7 @@ import { Devs } from "@utils/constants";
import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, Menu, PermissionsBits, Popout, TooltipContainer, UserStore } from "@webpack/common";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, match, Menu, PermissionsBits, Popout, TooltipContainer, UserStore } from "@webpack/common";
import type { Guild, GuildMember } from "discord-types/general";
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions";
@ -54,12 +54,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
}
});
@ -73,14 +73,12 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
action={() => {
const guild = GuildStore.getGuild(guildId);
let permissions: RoleOrUserPermission[];
let header: string;
switch (type) {
case MenuItemParentType.User: {
const { permissions, header } = match(type)
.returnType<{ permissions: RoleOrUserPermission[], header: string; }>()
.with(MenuItemParentType.User, () => {
const member = GuildMemberStore.getMember(guildId, id!);
permissions = getSortedRoles(guild, member)
const permissions: RoleOrUserPermission[] = getSortedRoles(guild, member)
.map(role => ({
type: PermissionType.Role,
...role
@ -93,37 +91,37 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
});
}
header = member.nick ?? UserStore.getUser(member.userId).username;
break;
}
case MenuItemParentType.Channel: {
return {
permissions,
header: member.nick ?? UserStore.getUser(member.userId).username
};
})
.with(MenuItemParentType.Channel, () => {
const channel = ChannelStore.getChannel(id!);
permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({
const permissions = sortPermissionOverwrites(Object.values(channel.permissionOverwrites).map(({ id, allow, deny, type }) => ({
type: type as PermissionType,
id,
overwriteAllow: allow,
overwriteDeny: deny
})), guildId);
header = channel.name;
break;
}
default: {
permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
return {
permissions,
header: channel.name
};
})
.otherwise(() => {
const permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
type: PermissionType.Role,
...role
}));
header = guild.name;
break;
}
}
return {
permissions,
header: guild.name
};
});
openRolesAndUsersPermissionsModal(permissions, guild, header);
}}
@ -133,32 +131,34 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback {
return (children, props) => {
if (!props) return;
if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild)))
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 = (() => {
switch (type) {
case MenuItemParentType.User:
return MenuItem(props.guildId, props.user.id, type);
case MenuItemParentType.Channel:
return MenuItem(props.guild.id, props.channel.id, type);
case MenuItemParentType.Guild:
return MenuItem(props.guild.id);
default:
return null;
}
})();
const item = match(type)
.with(MenuItemParentType.User, () => MenuItem(props.guildId, props.user.id, type))
.with(MenuItemParentType.Channel, () => MenuItem(props.guild.id, props.channel.id, type))
.with(MenuItemParentType.Guild, () => MenuItem(props.guild.id))
.otherwise(() => null);
if (item == null) return;
if (group)
group.push(item);
else if (childId === "roles" && props.guildId)
if (group) {
return group.push(item);
}
// "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>);
}
};
}

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 { ReviewType } from "../entities";
import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { cl } from "../utils";
import ReviewComponent from "./ReviewComponent";
import ReviewsView, { ReviewsInputComponent } from "./ReviewsView";
function Modal({ modalProps, modalKey, discordId, name }: { modalProps: any; modalKey: string, discordId: string; name: string; }) {
function Modal({ modalProps, modalKey, discordId, name, type }: { modalProps: any; modalKey: string, discordId: string; 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({ modalProps, modalKey, discordId, name }: { modalProps: any; mod
onFetchReviews={setData}
scrollToTop={() => ref.current?.scrollTo({ top: 0, behavior: "smooth" })}
hideOwnReview
type={type}
/>
</div>
</ModalContent>
@ -95,7 +97,7 @@ function Modal({ modalProps, modalKey, discordId, name }: { modalProps: any; mod
);
}
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

@ -21,7 +21,7 @@ import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpa
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
import { Auth, authorize } from "../auth";
import { Review } from "../entities";
import { Review, ReviewType } from "../entities";
import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { settings } from "../settings";
import { cl, showToast } from "../utils";
@ -45,6 +45,7 @@ interface Props extends UserProps {
page?: number;
scrollToTop?(): void;
hideOwnReview?: boolean;
type: ReviewType;
}
export default function ReviewsView({
@ -56,6 +57,7 @@ export default function ReviewsView({
page = 1,
showInput = false,
hideOwnReview = false,
type,
}: Props) {
const [signal, refetch] = useForceUpdater(true);
@ -80,6 +82,7 @@ export default function ReviewsView({
reviews={reviewData!.reviews}
hideOwnReview={hideOwnReview}
profileId={discordId}
type={type}
/>
{showInput && (
@ -94,7 +97,7 @@ export default function ReviewsView({
);
}
function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; profileId: string; }) {
function ReviewList({ refetch, reviews, hideOwnReview, profileId, type }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; profileId: string; type: ReviewType; }) {
const myId = UserStore.getCurrentUser().id;
return (
@ -111,7 +114,7 @@ function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch():
{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 { Guild, User } from "discord-types/general";
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";
@ -44,7 +44,7 @@ const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { gu
label="View Reviews"
id="vc-rdb-server-reviews"
icon={OpenExternalIcon}
action={() => openReviewsModal(guild.id, guild.name)}
action={() => openReviewsModal(guild.id, guild.name, ReviewType.Server)}
/>
);
};
@ -56,7 +56,7 @@ const userContextPatch: NavContextMenuPatchCallback = (children, { user }: { use
label="View Reviews"
id="vc-rdb-user-reviews"
icon={OpenExternalIcon}
action={() => openReviewsModal(user.id, user.username)}
action={() => openReviewsModal(user.id, user.username, ReviewType.User)}
/>
);
};
@ -157,7 +157,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

@ -57,7 +57,7 @@ export default definePlugin({
patches: [
// Chat Mentions
{
find: 'location:"UserMention',
find: ".USER_MENTION)",
replacement: [
{
match: /onContextMenu:\i,color:\i,\.\.\.\i(?=,children:)(?<=user:(\i),channel:(\i).{0,500}?)/,

View file

@ -4,21 +4,38 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
onlySnow: {
type: OptionType.BOOLEAN,
description: "Only play the Snow Halation Theme",
default: false,
restartNeeded: true
}
});
// NOTE - Ultimately should probably be turned into a ringtone picker plugin
export default definePlugin({
name: "SecretRingToneEnabler",
description: "Always play the secret version of the discord ringtone (except during special ringtone events)",
authors: [Devs.AndrewDLO, Devs.FieryFlames],
authors: [Devs.AndrewDLO, Devs.FieryFlames, Devs.RamziAH],
settings,
patches: [
{
find: '"call_ringing_beat"',
replacement: {
replacement: [
{
match: /500!==\i\(\)\.random\(1,1e3\)/,
replace: "false",
}
replace: "false"
},
],
{
predicate: () => settings.store.onlySnow,
match: /"call_ringing_beat",/,
replace: ""
}
]
}
]
});

View file

@ -39,7 +39,7 @@ interface StreamData {
gainNode?: GainNode,
id: string,
levelNode: AudioWorkletNode,
sinkId: string,
sinkId: string | "default",
stream: MediaStream,
streamSourceNode?: MediaStreamAudioSourceNode,
videoStreamId: string,
@ -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
@ -128,6 +128,12 @@ export default definePlugin({
gain.connect(data.audioContext.destination);
}
// @ts-expect-error
if (data.sinkId != null && data.sinkId !== data.audioContext.sinkId && "setSinkId" in AudioContext.prototype) {
// @ts-expect-error https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId
data.audioContext.setSinkId(data.sinkId);
}
data.gainNode.gain.value = data._mute
? 0
: data._volume / 100;

View file

@ -86,7 +86,7 @@ interface NotificationObject {
title: string;
content: string;
useBase64Icon: boolean;
icon: ArrayBuffer | string;
icon: string;
sourceApp: string;
}
@ -320,7 +320,13 @@ function shouldIgnoreForChannelType(channel: Channel) {
}
function sendMsgNotif(titleString: string, content: string, message: Message) {
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`)
.then(response => response.blob())
.then(blob => new Promise<string>(resolve => {
const r = new FileReader();
r.onload = () => resolve((r.result as string).split(",")[1]);
r.readAsDataURL(blob);
})).then(result => {
const msgData: NotificationObject = {
type: 1,
timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
@ -331,7 +337,7 @@ function sendMsgNotif(titleString: string, content: string, message: Message) {
title: titleString,
content: content,
useBase64Icon: true,
icon: new TextDecoder().decode(result),
icon: result,
sourceApp: "Vencord"
};

View file

@ -566,6 +566,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "niko",
id: 341377368075796483n,
},
RamziAH: {
name: "RamziAH",
id: 1279957227612147747n,
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly

View file

@ -18,7 +18,7 @@
import { MessageObject } from "@api/MessageEvents";
import { ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
import { Guild, Message, User } from "discord-types/general";
import { Channel, Guild, Message, User } from "discord-types/general";
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
@ -54,12 +54,12 @@ export async function openInviteModal(code: string) {
});
}
export function getCurrentChannel() {
export function getCurrentChannel(): Channel | undefined {
return ChannelStore.getChannel(SelectedChannelStore.getChannelId());
}
export function getCurrentGuild(): Guild | undefined {
return GuildStore.getGuild(getCurrentChannel()?.guild_id);
return GuildStore.getGuild(getCurrentChannel()?.guild_id!);
}
export function openPrivateChannel(userId: string) {

View file

@ -17,6 +17,7 @@
*/
import { Settings, SettingsStore } from "@api/Settings";
import { ThemeStore } from "@webpack/common";
let style: HTMLStyleElement;
@ -59,7 +60,18 @@ async function initThemes() {
const { themeLinks, enabledThemes } = Settings;
const links: string[] = [...themeLinks];
// "darker" and "midnight" both count as dark
const activeTheme = ThemeStore.theme === "light" ? "light" : "dark";
const links = themeLinks
.map(rawLink => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return rawLink;
const [, mode, link] = match;
return mode === activeTheme ? link : null;
})
.filter(link => link !== null);
if (IS_WEB) {
for (const theme of enabledThemes) {
@ -85,6 +97,7 @@ document.addEventListener("DOMContentLoaded", () => {
SettingsStore.addChangeListener("themeLinks", initThemes);
SettingsStore.addChangeListener("enabledThemes", initThemes);
ThemeStore.addChangeListener(initThemes);
if (!IS_WEB)
VencordNative.quickCss.addThemeChangeListener(initThemes);

View file

@ -53,6 +53,7 @@ export let RelationshipStore: Stores.RelationshipStore & t.FluxStore & {
};
export let EmojiStore: t.EmojiStore;
export let ThemeStore: t.ThemeStore;
export let WindowStore: t.WindowStore;
export let DraftStore: t.DraftStore;
@ -84,3 +85,4 @@ waitForStore("GuildChannelStore", m => GuildChannelStore = m);
waitForStore("MessageStore", m => MessageStore = m);
waitForStore("WindowStore", m => WindowStore = m);
waitForStore("EmojiStore", m => EmojiStore = m);
waitForStore("ThemeStore", m => ThemeStore = m);

View file

@ -91,7 +91,7 @@ export type Tooltip = ComponentType<{
/** Tooltip.Colors.BLACK */
color?: string;
/** TooltipPositions.TOP */
position?: string;
position?: PopoutPosition;
tooltipClassName?: string;
tooltipContentClassName?: string;
@ -110,7 +110,7 @@ export type TooltipContainer = ComponentType<PropsWithChildren<{
/** Tooltip.Colors.BLACK */
color?: string;
/** TooltipPositions.TOP */
position?: string;
position?: PopoutPosition;
spacing?: number;
className?: string;
@ -252,7 +252,7 @@ export type Select = ComponentType<PropsWithChildren<{
look?: 0 | 1;
className?: string;
popoutClassName?: string;
popoutPosition?: "top" | "left" | "right" | "bottom" | "center" | "window_center";
popoutPosition?: PopoutPosition;
optionClassName?: string;
autoFocus?: boolean;
@ -293,7 +293,7 @@ export type SearchableSelect = ComponentType<PropsWithChildren<{
className?: string;
popoutClassName?: string;
wrapperClassName?: string;
popoutPosition?: "top" | "left" | "right" | "bottom" | "center" | "window_center";
popoutPosition?: PopoutPosition;
optionClassName?: string;
autoFocus?: boolean;
@ -376,6 +376,8 @@ declare enum PopoutAnimation {
FADE = "4"
}
type PopoutPosition = "top" | "bottom" | "left" | "right" | "center" | "window_center";
export type Popout = ComponentType<{
children(
thing: {
@ -387,7 +389,7 @@ export type Popout = ComponentType<{
},
data: {
isShown: boolean;
position: string;
position: PopoutPosition;
}
): ReactNode;
shouldShow?: boolean;
@ -395,7 +397,7 @@ export type Popout = ComponentType<{
closePopout(): void;
isPositioned: boolean;
nudge: number;
position: string;
position: PopoutPosition;
setPopoutRef(ref: any): void;
updatePosition(): void;
}): ReactNode;
@ -404,13 +406,13 @@ export type Popout = ComponentType<{
onRequestClose?(): void;
/** "center" and others */
align?: string;
align?: "left" | "right" | "center";
/** Popout.Animation */
animation?: PopoutAnimation;
autoInvert?: boolean;
nudgeAlignIntoViewport?: boolean;
/** "bottom" and others */
position?: string;
position?: PopoutPosition;
positionKey?: string;
spacing?: number;
}> & {
@ -459,7 +461,7 @@ export type ScrollerThin = ComponentType<PropsWithChildren<{
style?: CSSProperties;
dir?: "ltr";
orientation?: "horizontal" | "vertical";
orientation?: "horizontal" | "vertical" | "auto";
paddingFix?: boolean;
fade?: boolean;

View file

@ -220,6 +220,14 @@ export class GuildStore extends FluxStore {
getAllGuildRoles(): Record<string, Record<string, Role>>;
}
export class ThemeStore extends FluxStore {
theme: "light" | "dark" | "darker" | "midnight";
darkSidebar: boolean;
isSystemThemeAvailable: boolean;
systemPrefersColorScheme: "light" | "dark";
systemTheme: null;
}
export type useStateFromStores = <T>(
stores: t.FluxStore[],
mapper: () => T,