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

This commit is contained in:
ryan-0324 2024-06-29 15:43:23 -04:00
commit 450efacf95
22 changed files with 553 additions and 263 deletions

View file

@ -1,6 +1,6 @@
{
"manifest_version": 3,
"minimum_chrome_version": "91",
"minimum_chrome_version": "111",
"name": "Vencord Web",
"description": "The cutest Discord mod now in your browser",

View file

@ -43,7 +43,7 @@
"browser_specific_settings": {
"gecko": {
"id": "vencord-firefox@vendicated.dev",
"strict_min_version": "91.0"
"strict_min_version": "128.0"
}
}
}

View file

@ -60,6 +60,7 @@ export declare abstract class ChannelRecordBase {
isManaged(): boolean;
isMediaChannel(): this is GuildMediaChannelRecord;
isMediaPost(): boolean;
/** This method is functionally the same as {@link isGroupDM}. */
isMultiUserDM(): this is GroupDMChannelRecord;
isNSFW(): boolean;
isOwner(userId: string): boolean;

View file

@ -19,6 +19,8 @@
import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { openNotificationSettingsModal } from "@components/VencordSettings/NotificationSettings";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, type ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import { AlertActionCreators, Button, Forms, Text, Timestamp, useEffect, useReducer, useRef, useState } from "@webpack/common";
@ -167,24 +169,31 @@ function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void
</ModalContent>
<ModalFooter>
<Button
disabled={log.length === 0}
onClick={() => {
AlertActionCreators.show({
title: "Are you sure?",
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
async onConfirm() {
await DataStore.set(DATA_KEY, []);
signals.forEach(x => { x(); });
},
confirmText: "Do it!",
confirmColor: "vc-notification-log-danger-btn",
cancelText: "Nevermind"
});
}}
>
Clear Notification Log
</Button>
<Flex>
<Button onClick={openNotificationSettingsModal}>
Notification Settings
</Button>
<Button
disabled={log.length === 0}
color={Button.Colors.RED}
onClick={() => {
AlertActionCreators.show({
title: "Are you sure?",
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
async onConfirm() {
await DataStore.set(DATA_KEY, []);
signals.forEach(x => { x(); });
},
confirmText: "Do it!",
confirmColor: "vc-notification-log-danger-btn",
cancelText: "Nevermind"
});
}}
>
Clear Notification Log
</Button>
</Flex>
</ModalFooter>
</ModalRoot>
);

28
src/components/Grid.tsx Normal file
View file

@ -0,0 +1,28 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { CSSProperties, JSX } from "react";
interface Props {
columns: number;
gap?: string;
inline?: boolean;
}
export function Grid(props: Props & JSX.IntrinsicElements["div"]) {
const style: CSSProperties = {
display: props.inline ? "inline-grid" : "grid",
gridTemplateColumns: `repeat(${props.columns}, 1fr)`,
gap: props.gap,
...props.style
};
return (
<div {...props} style={style}>
{props.children}
</div>
);
}

View file

@ -18,19 +18,17 @@
import "./iconStyles.css";
import { getTheme, Theme } from "@utils/discord";
import { classes } from "@utils/misc";
import { i18n } from "@webpack/common";
import type { PropsWithChildren, SVGProps } from "react";
import type { JSX, PropsWithChildren } from "react";
interface BaseIconProps extends IconProps {
viewBox: string;
}
interface IconProps extends SVGProps<SVGSVGElement> {
className?: string;
height?: string | number;
width?: string | number;
}
type IconProps = JSX.IntrinsicElements["svg"];
type ImageProps = JSX.IntrinsicElements["img"];
const Icon = ({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) => (
<svg
@ -248,3 +246,69 @@ export const NotesIcon = (props: IconProps) => (
<path d="M8 3c-.55229 0-1 .44772-1 1v1c0 .55228.44772 1 1 1h8c.5523 0 1-.44772 1-1V4c0-.55228-.4477-1-1-1h-.8755c-.3957 0-.771-.17576-1.0243-.47975l-.7334-.88007C13.0288 1.23454 12.528 1 12 1s-1.0288.23454-1.3668.64018l-.7334.88007C9.64647 2.82424 9.27121 3 8.8755 3H8Zm11 1.49996v.5c0 1.65685-1.3431 3-3 3H8c-1.65685 0-3-1.34315-3-3v-.5c0-.27614-.22554-.50437-.49791-.45887C3.08221 4.27826 2 5.51273 2 6.99996V19c0 1.6568 1.34315 3 3 3h14c1.6569 0 3-1.3432 3-3V6.99996c0-1.48723-1.0822-2.7217-2.5021-2.95887-.2724-.0455-.4979.18273-.4979.45887ZM8 12c-.55228 0-1 .4477-1 1 0 .5522.44772 1 1 1h8c.5523 0 1-.4478 1-1 0-.5523-.4477-1-1-1H8Zm-1 5c0-.5523.44772-1 1-1h5c.5523 0 1 .4477 1 1 0 .5522-.4477 1-1 1H8c-.55228 0-1-.4478-1-1Z" />
</Icon>
);
export const FolderIcon = (props: IconProps) => (
<Icon
{...props}
className={classes(props.className, "vc-folder-icon")}
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M2 5a3 3 0 0 1 3-3h3.93a2 2 0 0 1 1.66.9L12 5h7a3 3 0 0 1 3 3v11a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5Z" />
</Icon>
);
export const LogIcon = (props: IconProps) => (
<Icon
{...props}
className={classes(props.className, "vc-log-icon")}
viewBox="0 0 24 24"
fill="currentColor"
fillRule="evenodd"
>
<path d="M3.11 8H6v10.82c0 .86.37 1.68 1 2.27.46.43 1.02.71 1.63.84A1 1 0 0 0 9 22h10a4 4 0 0 0 4-4v-1a2 2 0 0 0-2-2h-1V5a3 3 0 0 0-3-3H4.67c-.87 0-1.7.32-2.34.9-.63.6-1 1.42-1 2.28 0 .71.3 1.35.52 1.75a5.35 5.35 0 0 0 .48.7l.01.01h.01L3.11 7l-.76.65a1 1 0 0 0 .76.35Zm1.56-4c-.38 0-.72.14-.97.37-.24.23-.37.52-.37.81a1.69 1.69 0 0 0 .3.82H6v-.83c0-.29-.13-.58-.37-.8C5.4 4.14 5.04 4 4.67 4Zm5 13a3.58 3.58 0 0 1 0 3H19a2 2 0 0 0 2-2v-1H9.66ZM3.86 6.35ZM11 8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm-1 5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1Z" />
</Icon>
);
export const RestartIcon = (props: IconProps) => (
<Icon
{...props}
className={classes(props.className, "vc-restart-icon")}
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M4 12a8 8 0 0 1 14.93-4H15a1 1 0 1 0 0 2h6a1 1 0 0 0 1-1V3a1 1 0 1 0-2 0v3a9.98 9.98 0 0 0-18 6 10 10 0 0 0 16.29 7.78 1 1 0 0 0-1.26-1.56A8 8 0 0 1 4 12Z" />
</Icon>
);
export const PaintbrushIcon = (props: IconProps) => (
<Icon
{...props}
className={classes(props.className, "vc-paintbrush-icon")}
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M15.35 7.24C15.9 6.67 16 5.8 16 5a3 3 0 1 1 3 3c-.8 0-1.67.09-2.24.65a1.5 1.5 0 0 0 0 2.11l1.12 1.12a3 3 0 0 1 0 4.24l-5 5a3 3 0 0 1-4.25 0l-5.76-5.75a3 3 0 0 1 0-4.24l4.04-4.04.97-.97a3 3 0 0 1 4.24 0l1.12 1.12c.58.58 1.52.58 2.1 0ZM6.9 9.9l-2.6 2.64a1 1 0 0 0 0 1.42l2.17 2.17.83-.84a1 1 0 0 1 1.42 1.42l-.84.83.59.59 1.83-1.84a1 1 0 0 1 1.42 1.42l-1.84 1.83.17.17a1 1 0 0 0 1.42 0l2.63-2.62L6.9 9.9Z" />
</Icon>
);
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
export function GithubIcon(props: ImageProps) {
const src = getTheme() === Theme.Light
? GithubIconLight
: GithubIconDark;
return <img {...props} src={src} />;
}
export function WebsiteIcon(props: ImageProps) {
const src = getTheme() === Theme.Light
? WebsiteIconLight
: WebsiteIconDark;
return <img {...props} src={src} />;
}

View file

@ -6,24 +6,18 @@
import "./LinkIconButton.css";
import { getTheme, Theme } from "@utils/discord";
import { MaskedLink, Tooltip } from "@webpack/common";
import type { ComponentType } from "react";
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
import { GithubIcon, WebsiteIcon } from "..";
export function GithubIcon() {
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
return <img src={src} aria-hidden className={"vc-settings-modal-link-icon"} />;
}
export const GithubLinkIcon = () => (
<GithubIcon aria-hidden className={"vc-settings-modal-link-icon"} />
);
export function WebsiteIcon() {
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
return <img src={src} aria-hidden className={"vc-settings-modal-link-icon"} />;
}
export const WebsiteLinkIcon = () => (
<WebsiteIcon aria-hidden className={"vc-settings-modal-link-icon"} />
);
interface Props {
text: string;
@ -40,5 +34,5 @@ const LinkIcon = ({ text, href, Icon }: Props & { Icon: ComponentType; }) => (
</Tooltip>
);
export const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteIcon} />;
export const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubIcon} />;
export const WebsiteButton = (props: Props) => <LinkIcon {...props} Icon={WebsiteLinkIcon} />;
export const GithubButton = (props: Props) => <LinkIcon {...props} Icon={GithubLinkIcon} />;

View file

@ -27,7 +27,7 @@ import { gitRemote } from "@shared/vencordUserAgent";
import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, type ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, type ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { OptionType, type Plugin } from "@utils/types";
import type { UserRecord as $UserRecord } from "@vencord/discord-types";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
@ -316,3 +316,13 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</ModalRoot>
);
}
export function openPluginModal(plugin: Plugin, onRestartNeeded?: (pluginName: string) => void) {
openModal(modalProps => (
<PluginModal
{...modalProps}
plugin={plugin}
onRestartNeeded={() => onRestartNeeded?.(plugin.name)}
/>
));
}

View file

@ -23,7 +23,7 @@ import { showNotice } from "@api/Notices";
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { CogWheel, InfoIcon } from "@components/Icons";
import PluginModal from "@components/PluginSettings/PluginModal";
import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { AddonCard } from "@components/VencordSettings/AddonCard";
import { SettingsTab } from "@components/VencordSettings/shared";
import { ChangeList } from "@utils/ChangeList";
@ -31,7 +31,6 @@ import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes, isObjectEmpty } from "@utils/misc";
import { openModalLazy } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import type { Plugin } from "@utils/types";
import { findByPropsLazy } from "@webpack";
@ -46,7 +45,7 @@ const { startDependenciesRecursive, startPlugin, stopPlugin } = proxyLazy(() =>
const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189");
const InputStyles: Record<string, string> = findByPropsLazy("inputDefault", "inputWrapper");
const InputStyles: Record<string, string> = findByPropsLazy("inputWrapper", "inputDefault", "error");
const ButtonClasses: Record<string, string> = findByPropsLazy("button", "disabled", "enabled");
@ -96,16 +95,6 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const isEnabled = () => settings.enabled ?? false;
function openModal() {
openModalLazy(() => Promise.resolve(modalProps => (
<PluginModal
{...modalProps}
plugin={plugin}
onRestartNeeded={() => { onRestartNeeded(plugin.name); }}
/>
)));
}
function toggleEnabled() {
const wasEnabled = isEnabled();
@ -164,7 +153,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
infoButton={
<button
role="switch"
onClick={() => { openModal(); }}
onClick={() => { openPluginModal(plugin, onRestartNeeded); }}
className={classes(ButtonClasses.button, cl("info-button"))}
>
{plugin.options && !isObjectEmpty(plugin.options)
@ -348,8 +337,8 @@ export default function PluginSettings() {
Filters
</Forms.FormTitle>
<div className={cl("filter-controls")}>
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
<div className={classes(Margins.bottom20, cl("filter-controls"))}>
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} />
<div className={InputStyles.inputWrapper}>
<Select
options={[
@ -362,6 +351,7 @@ export default function PluginSettings() {
select={onStatusChange}
isSelected={v => v === searchValue.status}
closeOnSelect={true}
className={InputStyles.inputDefault}
/>
</div>
</div>

View file

@ -19,6 +19,7 @@
import { showNotification } from "@api/Notifications";
import { Settings, useSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Grid } from "@components/Grid";
import { Link } from "@components/Link";
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { Margins } from "@utils/margins";
@ -154,24 +155,38 @@ function CloudTab() {
}}
validate={validateUrl}
/>
<Button
className={Margins.top8}
size={Button.Sizes.MEDIUM}
color={Button.Colors.RED}
disabled={!settings.cloud.authenticated}
onClick={() => {
AlertActionCreators.show({
title: "Are you sure?",
body: "Once your data is erased, we cannot recover it. There's no going back!",
onConfirm: eraseAllData,
confirmText: "Erase it!",
confirmColor: "vc-cloud-erase-data-danger-btn",
cancelText: "Nevermind"
});
}}
>
Erase All Data
</Button>
<Grid columns={2} gap="1em" className={Margins.top8}>
<Button
size={Button.Sizes.MEDIUM}
disabled={!settings.cloud.authenticated}
onClick={async () => {
await deauthorizeCloud();
settings.cloud.authenticated = false;
await authorizeCloud();
}}
>
Reauthorise
</Button>
<Button
size={Button.Sizes.MEDIUM}
color={Button.Colors.RED}
disabled={!settings.cloud.authenticated}
onClick={() => {
AlertActionCreators.show({
title: "Are you sure?",
body: "Once your data is erased, we cannot recover it. There's no going back!",
onConfirm: eraseAllData,
confirmText: "Erase it!",
confirmColor: "vc-cloud-erase-data-danger-btn",
cancelText: "Nevermind"
});
}}
>
Erase All Data
</Button>
</Grid>
<Forms.FormDivider className={Margins.top16} />
</Forms.FormSection >
<SettingsSyncSection />

View file

@ -0,0 +1,107 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useSettings } from "@api/Settings";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Forms, Select, Slider, Text } from "@webpack/common";
import { ErrorCard } from "..";
export function NotificationSettings() {
const settings = useSettings().notifications;
return (
<div style={{ padding: "1em 0" }}>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{settings.useNative !== "never" && Notification?.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
</ErrorCard>
)}
<Forms.FormText className={Margins.bottom8}>
Some plugins may show you notifications. These come in two styles:
<ul>
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
</ul>
</Forms.FormText>
<Select
placeholder="Notification Style"
options={[
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
] satisfies ({ value: typeof settings["useNative"]; } & Record<string, any>)[]}
closeOnSelect={true}
select={v => settings.useNative = v}
isSelected={v => v === settings.useNative}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select
isDisabled={settings.useNative === "always"}
placeholder="Notification Position"
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
] satisfies ({ value: typeof settings["position"]; } & Record<string, any>)[]}
select={v => settings.position = v}
isSelected={v => v === settings.position}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
<Slider
disabled={settings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0}
maxValue={20_000}
initialValue={settings.timeout}
onValueChange={v => settings.timeout = v}
onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>
The amount of notifications to save in the log until old ones are removed.
Set to <code>0</code> to disable Notification log and <code></code> to never automatically remove old Notifications
</Forms.FormText>
<Slider
markers={[0, 25, 50, 75, 100, 200]}
minValue={0}
maxValue={200}
stickToMarkers={true}
initialValue={settings.logLimit}
onValueChange={v => settings.logLimit = v}
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
</div>
);
}
export function openNotificationSettingsModal() {
openModal(props => (
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Settings</Text>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent>
<NotificationSettings />
</ModalContent>
</ModalRoot>
));
}

View file

@ -17,16 +17,19 @@
*/
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { type Settings, useSettings } from "@api/Settings";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
import { ErrorCard } from "@components/ErrorCard";
import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { Button, Card, Forms, Select, Slider, Switch, useMemo } from "@webpack/common";
import { Button, Card, Forms, Select, Switch, TooltipContainer, useMemo } from "@webpack/common";
import type { ComponentType } from "react";
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
import { openNotificationSettingsModal } from "./NotificationSettings";
import { SettingsTab, wrapTab } from "./shared";
const cl = classNameFactory("vc-settings-");
@ -38,6 +41,18 @@ type KeysOfType<T extends object, Type> = keyof {
[Key in keyof T as T[Key] extends Type ? Key : never]: never;
};
const iconWithTooltip = (Icon: ComponentType<{ className?: string; }>, tooltip: string) => () => (
<TooltipContainer text={tooltip}>
<Icon className={cl("quick-actions-img")} />
</TooltipContainer>
);
const NotificationLogIcon = iconWithTooltip(LogIcon, "Open Notification Log");
const QuickCssIcon = iconWithTooltip(PaintbrushIcon, "Edit QuickCSS");
const RelaunchIcon = iconWithTooltip(RestartIcon, "Relaunch Discord");
const OpenSettingsDirIcon = iconWithTooltip(FolderIcon, "Open Settings Directory");
const OpenGithubIcon = iconWithTooltip(GithubIcon, "View Vencord's GitHub Repository");
function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
fallbackValue: "Loading..."
@ -78,7 +93,7 @@ function VencordSettings() {
!IS_WEB && {
key: "transparent",
title: "Enable window transparency.",
note: "You need a theme that supports transparency or this will do nothing. Will stop the window from being resizable. Requires a full restart"
note: "You need a theme that supports transparency or this will do nothing. WILL STOP THE WINDOW FROM BEING RESIZABLE!! Requires a full restart"
},
!IS_WEB && isWindows && {
key: "winCtrlQ",
@ -97,32 +112,41 @@ function VencordSettings() {
<DonateCard image={donateImage} />
<Forms.FormSection title="Quick Actions">
<Card className={cl("quick-actions-card")}>
{!IS_WEB && (
<Button
onClick={relaunch}
size={Button.Sizes.SMALL}>
Restart Client
</Button>
)}
<Button
onClick={openNotificationLogModal}
look={Button.Looks.BLANK}
>
<NotificationLogIcon />
</Button>
<Button
onClick={() => { VencordNative.quickCss.openEditor(); }}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}>
Open QuickCSS File
look={Button.Looks.BLANK}
>
<QuickCssIcon />
</Button>
{!IS_WEB && (
<Button
onClick={relaunch}
look={Button.Looks.BLANK}
>
<RelaunchIcon />
</Button>
)}
{!IS_WEB && (
<Button
onClick={() => { showItemInFolder(settingsDir); }}
size={Button.Sizes.SMALL}
disabled={settingsDirPending}>
Open Settings Folder
look={Button.Looks.BLANK}
disabled={settingsDirPending}
>
<OpenSettingsDirIcon />
</Button>
)}
<Button
onClick={() => { VencordNative.native.openExternal("https://github.com/Vendicated/Vencord"); }}
size={Button.Sizes.SMALL}
disabled={settingsDirPending}>
Open in GitHub
look={Button.Looks.BLANK}
disabled={settingsDirPending}
>
<OpenGithubIcon />
</Button>
</Card>
</Forms.FormSection>
@ -130,9 +154,17 @@ function VencordSettings() {
<Forms.FormDivider />
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
<Forms.FormText className={Margins.bottom20}>
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
<Forms.FormText className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
Hint: You can change the position of this settings section in the
{" "}<Button
look={Button.Looks.BLANK}
style={{ color: "var(--text-link)", display: "inline-block" }}
onClick={() => { openPluginModal(Vencord.Plugins.plugins.Settings!); }}
>
settings of the Settings plugin
</Button>!
</Forms.FormText>
{Switches.map(s => s && (
<Switch
key={s.key}
@ -210,95 +242,20 @@ function VencordSettings() {
serialize={identity} />
</>}
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
<Forms.FormSection className={Margins.top16} title="Vencord Notifications" tag="h5">
<Flex>
<Button onClick={openNotificationSettingsModal}>
Notification Settings
</Button>
<Button onClick={openNotificationLogModal}>
View Notification Log
</Button>
</Flex>
</Forms.FormSection>
</SettingsTab>
);
}
function NotificationSection({ settings }: { settings: typeof Settings["notifications"]; }) {
return (
<>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{settings.useNative !== "never" && Notification?.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
</ErrorCard>
)}
<Forms.FormText className={Margins.bottom8}>
Some plugins may show you notifications. These come in two styles:
<ul>
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
</ul>
</Forms.FormText>
<Select
placeholder="Notification Style"
options={[
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
] satisfies ({ value: typeof settings["useNative"]; } & Record<string, any>)[]}
closeOnSelect={true}
select={v => { settings.useNative = v; }}
isSelected={v => v === settings.useNative}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select
isDisabled={settings.useNative === "always"}
placeholder="Notification Position"
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
] satisfies ({ value: typeof settings["position"]; } & Record<string, any>)[]}
select={v => { settings.position = v; }}
isSelected={v => v === settings.position}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
<Slider
disabled={settings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0}
maxValue={20_000}
initialValue={settings.timeout}
onValueChange={v => { settings.timeout = v; }}
onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>
The amount of notifications to save in the log until old ones are removed.
Set to <code>0</code> to disable Notification log and <code></code> to never automatically remove old Notifications
</Forms.FormText>
<Slider
markers={[0, 25, 50, 75, 100, 200]}
minValue={0}
maxValue={200}
stickToMarkers={true}
initialValue={settings.logLimit}
onValueChange={v => { settings.logLimit = v; }}
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
<Button
onClick={openNotificationLogModal}
disabled={settings.logLimit === 0}
>
Open Notification Log
</Button>
</>
);
}
interface DonateCardProps {
image: string;
}

View file

@ -11,16 +11,25 @@
}
.vc-settings-quick-actions-card {
color: var(--text-normal);
padding: 1em;
display: flex;
gap: 1em;
align-items: center;
justify-content: space-evenly;
flex-grow: 1;
flex-flow: row wrap;
gap: 1em;
flex-wrap: wrap;
align-items: center;
margin-bottom: 1em;
}
.vc-settings-quick-actions-card button {
min-width: unset;
}
.vc-settings-quick-actions-img {
width: 30px;
height: 30px;
}
.vc-settings-donate {
display: flex;
flex-direction: row;

View file

@ -77,8 +77,7 @@ export default definePlugin({
// Because of that, its WebpackInstance doesnt export wreq.m or wreq.c
// To circuvent this and disable Sentry we are gonna hook when wreq.g of its WebpackInstance is set.
// When that happens we are gonna obtain a reference to its internal module cache (wreq.c) and proxy its prototype,
// so, when the first require to initialize the Sentry is attempted, we are gonna forcefully throw an error and abort everything.
// When that happens we are gonna forcefully throw an error and abort everything.
Object.defineProperty(Function.prototype, "g", {
configurable: true,
@ -93,36 +92,30 @@ export default definePlugin({
// Ensure this is most likely the Sentry WebpackInstance.
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it
const { stack } = new Error();
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || this.c != null || !String(this).includes("exports:{}")) {
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) {
return;
}
const cacheExtractSym = Symbol("vencord.cacheExtract");
Object.defineProperty(Object.prototype, cacheExtractSym, {
configurable: true,
const assetPath = stack.match(/\/assets\/.+?\.js/)?.[0];
if (!assetPath) {
return;
}
get() {
// One more condition to check if this is the Sentry WebpackInstance
if (Array.isArray(this)) {
return { exports: {} };
}
const srcRequest = new XMLHttpRequest();
srcRequest.open("GET", assetPath, false);
srcRequest.send();
new Logger("NoTrack", "#8caaee").info("Disabling Sentry by proxying its WebpackInstance cache");
Object.setPrototypeOf(this, new Proxy(this, {
get() {
throw new Error("Sentry successfully disabled");
}
}));
// Final condition to see if this is the Sentry WebpackInstance
if (!srcRequest.responseText.includes("window.DiscordSentry=")) {
return;
}
Reflect.deleteProperty(Function.prototype, "g");
Reflect.deleteProperty(Object.prototype, cacheExtractSym);
Reflect.deleteProperty(window, "DiscordSentry");
return { exports: {} };
}
});
new Logger("NoTrack", "#8caaee").info("Disabling Sentry by erroring its WebpackInstance");
// WebpackRequire our fake module id
this(cacheExtractSym);
Reflect.deleteProperty(Function.prototype, "g");
Reflect.deleteProperty(window, "DiscordSentry");
throw new Error("Sentry successfully disabled");
}
});

View file

@ -17,6 +17,7 @@
*/
import { addAccessory } from "@api/MessageAccessories";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
@ -32,13 +33,13 @@ import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { AlertActionCreators, Button, Card, ChannelStore, Forms, GuildMemberStore, MarkupUtils, RelationshipStore, showToast, Toasts, UserStore } from "@webpack/common";
import { AlertActionCreators, Button, Card, ChannelStore, Forms, GuildMemberStore, MarkupUtils, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import type { JSX } from "react";
import gitHash from "~git-hash";
import plugins, { PluginMeta } from "~plugins";
import settings from "./settings";
import SettingsPlugin from "./settings";
const VENCORD_GUILD_ID = "1015060230222131221";
const VENBOT_USER_ID = "1017176847865352332";
@ -87,7 +88,7 @@ async function generateDebugInfoMessage() {
const info: Record<"Vencord" | "Client" | "Platform", string> & { "Last Crash Reason"?: string; } = {
Vencord:
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
`${SettingsPlugin.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
Client: `${RELEASE_CHANNEL} ~ ${client}`,
Platform: window.navigator.platform,
};
@ -133,6 +134,10 @@ function generatePluginList() {
const checkForUpdatesOnce = onlyOnce(checkForUpdates);
const settings = definePluginSettings({}).withPrivateSettings<{
dismissedDevBuildWarning?: boolean;
}>();
export default definePlugin({
name: "SupportHelper",
required: true,
@ -140,6 +145,8 @@ export default definePlugin({
authors: [Devs.Ven],
dependencies: ["CommandsAPI", "UserSettingsAPI", "MessageAccessoriesAPI"],
settings,
patches: [{
find: ".BEGINNING_DM.format",
replacement: {
@ -213,19 +220,22 @@ export default definePlugin({
return;
}
const repo = await VencordNative.updater.getRepo();
if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
if (!IS_STANDALONE && !settings.store.dismissedDevBuildWarning) {
AlertActionCreators.show({
title: "Hold on!",
body: (
<div>
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
contact your package maintainer for support instead.
</Forms.FormText>
</div>
)
body: <div>
<Forms.FormText>You are using a custom build of Vencord, which we do not provide support for!</Forms.FormText>
<Forms.FormText className={Margins.top8}>
We only provide support for <Link href="https://vencord.dev/download">official builds</Link>.
Either <Link href="https://vencord.dev/download">switch to an official build</Link> or figure your issue out yourself.
</Forms.FormText>
<Text variant="text-md/bold" className={Margins.top8}>You will be banned from receiving support if you ignore this rule.</Text>
</div>,
confirmText: "Understood",
secondaryConfirmText: "Don't show again",
onConfirmSecondary: () => { settings.store.dismissedDevBuildWarning = true; }
});
return;
}

View file

@ -4,48 +4,62 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import { classes } from "@utils/misc";
import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { findByCodeLazy, findByPropsLazy, findLazy } from "@webpack";
import { Heading, i18n, RelationshipStore, Text } from "@webpack/common";
const container: Record<string, string> = findByPropsLazy("memberSinceWrapper");
const containerWrapper: Record<string, string> = findByPropsLazy("memberSinceWrapper");
const container: Record<string, string> = findByPropsLazy("memberSince");
const getCreatedAtDate = findByCodeLazy('month:"short",day:"numeric"');
const lastSection: Record<string, string> = findByPropsLazy("lastSection");
const cl = classNameFactory("vc-friendssince-");
const section: Record<string, string> = findLazy(m => m.section !== undefined && Object.keys(m).length === 1);
export default definePlugin({
name: "FriendsSince",
description: "Shows when you became friends with someone in the user popout",
authors: [Devs.Elvyra],
authors: [Devs.Elvyra, Devs.Antti],
patches: [
// User popup
// User popup - old layout
{
find: ".USER_PROFILE}};return",
replacement: {
match: /,{userId:(\i.id).{0,30}}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })"
replace: "$&,$self.friendsSinceOld({ userId: $1 })"
}
},
// User DMs "User Profile" popup in the right
// DM User Sidebar - old layout
{
find: ".PROFILE_PANEL,",
replacement: {
match: /,{userId:([^,]+?)}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })"
replace: "$&,$self.friendsSinceOld({ userId: $1 })"
}
},
// User Profile Modal
// User Profile Modal - old layout
{
find: ".userInfoSectionHeader,",
replacement: {
match: /(\.Messages\.USER_PROFILE_MEMBER_SINCE.+?userId:(.+?),textClassName:)(\i\.userInfoText)}\)/,
replace: (_, rest, userId, textClassName) => `${rest}!$self.getFriendSince(${userId}) ? ${textClassName} : void 0 }), $self.friendsSince({ userId: ${userId}, textClassName: ${textClassName} })`
replace: (_, rest, userId, textClassName) => `${rest}!$self.getFriendSince(${userId}) ? ${textClassName} : void 0 }), $self.friendsSinceOld({ userId: ${userId}, textClassName: ${textClassName} })`
}
},
// DM User Sidebar - new layout
{
find: ".PANEL}),nicknameIcons",
replacement: {
match: /USER_PROFILE_MEMBER_SINCE,.{0,100}userId:(\i\.id)}\)}\)/,
replace: "$&,$self.friendsSinceNew({userId:$1,isSidebar:true})"
}
},
// User Profile Modal - new layout
{
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}),"
}
}
],
@ -54,7 +68,7 @@ export default definePlugin({
? RelationshipStore.getSince(userId)
: null,
friendsSince: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => {
friendsSinceOld: ErrorBoundary.wrap(({ userId, textClassName }: { userId: string; textClassName?: string; }) => {
if (!RelationshipStore.isFriend(userId)) return null;
const friendsSince = RelationshipStore.getSince(userId);
@ -62,11 +76,11 @@ export default definePlugin({
return (
<div className={lastSection.section}>
<Heading variant="eyebrow" className={cl("title")}>
<Heading variant="eyebrow">
Friends Since
</Heading>
<div className={container.memberSinceWrapper}>
<div className={containerWrapper.memberSinceWrapper}>
{!!getCurrentChannel()?.guild_id && (
<svg
aria-hidden="true"
@ -79,11 +93,54 @@ export default definePlugin({
<path d="M3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" />
</svg>
)}
<Text variant="text-sm/normal" className={classes(cl("body"), textClassName)}>
<Text variant="text-sm/normal" className={textClassName}>
{getCreatedAtDate(friendsSince, i18n.getLocale())}
</Text>
</div>
</div>
);
}, { noop: true })
}, { noop: true }),
friendsSinceNew: 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>
{
isSidebar ? (
<Text variant="text-sm/normal">
{getCreatedAtDate(friendsSince, i18n.getLocale())}
</Text>
) : (
<div className={containerWrapper.memberSinceWrapper}>
<div className={container.memberSince}>
{!!getCurrentChannel()?.guild_id && (
<svg
aria-hidden="true"
width="16"
height="16"
viewBox="0 0 24 24"
fill="var(--interactive-normal)"
>
<path d="M13 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM3 5v-.75C3 3.56 3.56 3 4.25 3s1.24.56 1.33 1.25C6.12 8.65 9.46 12 13 12h1a8 8 0 0 1 8 8 2 2 0 0 1-2 2 .21.21 0 0 1-.2-.15 7.65 7.65 0 0 0-1.32-2.3c-.15-.2-.42-.06-.39.17l.25 2c.02.15-.1.28-.25.28H9a2 2 0 0 1-2-2v-2.22c0-1.57-.67-3.05-1.53-4.37A15.85 15.85 0 0 1 3 5Z" />
</svg>
)}
<Text variant="text-sm/normal">
{getCreatedAtDate(friendsSince, i18n.getLocale())}
</Text>
</div>
</div>
)
}
</section>
);
}, { noop: true }),
});

View file

@ -1,12 +0,0 @@
/* copy pasted from discord */
.vc-friendssince-title {
display: flex;
font-weight: 700;
margin-bottom: 6px
}
.vc-friendssince-body {
font-size: 14px;
line-height: 18px
}

View file

@ -0,0 +1,6 @@
# ShowAllRoles
Display all roles on the new profiles instead of limiting them to the default two rows.
![image](https://github.com/Vendicated/Vencord/assets/71079641/3f021f03-c6f9-4fe5-83ac-a1891b5e4b37)

View file

@ -0,0 +1,23 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "ShowAllRoles",
description: "Show all roles in new profiles.",
authors: [Devs.Luna],
patches: [
{
find: ".Messages.VIEW_ALL_ROLES",
replacement: {
match: /return null!=\i(?=\?\i\.slice)/,
replace: "return false"
}
}
]
});

View file

@ -25,7 +25,7 @@ import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType, type Patch } from "@utils/types";
import type { ChannelRecord, GuildCategoryChannelRecord, GuildChannel, GuildChannelRecord, Role } from "@vencord/discord-types";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, Permissions, PermissionStore, Tooltip } from "@webpack/common";
import { Permissions, PermissionStore, Tooltip } from "@webpack/common";
import HiddenChannelLockScreen from "./components/HiddenChannelLockScreen";
@ -476,14 +476,11 @@ export default definePlugin({
}
],
isHiddenChannel(channel?: ChannelRecord & { channelId?: string; }, checkConnect = false) {
if (!channel) return false;
if (channel.channelId)
channel = ChannelStore.getChannel(channel.channelId);
isHiddenChannel(channel?: ChannelRecord, checkConnect = false) {
if (!channel || channel.isPrivate()) return false;
return !PermissionStore.can(Permissions.VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(Permissions.CONNECT, channel);
return !PermissionStore.can(Permissions.VIEW_CHANNEL, channel)
|| checkConnect && !PermissionStore.can(Permissions.CONNECT, channel);
},
resolveGuildChannels(channels: Record<string | number, GuildChannel[] | string | number>, shouldIncludeHidden: boolean) {

View file

@ -199,6 +199,34 @@ export default definePlugin({
match: /supports\(\i\)\{switch\(\i\)\{(case (\i).\i)/,
replace: "$&.DISABLE_VIDEO:return true;$1"
}
},
{
find: ".Messages.SEARCH_WITH_GOOGLE",
replacement: {
match: /\i\.isPlatformEmbedded/,
replace: "true"
}
},
{
find: ".Messages.COPY,hint:",
replacement: [
{
match: /\i\.isPlatformEmbedded/,
replace: "true"
},
{
match: /\i\.\i\.copy/,
replace: "Vencord.Webpack.Common.Clipboard.copy"
}]
},
// Automod add filter words
{
find: '("interactionUsernameProfile',
replacement:
{
match: /\i\.isPlatformEmbedded(?=.{0,50}\.tagName)/,
replace: "true"
},
}
],

View file

@ -529,6 +529,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
nekohaxx: {
name: "nekohaxx",
id: 1176270221628153886n
},
Antti: {
name: "Antti",
id: 312974985876471810n
}
} satisfies Record<string, Dev>);