Compare commits
40 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
03915b7533 | ||
|
5e2ec368ad | ||
|
ab8c93fbac | ||
|
d6a3edefd9 | ||
|
727297ec4e | ||
|
eccc4b0be1 | ||
|
8465140bc4 | ||
|
e6ccb751a0 | ||
|
dfc7a15083 | ||
|
37003edae9 | ||
|
faa90eccd3 | ||
|
c91b0df607 | ||
|
f56d99e133 | ||
|
c690662802 | ||
|
4918d699d5 | ||
|
5ec517875e | ||
|
cf56ad985b | ||
|
c09d1558f7 | ||
|
eb190b660e | ||
|
d6f9068695 | ||
|
cb507babaa | ||
|
235d114193 | ||
|
9aba70dcb1 | ||
|
0b61d29c31 | ||
|
335a13a38a | ||
|
128ee41252 | ||
|
ccca41a168 | ||
|
af4c7d8a90 | ||
|
77c691651e | ||
|
e14ec96e21 | ||
|
ff1f337699 | ||
|
3ca87848e5 | ||
|
9420735bc7 | ||
|
6807820f6c | ||
|
3cad0d60b4 | ||
|
fbbc198b1b | ||
|
224ae979f2 | ||
|
27fc20118b | ||
|
60ccd8cc25 | ||
|
5c1519156b |
10
README.md
10
README.md
@ -4,12 +4,14 @@ The cutest Discord client mod
|
||||
|
||||
## Features
|
||||
|
||||
- Super easy to install (one click installer)
|
||||
- 90+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||
- Super easy to install (Download Installer, open, click install button, done)
|
||||
- 100+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
|
||||
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
|
||||
- Fairly lightweight despite the many inbuilt plugins
|
||||
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
|
||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
||||
- Works in all Electron versions (Confirmed working on versions 13-23)
|
||||
- Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
|
||||
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
||||
|
||||
## Installing / Uninstalling
|
||||
@ -20,7 +22,7 @@ The cutest Discord client mod
|
||||
|
||||
[![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
|
||||
|
||||
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that QuickCSS and plugins making use of external resources will not work with the UserScript.
|
||||
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that the CSS Editor, Themes loaded from remote sources and co. will not work in the UserScript. Use the extension if you need any of those
|
||||
|
||||
## Building from Source
|
||||
|
||||
|
@ -92,6 +92,7 @@ function GM_fetch(url, opt) {
|
||||
resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
|
||||
resp.text = () => blobTo("text", blob);
|
||||
resp.json = async () => JSON.parse(await blobTo("text", blob));
|
||||
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
|
||||
resolve(resp);
|
||||
};
|
||||
options.ontimeout = () => reject("fetch timeout");
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.0.5",
|
||||
"version": "1.1.0",
|
||||
"description": "The cutest Discord client mod",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
|
@ -95,3 +95,12 @@ async function init() {
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
if (!IS_WEB && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.head.append(Object.assign(document.createElement("style"), {
|
||||
id: "vencord-native-titlebar-style",
|
||||
textContent: "[class*=titleBar-]{display: none!important}"
|
||||
}));
|
||||
}, { once: true });
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { User } from "discord-types/general";
|
||||
import { ComponentType, HTMLProps } from "react";
|
||||
|
||||
@ -52,6 +53,7 @@ const Badges = new Set<ProfileBadge>();
|
||||
* @param badge The badge to register
|
||||
*/
|
||||
export function addBadge(badge: ProfileBadge) {
|
||||
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
|
||||
Badges.add(badge);
|
||||
}
|
||||
|
||||
|
@ -89,4 +89,6 @@ export default ErrorBoundary.wrap(function NotificationComponent({
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}, {
|
||||
onError: ({ props }) => props.onClose!()
|
||||
});
|
||||
|
@ -34,6 +34,7 @@ export interface Settings {
|
||||
frameless: boolean;
|
||||
transparent: boolean;
|
||||
winCtrlQ: boolean;
|
||||
winNativeTitleBar: boolean;
|
||||
plugins: {
|
||||
[plugin: string]: {
|
||||
enabled: boolean;
|
||||
@ -57,6 +58,7 @@ const DefaultSettings: Settings = {
|
||||
frameless: false,
|
||||
transparent: false,
|
||||
winCtrlQ: false,
|
||||
winNativeTitleBar: false,
|
||||
plugins: {},
|
||||
|
||||
notifications: {
|
||||
@ -90,7 +92,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
|
||||
// Return empty for plugins with no settings
|
||||
if (path === "plugins" && p in plugins)
|
||||
return target[p] = makeProxy({
|
||||
enabled: plugins[p].required ?? false
|
||||
enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
|
||||
}, root, `plugins.${p}`);
|
||||
|
||||
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
|
||||
|
@ -17,20 +17,24 @@
|
||||
*/
|
||||
|
||||
import Logger from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { LazyComponent } from "@utils/misc";
|
||||
import { Margins, React } from "@webpack/common";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import { ErrorCard } from "./ErrorCard";
|
||||
|
||||
interface Props {
|
||||
interface Props<T = any> {
|
||||
/** Render nothing if an error occurs */
|
||||
noop?: boolean;
|
||||
/** Fallback component to render if an error occurs */
|
||||
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
|
||||
/** called when an error occurs */
|
||||
onError?(error: Error, errorInfo: React.ErrorInfo): void;
|
||||
/** called when an error occurs. The props property is only available if using .wrap */
|
||||
onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
|
||||
/** Custom error message */
|
||||
message?: string;
|
||||
|
||||
/** The props passed to the wrapped component. Only used by wrap */
|
||||
wrappedProps?: T;
|
||||
}
|
||||
|
||||
const color = "#e78284";
|
||||
@ -65,7 +69,7 @@ const ErrorBoundary = LazyComponent(() => {
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
this.props.onError?.(error, errorInfo);
|
||||
this.props.onError?.({ error, errorInfo, props: this.props.wrappedProps });
|
||||
logger.error("A component threw an Error\n", error);
|
||||
logger.error("Component Stack", errorInfo.componentStack);
|
||||
}
|
||||
@ -84,15 +88,13 @@ const ErrorBoundary = LazyComponent(() => {
|
||||
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
|
||||
|
||||
return (
|
||||
<ErrorCard style={{
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<ErrorCard style={{ overflow: "hidden" }}>
|
||||
<h1>Oh no!</h1>
|
||||
<p>{msg}</p>
|
||||
<code>
|
||||
{this.state.message}
|
||||
{!!this.state.stack && (
|
||||
<pre className={Margins.marginTop8}>
|
||||
<pre className={Margins.top8}>
|
||||
{this.state.stack}
|
||||
</pre>
|
||||
)}
|
||||
@ -103,11 +105,11 @@ const ErrorBoundary = LazyComponent(() => {
|
||||
};
|
||||
}) as
|
||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
|
||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.ComponentType<T>;
|
||||
};
|
||||
|
||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||
<ErrorBoundary {...errorBoundaryProps}>
|
||||
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
|
||||
<Component {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
7
src/components/ErrorCard.css
Normal file
7
src/components/ErrorCard.css
Normal file
@ -0,0 +1,7 @@
|
||||
.vc-error-card {
|
||||
padding: 2em;
|
||||
background-color: #e7828430;
|
||||
border: 1px solid #e78284;
|
||||
border-radius: 5px;
|
||||
color: var(--text-normal, white);
|
||||
}
|
@ -16,24 +16,15 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Card } from "@webpack/common";
|
||||
import "./ErrorCard.css";
|
||||
|
||||
interface Props {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
}
|
||||
export function ErrorCard(props: React.PropsWithChildren<Props>) {
|
||||
import { classes } from "@utils/misc";
|
||||
import type { HTMLProps } from "react";
|
||||
|
||||
export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
|
||||
return (
|
||||
<Card className={props.className} style={
|
||||
{
|
||||
padding: "2em",
|
||||
backgroundColor: "#e7828430",
|
||||
borderColor: "#e78284",
|
||||
color: "var(--text-normal)",
|
||||
...props.style
|
||||
}
|
||||
}>
|
||||
<div {...props} className={classes(props.className, "vc-error-card")}>
|
||||
{props.children}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -17,10 +17,11 @@
|
||||
*/
|
||||
|
||||
import { debounce } from "@utils/debounce";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { makeCodeblock } from "@utils/misc";
|
||||
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
|
||||
import { search } from "@webpack";
|
||||
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
||||
import { Button, Clipboard, Forms, Parser, React, Switch, Text, TextInput } from "@webpack/common";
|
||||
|
||||
import { CheckedTextInput } from "./CheckedTextInput";
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
@ -128,7 +129,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
||||
)}
|
||||
|
||||
{!!diff?.length && (
|
||||
<Button className={Margins.marginTop20} onClick={() => {
|
||||
<Button className={Margins.top20} onClick={() => {
|
||||
try {
|
||||
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
|
||||
setCompileResult([true, "Compiled successfully"]);
|
||||
@ -202,7 +203,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||
)}
|
||||
|
||||
<Switch
|
||||
className={Margins.marginTop8}
|
||||
className={Margins.top8}
|
||||
value={isFunc}
|
||||
onChange={setIsFunc}
|
||||
note="'replacement' will be evaled if this is toggled"
|
||||
@ -256,7 +257,7 @@ function PatchHelper() {
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
|
||||
<Text variant="heading-md/normal" tag="h2" className={Margins.bottom8}>Patch Helper</Text>
|
||||
<Forms.FormTitle>find</Forms.FormTitle>
|
||||
<TextInput
|
||||
type="text"
|
||||
@ -296,7 +297,7 @@ function PatchHelper() {
|
||||
|
||||
{!!(find && match && replacement) && (
|
||||
<>
|
||||
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle>
|
||||
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
|
||||
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
|
||||
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
|
||||
</>
|
||||
|
@ -30,11 +30,12 @@ import PluginModal from "@components/PluginSettings/PluginModal";
|
||||
import { Switch } from "@components/Switch";
|
||||
import { ChangeList } from "@utils/ChangeList";
|
||||
import Logger from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes, LazyComponent, useAwaiter } from "@utils/misc";
|
||||
import { openModalLazy } from "@utils/modal";
|
||||
import { Plugin } from "@utils/types";
|
||||
import { findByCode, findByPropsLazy } from "@webpack";
|
||||
import { Alerts, Button, Card, Forms, Margins, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||
import { Alerts, Button, Card, Forms, Parser, React, Select, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
||||
@ -296,15 +297,15 @@ export default ErrorBoundary.wrap(function PluginSettings() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Forms.FormSection className={Margins.marginTop16}>
|
||||
<Forms.FormSection className={Margins.top16}>
|
||||
<ReloadRequiredCard required={changes.hasChanges} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||
Filters
|
||||
</Forms.FormTitle>
|
||||
|
||||
<div className={cl("filter-controls")}>
|
||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.marginBottom20} />
|
||||
<TextInput autoFocus value={searchValue.value} placeholder="Search for a plugin..." onChange={onSearch} className={Margins.bottom20} />
|
||||
<div className={InputStyles.inputWrapper}>
|
||||
<Select
|
||||
className={InputStyles.inputDefault}
|
||||
@ -321,15 +322,15 @@ export default ErrorBoundary.wrap(function PluginSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
|
||||
<Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
|
||||
|
||||
<div className={cl("grid")}>
|
||||
{plugins}
|
||||
</div>
|
||||
|
||||
<Forms.FormDivider className={Margins.marginTop20} />
|
||||
<Forms.FormDivider className={Margins.top20} />
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.top20, Margins.bottom8)}>
|
||||
Required Plugins
|
||||
</Forms.FormTitle>
|
||||
<div className={cl("grid")}>
|
||||
|
@ -26,8 +26,8 @@ interface SwitchProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SWITCH_ON = "var(--status-green-600)";
|
||||
const SWITCH_OFF = "var(--primary-dark-400)";
|
||||
const SWITCH_ON = "var(--green-360)";
|
||||
const SWITCH_OFF = "var(--primary-400)";
|
||||
const SwitchClasses = findByPropsLazy("slider", "input", "container");
|
||||
|
||||
export function Switch({ checked, onChange, disabled }: SwitchProps) {
|
||||
|
@ -18,25 +18,26 @@
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
|
||||
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
|
||||
import { Button, Card, Forms, Text } from "@webpack/common";
|
||||
|
||||
function BackupRestoreTab() {
|
||||
return (
|
||||
<Forms.FormSection title="Settings Sync" className={Margins.marginTop16}>
|
||||
<Forms.FormSection title="Settings Sync" className={Margins.top16}>
|
||||
<Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
|
||||
<Flex flexDirection="column">
|
||||
<strong>Warning</strong>
|
||||
<span>Importing a settings file will overwrite your current settings.</span>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
||||
<Text variant="text-md/normal" className={Margins.bottom8}>
|
||||
You can import and export your Vencord settings as a JSON file.
|
||||
This allows you to easily transfer your settings to another device,
|
||||
or recover your settings after reinstalling Vencord or Discord.
|
||||
</Text>
|
||||
<Text variant="text-md/normal" className={Margins.marginBottom8}>
|
||||
<Text variant="text-md/normal" className={Margins.bottom8}>
|
||||
Settings Export contains:
|
||||
<ul>
|
||||
<li>— Custom QuickCSS</li>
|
||||
|
@ -19,9 +19,10 @@
|
||||
import { useSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Link } from "@components/Link";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { useAwaiter } from "@utils/misc";
|
||||
import { findLazy } from "@webpack";
|
||||
import { Card, Forms, Margins, React, TextArea } from "@webpack/common";
|
||||
import { Card, Forms, React, TextArea } from "@webpack/common";
|
||||
|
||||
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
|
||||
|
||||
@ -51,7 +52,7 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle className={Margins.marginTop20} tag="h5">Validator</Forms.FormTitle>
|
||||
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
|
||||
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
|
||||
<div>
|
||||
{themeLinks.map(link => (
|
||||
@ -93,7 +94,7 @@ export default ErrorBoundary.wrap(function () {
|
||||
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
|
||||
<Forms.FormText>One link per line</Forms.FormText>
|
||||
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
|
||||
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
||||
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
|
||||
<div style={{ marginBottom: ".5em" }}>
|
||||
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
|
||||
|
@ -22,9 +22,10 @@ import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { Link } from "@components/Link";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes, useAwaiter } from "@utils/misc";
|
||||
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
|
||||
import { Alerts, Button, Card, Forms, Margins, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
|
||||
@ -109,14 +110,14 @@ function Updatable(props: CommonProps) {
|
||||
</ErrorCard>
|
||||
</>
|
||||
) : (
|
||||
<Forms.FormText className={Margins.marginBottom8}>
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
|
||||
</Forms.FormText>
|
||||
)}
|
||||
|
||||
{isOutdated && <Changes updates={updates} {...props} />}
|
||||
|
||||
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
|
||||
<Flex className={classes(Margins.bottom8, Margins.top8)}>
|
||||
{isOutdated && <Button
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={isUpdating || isChecking}
|
||||
@ -175,7 +176,7 @@ function Updatable(props: CommonProps) {
|
||||
function Newer(props: CommonProps) {
|
||||
return (
|
||||
<>
|
||||
<Forms.FormText className={Margins.marginBottom8}>
|
||||
<Forms.FormText className={Margins.bottom8}>
|
||||
Your local copy has more recent commits. Please stash or reset them.
|
||||
</Forms.FormText>
|
||||
<Changes {...props} updates={changes} />
|
||||
@ -199,7 +200,7 @@ function Updater() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Forms.FormSection className={Margins.marginTop16}>
|
||||
<Forms.FormSection className={Margins.top16}>
|
||||
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
|
||||
<Switch
|
||||
value={settings.notifyAboutUpdates}
|
||||
@ -225,7 +226,7 @@ function Updater() {
|
||||
</Link>
|
||||
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText>
|
||||
|
||||
<Forms.FormDivider className={Margins.marginTop8 + " " + Margins.marginBottom8} />
|
||||
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
|
||||
|
||||
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
|
||||
|
||||
|
@ -63,11 +63,15 @@ function VencordSettings() {
|
||||
title: "Enable React Developer Tools",
|
||||
note: "Requires a full restart"
|
||||
},
|
||||
!IS_WEB && !isWindows && {
|
||||
!IS_WEB && (!isWindows ? {
|
||||
key: "frameless",
|
||||
title: "Disable the window frame",
|
||||
note: "Requires a full restart"
|
||||
},
|
||||
} : {
|
||||
key: "winNativeTitleBar",
|
||||
title: "Use Windows' native title bar instead of Discord's custom one",
|
||||
note: "Requires a full restart"
|
||||
}),
|
||||
!IS_WEB && {
|
||||
key: "transparent",
|
||||
title: "Enable window transparency",
|
||||
|
@ -20,6 +20,7 @@ import "./settingsStyles.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { handleComponentFailed } from "@components/handleComponentFailed";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { Forms, SettingsRouter, Text } from "@webpack/common";
|
||||
|
||||
@ -61,8 +62,8 @@ function Settings(props: SettingsProps) {
|
||||
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
|
||||
|
||||
<TabBar
|
||||
type={TabBar.Types.TOP}
|
||||
look={TabBar.Looks.BRAND}
|
||||
type="top"
|
||||
look="brand"
|
||||
className={cl("tab-bar")}
|
||||
selectedItem={tab}
|
||||
onItemSelect={SettingsRouter.open}
|
||||
@ -83,7 +84,7 @@ function Settings(props: SettingsProps) {
|
||||
}
|
||||
|
||||
export default function (props: SettingsProps) {
|
||||
return <ErrorBoundary>
|
||||
return <ErrorBoundary onError={handleComponentFailed}>
|
||||
<Settings tab={props.tab} />
|
||||
</ErrorBoundary>;
|
||||
}
|
||||
|
@ -16,29 +16,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { isOutdated, rebuild, update } from "@utils/updater";
|
||||
import { maybePromptToUpdate } from "@utils/updater";
|
||||
|
||||
export async function handleComponentFailed() {
|
||||
if (isOutdated) {
|
||||
setImmediate(async () => {
|
||||
const wantsUpdate = confirm(
|
||||
export function handleComponentFailed() {
|
||||
maybePromptToUpdate(
|
||||
"Uh Oh! Failed to render this Page." +
|
||||
" However, there is an update available that might fix it." +
|
||||
" Would you like to update and restart now?"
|
||||
);
|
||||
if (wantsUpdate) {
|
||||
try {
|
||||
await update();
|
||||
await rebuild();
|
||||
if (IS_WEB)
|
||||
location.reload();
|
||||
else
|
||||
DiscordNative.app.relaunch();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("That also failed :( Try updating or reinstalling with the installer!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,8 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "preload.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false
|
||||
nodeIntegration: false,
|
||||
sandbox: false
|
||||
}
|
||||
});
|
||||
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
|
||||
|
@ -79,7 +79,10 @@ if (!process.argv.includes("--vanilla")) {
|
||||
options.webPreferences.sandbox = false;
|
||||
if (settings.frameless) {
|
||||
options.frame = false;
|
||||
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
|
||||
delete options.frame;
|
||||
}
|
||||
|
||||
if (settings.transparent) {
|
||||
options.transparent = true;
|
||||
options.backgroundColor = "#00000000";
|
||||
|
56
src/plugins/FixInbox.tsx
Normal file
56
src/plugins/FixInbox.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Forms } from "@webpack/common";
|
||||
|
||||
export default definePlugin({
|
||||
name: "FixInbox",
|
||||
description: "Fixes the Unreads Inbox from crashing Discord when you're in lots of guilds.",
|
||||
authors: [Devs.Megu],
|
||||
|
||||
patches: [{
|
||||
find: "INBOX_OPEN:function",
|
||||
replacement: {
|
||||
// This function normally dispatches a subscribe event to every guild.
|
||||
// this is badbadbadbadbad so we just get rid of it.
|
||||
match: /INBOX_OPEN:function.+?\{/,
|
||||
replace: "$&return true;"
|
||||
}
|
||||
}],
|
||||
|
||||
settingsAboutComponent() {
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">What's the problem?</Forms.FormTitle>
|
||||
<Forms.FormText style={{ marginBottom: 8 }}>
|
||||
By default, Discord emits a GUILD_SUBSCRIPTIONS event for every guild you're in.
|
||||
When you're in a lot of guilds, this can cause the gateway to ratelimit you.
|
||||
This causes the client to crash and get stuck in an infinite ratelimit loop as it tries to reconnect.
|
||||
</Forms.FormText>
|
||||
|
||||
<Forms.FormTitle tag="h3">How does it work?</Forms.FormTitle>
|
||||
<Forms.FormText>
|
||||
This plugin works by stopping the client from sending GUILD_SUBSCRIPTIONS events to the gateway when you open the unreads inbox.
|
||||
This means that not all unreads will be shown, instead only already-subscribed guilds' unreads will be shown, but your client won't crash anymore.
|
||||
</Forms.FormText>
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
||||
});
|
@ -24,9 +24,10 @@ import { Heart } from "@components/Heart";
|
||||
import { Devs } from "@utils/constants";
|
||||
import IpcEvents from "@utils/IpcEvents";
|
||||
import Logger from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { closeModal, Modals, openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Forms, Margins } from "@webpack/common";
|
||||
import { Forms } from "@webpack/common";
|
||||
|
||||
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
|
||||
|
||||
@ -150,7 +151,7 @@ export default definePlugin({
|
||||
<Forms.FormText>
|
||||
This Badge is a special perk for Vencord Donors
|
||||
</Forms.FormText>
|
||||
<Forms.FormText className={Margins.marginTop20}>
|
||||
<Forms.FormText className={Margins.top20}>
|
||||
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
|
||||
</Forms.FormText>
|
||||
</div>
|
||||
|
@ -43,7 +43,7 @@ export default definePlugin({
|
||||
{
|
||||
find: '"Menu API',
|
||||
replacement: {
|
||||
match: /function.{0,80}type===(.{1,3})\..{1,3}\).{0,50}navigable:.+?Menu API/s,
|
||||
match: /function.{0,80}type===(\i)\).{0,50}navigable:.+?Menu API/s,
|
||||
replace: (m, mod) => {
|
||||
let nicenNames = "";
|
||||
const redefines = [] as string[];
|
||||
|
37
src/plugins/colorSighted.ts
Normal file
37
src/plugins/colorSighted.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "ColorSighted",
|
||||
description: "Removes the colorblind-friendly icons from statuses, just like 2015-2017 Discord",
|
||||
authors: [Devs.lewisakura],
|
||||
patches: [
|
||||
{
|
||||
find: "Masks.STATUS_ONLINE",
|
||||
replacement: {
|
||||
// we can use global replacement here - these are specific to the status icons and are used nowhere else,
|
||||
// so it keeps the patch and plugin small and simple
|
||||
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
|
||||
replace: "Masks.STATUS_ONLINE"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
@ -18,6 +18,9 @@
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import * as Webpack from "@webpack";
|
||||
import { extract, filters, findAll, search } from "@webpack";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
const WEB_ONLY = (f: string) => () => {
|
||||
throw new Error(`'${f}' is Discord Desktop only.`);
|
||||
@ -29,19 +32,48 @@ export default definePlugin({
|
||||
authors: [Devs.Ven],
|
||||
|
||||
getShortcuts() {
|
||||
function newFindWrapper(filterFactory: (props: any) => Webpack.FilterFn) {
|
||||
const cache = new Map<string, any>();
|
||||
|
||||
return function (filterProps: any) {
|
||||
const cacheKey = String(filterProps);
|
||||
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
||||
|
||||
const matches = findAll(filterFactory(filterProps));
|
||||
|
||||
const result = (() => {
|
||||
switch (matches.length) {
|
||||
case 0: return null;
|
||||
case 1: return matches[0];
|
||||
default:
|
||||
const uniqueMatches = [...new Set(matches)];
|
||||
if (uniqueMatches.length > 1)
|
||||
console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches);
|
||||
|
||||
return matches[0];
|
||||
}
|
||||
})();
|
||||
if (result && cacheKey) cache.set(cacheKey, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
toClip: IS_WEB ? WEB_ONLY("toClip") : window.DiscordNative.clipboard.copy,
|
||||
fromClip: IS_WEB ? WEB_ONLY("fromClip") : window.DiscordNative.clipboard.read,
|
||||
wp: Vencord.Webpack,
|
||||
wpc: Vencord.Webpack.wreq.c,
|
||||
wreq: Vencord.Webpack.wreq,
|
||||
wpsearch: Vencord.Webpack.search,
|
||||
wpex: Vencord.Webpack.extract,
|
||||
wpc: Webpack.wreq.c,
|
||||
wreq: Webpack.wreq,
|
||||
wpsearch: search,
|
||||
wpex: extract,
|
||||
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
|
||||
findByProps: Vencord.Webpack.findByProps,
|
||||
find: Vencord.Webpack.find,
|
||||
Plugins: Vencord.Plugins,
|
||||
React: Vencord.Webpack.Common.React,
|
||||
find: newFindWrapper(f => f),
|
||||
findAll,
|
||||
findByProps: newFindWrapper(filters.byProps),
|
||||
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
|
||||
findByCode: newFindWrapper(filters.byCode),
|
||||
findAllByCode: (code: string) => findAll(filters.byCode(code)),
|
||||
PluginsApi: Vencord.Plugins,
|
||||
plugins: Vencord.Plugins.plugins,
|
||||
React,
|
||||
Settings: Vencord.Settings,
|
||||
Api: Vencord.Api,
|
||||
reload: () => location.reload(),
|
||||
|
134
src/plugins/crashHandler.ts
Normal file
134
src/plugins/crashHandler.ts
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { definePluginSettings } from "@api/settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import Logger from "@utils/Logger";
|
||||
import { closeAllModals } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { maybePromptToUpdate } from "@utils/updater";
|
||||
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
const CrashHandlerLogger = new Logger("CrashHandler");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
attemptToPreventCrashes: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to attempt to prevent Discord crashes.",
|
||||
default: true
|
||||
},
|
||||
attemptToNavigateToHome: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to attempt to navigate to the home when preventing Discord crashes.",
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "CrashHandler",
|
||||
description: "Utility plugin for handling and possibly recovering from Crashes without a restart",
|
||||
authors: [Devs.Nuckyz],
|
||||
enabledByDefault: true,
|
||||
|
||||
popAllModals: undefined as (() => void) | undefined,
|
||||
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".Messages.ERRORS_UNEXPECTED_CRASH",
|
||||
replacement: {
|
||||
match: /(?=this\.setState\()/,
|
||||
replace: "$self.handleCrash(this)||"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: 'dispatch({type:"MODAL_POP_ALL"})',
|
||||
replacement: {
|
||||
match: /(?<=(?<popAll>\i)=function\(\){\(0,\i\.\i\)\(\);\i\.\i\.dispatch\({type:"MODAL_POP_ALL"}\).+};)/,
|
||||
replace: "$self.popAllModals=$<popAll>;"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
handleCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
||||
try {
|
||||
maybePromptToUpdate("Uh oh, Discord has just crashed... but good news, there is a Vencord update available that might fix this issue! Would you like to update now?", true);
|
||||
|
||||
if (settings.store.attemptToPreventCrashes) {
|
||||
this.handlePreventCrash(_this);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.error("Failed to handle crash", err);
|
||||
}
|
||||
},
|
||||
|
||||
handlePreventCrash(_this: ReactElement & { forceUpdate: () => void; }) {
|
||||
try {
|
||||
showNotification({
|
||||
color: "#eed202",
|
||||
title: "Discord has crashed!",
|
||||
body: "Attempting to recover...",
|
||||
});
|
||||
} catch { }
|
||||
|
||||
try {
|
||||
FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to close open context menu.", err);
|
||||
}
|
||||
try {
|
||||
this.popAllModals?.();
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to close old modals.", err);
|
||||
}
|
||||
try {
|
||||
closeAllModals();
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to close all open modals.", err);
|
||||
}
|
||||
try {
|
||||
FluxDispatcher.dispatch({ type: "USER_PROFILE_MODAL_CLOSE" });
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to close user popout.", err);
|
||||
}
|
||||
try {
|
||||
FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to pop all layers.", err);
|
||||
}
|
||||
if (settings.store.attemptToNavigateToHome) {
|
||||
try {
|
||||
NavigationRouter.transitionTo("/channels/@me");
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to navigate to home", err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
_this.forceUpdate();
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to update crash handler component.", err);
|
||||
}
|
||||
}
|
||||
});
|
@ -19,6 +19,7 @@
|
||||
import { definePluginSettings } from "@api/settings";
|
||||
import { Link } from "@components/Link";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isTruthy } from "@utils/guards";
|
||||
import { useAwaiter } from "@utils/misc";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
|
||||
@ -56,11 +57,11 @@ interface ActivityAssets {
|
||||
}
|
||||
|
||||
interface Activity {
|
||||
state: string;
|
||||
state?: string;
|
||||
details?: string;
|
||||
timestamps?: {
|
||||
start?: Number;
|
||||
end?: Number;
|
||||
start?: number;
|
||||
end?: number;
|
||||
};
|
||||
assets?: ActivityAssets;
|
||||
buttons?: Array<string>;
|
||||
@ -70,7 +71,7 @@ interface Activity {
|
||||
button_urls?: Array<string>;
|
||||
};
|
||||
type: ActivityType;
|
||||
flags: Number;
|
||||
flags: number;
|
||||
}
|
||||
|
||||
enum ActivityType {
|
||||
@ -93,13 +94,13 @@ const numOpt = (description: string) => ({
|
||||
onChange: setRpc
|
||||
}) as const;
|
||||
|
||||
const choice = (label: string, value: any, _default?: Boolean) => ({
|
||||
const choice = (label: string, value: any, _default?: boolean) => ({
|
||||
label,
|
||||
value,
|
||||
default: _default
|
||||
}) as const;
|
||||
|
||||
const choiceOpt = (description: string, options) => ({
|
||||
const choiceOpt = <T,>(description: string, options: T) => ({
|
||||
type: OptionType.SELECT,
|
||||
description,
|
||||
onChange: setRpc,
|
||||
@ -173,13 +174,13 @@ async function createActivity(): Promise<Activity | undefined> {
|
||||
activity.buttons = [
|
||||
buttonOneText,
|
||||
buttonTwoText
|
||||
].filter(Boolean);
|
||||
].filter(isTruthy);
|
||||
|
||||
activity.metadata = {
|
||||
button_urls: [
|
||||
buttonOneURL,
|
||||
buttonTwoURL
|
||||
].filter(Boolean)
|
||||
].filter(isTruthy)
|
||||
};
|
||||
}
|
||||
|
||||
@ -206,12 +207,10 @@ async function createActivity(): Promise<Activity | undefined> {
|
||||
delete activity[k];
|
||||
}
|
||||
|
||||
// WHAT DO YOU WANT FROM ME
|
||||
// eslint-disable-next-line consistent-return
|
||||
return activity;
|
||||
}
|
||||
|
||||
async function setRpc(disable?: Boolean) {
|
||||
async function setRpc(disable?: boolean) {
|
||||
const activity: Activity | undefined = await createActivity();
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
|
@ -20,11 +20,12 @@ import { migratePluginSettings, Settings } from "@api/settings";
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { Devs } from "@utils/constants";
|
||||
import Logger from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { makeLazy } from "@utils/misc";
|
||||
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { Forms, GuildStore, Margins, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||
import { Forms, GuildStore, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||
|
||||
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
|
||||
|
||||
@ -96,7 +97,7 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
|
||||
|
||||
return (
|
||||
<>
|
||||
<Forms.FormTitle className={Margins.marginTop20}>Custom Name</Forms.FormTitle>
|
||||
<Forms.FormTitle className={Margins.top20}>Custom Name</Forms.FormTitle>
|
||||
<CheckedTextInput
|
||||
value={name}
|
||||
onChange={setName}
|
||||
|
@ -22,11 +22,25 @@ import { Devs } from "@utils/constants";
|
||||
import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { ChannelStore, UserStore } from "@webpack/common";
|
||||
import { ChannelStore, PermissionStore, UserStore } from "@webpack/common";
|
||||
|
||||
const DRAFT_TYPE = 0;
|
||||
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
|
||||
|
||||
const USE_EXTERNAL_EMOJIS = 1n << 18n;
|
||||
const USE_EXTERNAL_STICKERS = 1n << 37n;
|
||||
|
||||
enum EmojiIntentions {
|
||||
REACTION = 0,
|
||||
STATUS = 1,
|
||||
COMMUNITY_CONTENT = 2,
|
||||
CHAT = 3,
|
||||
GUILD_STICKER_RELATED_EMOJI = 4,
|
||||
GUILD_ROLE_BENEFIT_EMOJI = 5,
|
||||
COMMUNITY_CONTENT_ONLY = 6,
|
||||
SOUNDBOARD = 7
|
||||
}
|
||||
|
||||
interface BaseSticker {
|
||||
available: boolean;
|
||||
description: string;
|
||||
@ -58,26 +72,39 @@ migratePluginSettings("FakeNitro", "NitroBypass");
|
||||
|
||||
export default definePlugin({
|
||||
name: "FakeNitro",
|
||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity],
|
||||
description: "Allows you to stream in nitro quality and send fake emojis/stickers.",
|
||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain],
|
||||
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
||||
dependencies: ["MessageEventsAPI"],
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "canUseAnimatedEmojis:function",
|
||||
find: ".PREMIUM_LOCKED;",
|
||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
||||
replacement: [
|
||||
"canUseAnimatedEmojis",
|
||||
"canUseEmojisEverywhere"
|
||||
].map(func => {
|
||||
return {
|
||||
match: new RegExp(`${func}:function\\(.+?\\{`),
|
||||
replace: "$&return true;"
|
||||
};
|
||||
})
|
||||
{
|
||||
match: /(?<=(?<intention>\i)=\i\.intention)/,
|
||||
replace: ",fakeNitroIntention=$<intention>"
|
||||
},
|
||||
{
|
||||
match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g,
|
||||
replace: ",fakeNitroIntention"
|
||||
},
|
||||
{
|
||||
match: /(?<=&&!\i&&)!(?<canUseExternal>\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
|
||||
replace: `(!$<canUseExternal>&&![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "canUseAnimatedEmojis:function",
|
||||
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
|
||||
replacement: {
|
||||
match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\((?<user>\i))\){(?<premiumCheck>.+?\))/g,
|
||||
replace: `,fakeNitroIntention){$<premiumCheck>||fakeNitroIntention===undefined||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "canUseStickersEverywhere:function",
|
||||
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
|
||||
replacement: {
|
||||
match: /canUseStickersEverywhere:function\(.+?\{/,
|
||||
@ -93,7 +120,7 @@ export default definePlugin({
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "canUseAnimatedEmojis:function",
|
||||
find: "canStreamHighQuality:function",
|
||||
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
|
||||
replacement: [
|
||||
"canUseHighVideoUploadQuality",
|
||||
@ -114,6 +141,13 @@ export default definePlugin({
|
||||
replace: ""
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "canUseClientThemes:function",
|
||||
replacement: {
|
||||
match: /(?<=canUseClientThemes:function\(\i\){)/,
|
||||
replace: "return true;"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
options: {
|
||||
@ -161,6 +195,22 @@ export default definePlugin({
|
||||
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
|
||||
},
|
||||
|
||||
hasPermissionToUseExternalEmojis(channelId: string) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
|
||||
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
|
||||
|
||||
return PermissionStore.can(USE_EXTERNAL_EMOJIS, channel);
|
||||
},
|
||||
|
||||
hasPermissionToUseExternalStickers(channelId: string) {
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
|
||||
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
|
||||
|
||||
return PermissionStore.can(USE_EXTERNAL_STICKERS, channel);
|
||||
},
|
||||
|
||||
getStickerLink(stickerId: string) {
|
||||
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`;
|
||||
},
|
||||
@ -245,7 +295,7 @@ export default definePlugin({
|
||||
if (!sticker)
|
||||
break stickerBypass;
|
||||
|
||||
if (sticker.available !== false && (this.canUseStickers || (sticker as GuildSticker)?.guild_id === guildId))
|
||||
if (sticker.available !== false && ((this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId)) || (sticker as GuildSticker)?.guild_id === guildId))
|
||||
break stickerBypass;
|
||||
|
||||
let link = this.getStickerLink(sticker.id);
|
||||
@ -268,7 +318,7 @@ export default definePlugin({
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.canUseEmotes && settings.enableEmojiBypass) {
|
||||
if ((!this.canUseEmotes || !this.hasPermissionToUseExternalEmojis(channelId)) && settings.enableEmojiBypass) {
|
||||
for (const emoji of messageObj.validNonShortcutEmojis) {
|
||||
if (!emoji.require_colons) continue;
|
||||
if (emoji.guildId === guildId && !emoji.animated) continue;
|
||||
@ -284,8 +334,9 @@ export default definePlugin({
|
||||
return { cancel: false };
|
||||
});
|
||||
|
||||
if (!this.canUseEmotes && settings.enableEmojiBypass) {
|
||||
this.preEdit = addPreEditListener((_, __, messageObj) => {
|
||||
this.preEdit = addPreEditListener((channelId, __, messageObj) => {
|
||||
if (this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId)) return;
|
||||
|
||||
const { guildId } = this;
|
||||
|
||||
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
|
||||
@ -299,7 +350,6 @@ export default definePlugin({
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
|
@ -34,7 +34,7 @@ interface Activity {
|
||||
state: string;
|
||||
details?: string;
|
||||
timestamps?: {
|
||||
start?: Number;
|
||||
start?: number;
|
||||
};
|
||||
assets?: ActivityAssets;
|
||||
buttons?: Array<string>;
|
||||
@ -43,8 +43,8 @@ interface Activity {
|
||||
metadata?: {
|
||||
button_urls?: Array<string>;
|
||||
};
|
||||
type: Number;
|
||||
flags: Number;
|
||||
type: number;
|
||||
flags: number;
|
||||
}
|
||||
|
||||
interface TrackData {
|
||||
|
@ -56,7 +56,7 @@ function MemberCount() {
|
||||
<div {...props}>
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: "var(--status-green-600)",
|
||||
backgroundColor: "var(--green-360)",
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
@ -64,7 +64,7 @@ function MemberCount() {
|
||||
marginRight: "0.5em"
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: "var(--status-green-600)" }}>{online}</span>
|
||||
<span style={{ color: "var(--green-360)" }}>{online}</span>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
@ -76,13 +76,13 @@ function MemberCount() {
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
borderRadius: "50%",
|
||||
border: "3px solid var(--status-grey-500)",
|
||||
border: "3px solid var(--primary-400)",
|
||||
display: "inline-block",
|
||||
marginRight: "0.5em",
|
||||
marginLeft: "1em"
|
||||
}}
|
||||
/>
|
||||
<span style={{ color: "var(--status-grey-500)" }}>{total}</span>
|
||||
<span style={{ color: "var(--primary-400)" }}>{total}</span>
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
@ -17,11 +17,13 @@
|
||||
*/
|
||||
|
||||
import { addAccessory } from "@api/MessageAccessories";
|
||||
import { Settings } from "@api/settings";
|
||||
import { definePluginSettings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants.js";
|
||||
import { classes, LazyComponent } from "@utils/misc";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { filters, findByPropsLazy, waitFor } from "@webpack";
|
||||
import { find, findByCode, findByPropsLazy } from "@webpack";
|
||||
import {
|
||||
Button,
|
||||
ChannelStore,
|
||||
@ -36,41 +38,20 @@ import {
|
||||
} from "@webpack/common";
|
||||
import { Channel, Guild, Message } from "discord-types/general";
|
||||
|
||||
let messageCache: { [id: string]: { message?: Message, fetched: boolean; }; } = {};
|
||||
const messageCache = new Map<string, {
|
||||
message?: Message;
|
||||
fetched: boolean;
|
||||
}>();
|
||||
|
||||
let AutomodEmbed: React.ComponentType<any>,
|
||||
Embed: React.ComponentType<any>,
|
||||
ChannelMessage: React.ComponentType<any>,
|
||||
Endpoints: Record<string, any>;
|
||||
const Embed = LazyComponent(() => findByCode(".inlineMediaEmbed"));
|
||||
const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes('["message","compact","className",')));
|
||||
|
||||
waitFor(["mle_AutomodEmbed"], m => (AutomodEmbed = m.mle_AutomodEmbed));
|
||||
waitFor(filters.byCode(".inlineMediaEmbed"), m => Embed = m);
|
||||
waitFor(m => m.type?.toString()?.includes('["message","compact","className",'), m => ChannelMessage = m);
|
||||
waitFor(["MESSAGE_CREATE_ATTACHMENT_UPLOAD"], _ => Endpoints = _);
|
||||
const SearchResultClasses = findByPropsLazy("message", "searchResult");
|
||||
|
||||
const messageFetchQueue = new Queue();
|
||||
async function fetchMessage(channelID: string, messageID: string): Promise<Message | void> {
|
||||
if (messageID in messageCache && !messageCache[messageID].fetched) return Promise.resolve();
|
||||
if (messageCache[messageID]?.fetched) return Promise.resolve(messageCache[messageID].message);
|
||||
let AutoModEmbed: React.ComponentType<any> = () => null;
|
||||
|
||||
messageCache[messageID] = { fetched: false };
|
||||
const res = await RestAPI.get({
|
||||
url: Endpoints.MESSAGES(channelID),
|
||||
query: {
|
||||
limit: 1,
|
||||
around: messageID
|
||||
},
|
||||
retries: 2
|
||||
}).catch(() => { });
|
||||
const apiMessage = res.body?.[0];
|
||||
const message: Message = MessageStore.getMessages(apiMessage.channel_id).receiveMessage(apiMessage).get(apiMessage.id);
|
||||
messageCache[message.id] = {
|
||||
message: message,
|
||||
fetched: true
|
||||
};
|
||||
return Promise.resolve(message);
|
||||
}
|
||||
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
|
||||
const tenorRegex = /https:\/\/(?:www.)?tenor\.com/;
|
||||
|
||||
interface Attachment {
|
||||
height: number;
|
||||
@ -79,66 +60,133 @@ interface Attachment {
|
||||
proxyURL?: string;
|
||||
}
|
||||
|
||||
const isTenorGif = /https:\/\/(?:www.)?tenor\.com/;
|
||||
function getImages(message: Message): Attachment[] {
|
||||
const attachments: Attachment[] = [];
|
||||
message.attachments?.forEach(a => {
|
||||
if (a.content_type!.startsWith("image/")) attachments.push({
|
||||
height: a.height!,
|
||||
width: a.width!,
|
||||
url: a.url,
|
||||
proxyURL: a.proxy_url!
|
||||
});
|
||||
});
|
||||
message.embeds?.forEach(e => {
|
||||
if (e.type === "image") attachments.push(
|
||||
e.image ? { ...e.image } : { ...e.thumbnail! }
|
||||
);
|
||||
if (e.type === "gifv" && !isTenorGif.test(e.url!)) {
|
||||
attachments.push({
|
||||
height: e.thumbnail!.height,
|
||||
width: e.thumbnail!.width,
|
||||
url: e.url!
|
||||
});
|
||||
}
|
||||
});
|
||||
return attachments;
|
||||
}
|
||||
|
||||
const noContent = (attachments: number, embeds: number): string => {
|
||||
if (!attachments && !embeds) return "";
|
||||
if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
||||
if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`;
|
||||
return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
||||
};
|
||||
|
||||
function requiresRichEmbed(message: Message) {
|
||||
if (message.attachments.every(a => a.content_type?.startsWith("image/"))
|
||||
&& message.embeds.every(e => e.type === "image" || (e.type === "gifv" && !isTenorGif.test(e.url!)))
|
||||
&& !message.components.length
|
||||
) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const computeWidthAndHeight = (width: number, height: number) => {
|
||||
const maxWidth = 400, maxHeight = 300;
|
||||
let newWidth: number, newHeight: number;
|
||||
if (width > height) {
|
||||
newWidth = Math.min(width, maxWidth);
|
||||
newHeight = Math.round(height / (width / newWidth));
|
||||
} else {
|
||||
newHeight = Math.min(height, maxHeight);
|
||||
newWidth = Math.round(width / (height / newHeight));
|
||||
}
|
||||
return { width: newWidth, height: newHeight };
|
||||
};
|
||||
|
||||
interface MessageEmbedProps {
|
||||
message: Message;
|
||||
channel: Channel;
|
||||
guildID: string;
|
||||
}
|
||||
|
||||
const messageFetchQueue = new Queue();
|
||||
|
||||
const settings = definePluginSettings({
|
||||
messageBackgroundColor: {
|
||||
description: "Background color for messages in rich embeds",
|
||||
type: OptionType.BOOLEAN
|
||||
},
|
||||
automodEmbeds: {
|
||||
description: "Use automod embeds instead of rich embeds (smaller but less info)",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{
|
||||
label: "Always use automod embeds",
|
||||
value: "always"
|
||||
},
|
||||
{
|
||||
label: "Prefer automod embeds, but use rich embeds if some content can't be shown",
|
||||
value: "prefer"
|
||||
},
|
||||
{
|
||||
label: "Never use automod embeds",
|
||||
value: "never",
|
||||
default: true
|
||||
}
|
||||
]
|
||||
},
|
||||
clearMessageCache: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Clear the linked message cache",
|
||||
component: () =>
|
||||
<Button onClick={() => messageCache.clear()}>
|
||||
Clear the linked message cache
|
||||
</Button>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
async function fetchMessage(channelID: string, messageID: string) {
|
||||
const cached = messageCache.get(messageID);
|
||||
if (cached) return cached.message;
|
||||
|
||||
messageCache.set(messageID, { fetched: false });
|
||||
|
||||
const res = await RestAPI.get({
|
||||
url: `/channels/${channelID}/messages`,
|
||||
query: {
|
||||
limit: 1,
|
||||
around: messageID
|
||||
},
|
||||
retries: 2
|
||||
}).catch(() => null);
|
||||
|
||||
const msg = res?.body?.[0];
|
||||
if (!msg) return;
|
||||
|
||||
const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id);
|
||||
|
||||
messageCache.set(message.id, {
|
||||
message,
|
||||
fetched: true
|
||||
});
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
function getImages(message: Message): Attachment[] {
|
||||
const attachments: Attachment[] = [];
|
||||
|
||||
for (const { content_type, height, width, url, proxy_url } of message.attachments ?? []) {
|
||||
if (content_type?.startsWith("image/"))
|
||||
attachments.push({
|
||||
height: height!,
|
||||
width: width!,
|
||||
url: url,
|
||||
proxyURL: proxy_url!
|
||||
});
|
||||
}
|
||||
|
||||
for (const { type, image, thumbnail, url } of message.embeds ?? []) {
|
||||
if (type === "image")
|
||||
attachments.push({ ...(image ?? thumbnail!) });
|
||||
else if (url && type === "gifv" && !tenorRegex.test(url))
|
||||
attachments.push({
|
||||
height: thumbnail!.height,
|
||||
width: thumbnail!.width,
|
||||
url
|
||||
});
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
function noContent(attachments: number, embeds: number) {
|
||||
if (!attachments && !embeds) return "";
|
||||
if (!attachments) return `[no content, ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
||||
if (!embeds) return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""}]`;
|
||||
return `[no content, ${attachments} attachment${attachments !== 1 ? "s" : ""} and ${embeds} embed${embeds !== 1 ? "s" : ""}]`;
|
||||
}
|
||||
|
||||
function requiresRichEmbed(message: Message) {
|
||||
if (message.components.length) return true;
|
||||
if (message.attachments.some(a => !a.content_type?.startsWith("image/"))) return true;
|
||||
if (message.embeds.some(e => e.type !== "image" && (e.type !== "gifv" || tenorRegex.test(e.url!)))) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function computeWidthAndHeight(width: number, height: number) {
|
||||
const maxWidth = 400;
|
||||
const maxHeight = 300;
|
||||
|
||||
if (width > height) {
|
||||
const adjustedWidth = Math.min(width, maxWidth);
|
||||
return { width: adjustedWidth, height: Math.round(height / (width / adjustedWidth)) };
|
||||
}
|
||||
|
||||
const adjustedHeight = Math.min(height, maxHeight);
|
||||
return { width: Math.round(width / (height / adjustedHeight)), height: adjustedHeight };
|
||||
}
|
||||
|
||||
function withEmbeddedBy(message: Message, embeddedBy: string[]) {
|
||||
return new Proxy(message, {
|
||||
get(_, prop) {
|
||||
@ -149,68 +197,15 @@ function withEmbeddedBy(message: Message, embeddedBy: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageLinkEmbeds",
|
||||
description: "Adds a preview to messages that link another message",
|
||||
authors: [Devs.TheSun],
|
||||
dependencies: ["MessageAccessoriesAPI"],
|
||||
patches: [
|
||||
{
|
||||
find: ".embedCard",
|
||||
replacement: [{
|
||||
match: /{"use strict";(.{0,10})\(\)=>(.{1,2})}\);/,
|
||||
replace: '{"use strict";$1()=>$2,me:()=>messageEmbed});'
|
||||
}, {
|
||||
match: /function (.{1,2})\(.{1,2}\){var .{1,2}=.{1,2}\.message,.{1,2}=.{1,2}\.channel.{0,300}\.embedCard.{0,500}}\)}/,
|
||||
replace: "$&;var messageEmbed={mle_AutomodEmbed:$1};"
|
||||
}]
|
||||
}
|
||||
],
|
||||
options: {
|
||||
messageBackgroundColor: {
|
||||
description: "Background color for messages in rich embeds",
|
||||
type: OptionType.BOOLEAN
|
||||
},
|
||||
automodEmbeds: {
|
||||
description: "Use automod embeds instead of rich embeds (smaller but less info)",
|
||||
type: OptionType.SELECT,
|
||||
options: [{
|
||||
label: "Always use automod embeds",
|
||||
value: "always"
|
||||
}, {
|
||||
label: "Prefer automod embeds, but use rich embeds if some content can't be shown",
|
||||
value: "prefer"
|
||||
}, {
|
||||
label: "Never use automod embeds",
|
||||
value: "never",
|
||||
default: true
|
||||
}]
|
||||
},
|
||||
clearMessageCache: {
|
||||
type: OptionType.COMPONENT,
|
||||
description: "Clear the linked message cache",
|
||||
component: () =>
|
||||
<Button onClick={() => messageCache = {}}>
|
||||
Clear the linked message cache
|
||||
</Button>
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
addAccessory("messageLinkEmbed", props => this.messageEmbedAccessory(props), 4 /* just above rich embeds*/);
|
||||
},
|
||||
|
||||
messageLinkRegex: /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,19}|@me)\/(\d{17,19})\/(\d{17,19})/g,
|
||||
|
||||
messageEmbedAccessory(props) {
|
||||
const { message }: { message: Message; } = props;
|
||||
function MessageEmbedAccessory({ message }: { message: Message; }) {
|
||||
// @ts-ignore
|
||||
const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];
|
||||
|
||||
const accessories = [] as (JSX.Element | null)[];
|
||||
|
||||
let match = null as RegExpMatchArray | null;
|
||||
while ((match = this.messageLinkRegex.exec(message.content!)) !== null) {
|
||||
while ((match = messageLinkRegex.exec(message.content!)) !== null) {
|
||||
const [_, guildID, channelID, messageID] = match;
|
||||
if (embeddedBy.includes(messageID)) {
|
||||
continue;
|
||||
@ -221,11 +216,12 @@ export default definePlugin({
|
||||
continue;
|
||||
}
|
||||
|
||||
let linkedMessage = messageCache[messageID]?.message;
|
||||
let linkedMessage = messageCache.get(messageID)?.message;
|
||||
if (!linkedMessage) {
|
||||
linkedMessage ??= MessageStore.getMessage(channelID, messageID);
|
||||
if (linkedMessage) messageCache[messageID] = { message: linkedMessage, fetched: true };
|
||||
else {
|
||||
if (linkedMessage) {
|
||||
messageCache.set(messageID, { message: linkedMessage, fetched: true });
|
||||
} else {
|
||||
const msg = { ...message } as any;
|
||||
delete msg.embeds;
|
||||
messageFetchQueue.push(() => fetchMessage(channelID, messageID)
|
||||
@ -237,30 +233,30 @@ export default definePlugin({
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const messageProps: MessageEmbedProps = {
|
||||
message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
|
||||
channel: linkedChannel,
|
||||
guildID
|
||||
};
|
||||
|
||||
const type = Settings.plugins[this.name].automodEmbeds;
|
||||
const type = settings.store.automodEmbeds;
|
||||
accessories.push(
|
||||
type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage))
|
||||
? this.automodEmbedAccessory(messageProps)
|
||||
: this.channelMessageEmbedAccessory(messageProps)
|
||||
? <AutomodEmbedAccessory {...messageProps} />
|
||||
: <ChannelMessageEmbedAccessory {...messageProps} />
|
||||
);
|
||||
}
|
||||
return accessories;
|
||||
},
|
||||
|
||||
channelMessageEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
||||
const { message, channel, guildID } = props;
|
||||
return accessories.length ? <>{accessories}</> : null;
|
||||
}
|
||||
|
||||
function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbedProps): JSX.Element | null {
|
||||
const isDM = guildID === "@me";
|
||||
|
||||
const guild = !isDM && GuildStore.getGuild(channel.guild_id);
|
||||
const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]);
|
||||
const classNames = [SearchResultClasses.message];
|
||||
if (Settings.plugins[this.name].messageBackgroundColor) classNames.push(SearchResultClasses.searchResult);
|
||||
|
||||
|
||||
return <Embed
|
||||
embed={{
|
||||
@ -268,62 +264,105 @@ export default definePlugin({
|
||||
color: "var(--background-secondary)",
|
||||
author: {
|
||||
name: <Text variant="text-xs/medium" tag="span">
|
||||
{[
|
||||
<span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>,
|
||||
...(isDM
|
||||
{isDM
|
||||
? Parser.parse(`<@${dmReceiver.id}>`)
|
||||
: Parser.parse(`<#${channel.id}>`)
|
||||
)
|
||||
]}
|
||||
}
|
||||
</Text>,
|
||||
iconProxyURL: guild
|
||||
? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png`
|
||||
: `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}`
|
||||
}
|
||||
}}
|
||||
renderDescription={() => {
|
||||
return <div key={message.id} className={classNames.join(" ")}>
|
||||
renderDescription={() => (
|
||||
<div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>
|
||||
<ChannelMessage
|
||||
id={`message-link-embeds-${message.id}`}
|
||||
message={message}
|
||||
channel={channel}
|
||||
subscribeToComponentDispatch={false}
|
||||
/>
|
||||
</div >;
|
||||
}}
|
||||
</div>
|
||||
)}
|
||||
/>;
|
||||
},
|
||||
}
|
||||
|
||||
automodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
||||
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
||||
const { message, channel, guildID } = props;
|
||||
|
||||
const isDM = guildID === "@me";
|
||||
const images = getImages(message);
|
||||
const { parse } = Parser;
|
||||
|
||||
return <AutomodEmbed
|
||||
return <AutoModEmbed
|
||||
channel={channel}
|
||||
childrenAccessories={<Text color="text-muted" variant="text-xs/medium" tag="span">
|
||||
{[
|
||||
...(isDM ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) : parse(`<#${channel.id}>`)),
|
||||
childrenAccessories={
|
||||
<Text color="text-muted" variant="text-xs/medium" tag="span">
|
||||
{isDM
|
||||
? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)
|
||||
: parse(`<#${channel.id}>`)
|
||||
},
|
||||
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
|
||||
]}
|
||||
</Text>}
|
||||
</Text>
|
||||
}
|
||||
compact={false}
|
||||
content={[
|
||||
...(message.content || !(message.attachments.length > images.length)
|
||||
content={
|
||||
<>
|
||||
{message.content || message.attachments.length <= images.length
|
||||
? parse(message.content)
|
||||
: [noContent(message.attachments.length, message.embeds.length)]
|
||||
),
|
||||
...(images.map<JSX.Element>(a => {
|
||||
const { width, height } = computeWidthAndHeight(a.width, a.height);
|
||||
return <div><img src={a.url} width={width} height={height} /></div>;
|
||||
}
|
||||
))
|
||||
]}
|
||||
{images.map(a => {
|
||||
const { width, height } = computeWidthAndHeight(a.width, a.height);
|
||||
return (
|
||||
<div>
|
||||
<img src={a.url} width={width} height={height} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
}
|
||||
hideTimestamp={false}
|
||||
message={message}
|
||||
_messageEmbed="automod"
|
||||
/>;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageLinkEmbeds",
|
||||
description: "Adds a preview to messages that link another message",
|
||||
authors: [Devs.TheSun, Devs.Ven],
|
||||
dependencies: ["MessageAccessoriesAPI"],
|
||||
patches: [
|
||||
{
|
||||
find: ".embedCard",
|
||||
replacement: [{
|
||||
match: /function (\i)\(\i\){var \i=\i\.message,\i=\i\.channel.{0,200}\.hideTimestamp/,
|
||||
replace: "$self.AutoModEmbed=$1;$&"
|
||||
}]
|
||||
}
|
||||
],
|
||||
|
||||
set AutoModEmbed(e: any) {
|
||||
AutoModEmbed = e;
|
||||
},
|
||||
|
||||
settings,
|
||||
|
||||
start() {
|
||||
addAccessory("messageLinkEmbed", props => {
|
||||
if (!messageLinkRegex.test(props.message.content))
|
||||
return null;
|
||||
|
||||
// need to reset the regex because it's global
|
||||
messageLinkRegex.lastIndex = 0;
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<MessageEmbedAccessory message={props.message} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}, 4 /* just above rich embeds */);
|
||||
},
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ migratePluginSettings("NoDevtoolsWarning", "STFU");
|
||||
|
||||
export default definePlugin({
|
||||
name: "NoDevtoolsWarning",
|
||||
description: "Disables the 'HOLD UP' banner in the console",
|
||||
description: "Disables the 'HOLD UP' banner in the console. As a side effect, also prevents Discord from hiding your token, which prevents random logouts.",
|
||||
authors: [Devs.Ven],
|
||||
patches: [{
|
||||
find: "setDevtoolsCallbacks",
|
||||
|
@ -16,9 +16,11 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { migratePluginSettings } from "@api/settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
migratePluginSettings("NoF1", "No F1");
|
||||
export default definePlugin({
|
||||
name: "NoF1",
|
||||
description: "Disables F1 help bind.",
|
||||
|
@ -16,9 +16,11 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { migratePluginSettings } from "@api/settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
migratePluginSettings("NoRPC", "No RPC");
|
||||
export default definePlugin({
|
||||
name: "NoRPC",
|
||||
description: "Disables Discord's RPC server.",
|
||||
|
@ -55,7 +55,7 @@ const Icons = {
|
||||
};
|
||||
type Platform = keyof typeof Icons;
|
||||
|
||||
const getStatusColor = findByCodeLazy("STATUS_YELLOW", "TWITCH", "STATUS_GREY");
|
||||
const getStatusColor = findByCodeLazy(".TWITCH", ".STREAMING", ".INVISIBLE");
|
||||
|
||||
const PlatformIcon = ({ platform, status }: { platform: Platform, status: string; }) => {
|
||||
const tooltip = platform[0].toUpperCase() + platform.slice(1);
|
||||
|
@ -30,10 +30,10 @@ export default definePlugin({
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".revealSpoiler=function",
|
||||
find: ".removeObscurity=function",
|
||||
replacement: {
|
||||
match: /\.revealSpoiler=function\((.{1,2})\){/,
|
||||
replace: ".revealSpoiler=function($1){$self.reveal($1);"
|
||||
match: /\.removeObscurity=function\((\i)\){/,
|
||||
replace: ".removeObscurity=function($1){$self.reveal($1);"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -32,6 +32,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
|
||||
fallbackValue: [],
|
||||
deps: [refetchCount],
|
||||
});
|
||||
const username = UserStore.getUser(userId)?.username ?? "";
|
||||
|
||||
const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
|
||||
|
||||
@ -79,7 +80,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
|
||||
<textarea
|
||||
className={classes(Classes.textarea.replace("textarea", ""), "enter-comment")}
|
||||
// this produces something like '-_59yqs ...' but since no class exists with that name its fine
|
||||
placeholder={"Review @" + UserStore.getUser(userId)?.username ?? ""}
|
||||
placeholder={reviews?.some(r => r.senderdiscordid === UserStore.getCurrentUser().id) ? `Update review for @${username}` : `Review @${username}`}
|
||||
onKeyDown={onKeyPress}
|
||||
style={{
|
||||
marginTop: "6px",
|
||||
|
123
src/plugins/roleColorEverywhere.tsx
Normal file
123
src/plugins/roleColorEverywhere.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
chatMentions: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Show role colors in chat mentions (including in the message box)",
|
||||
restartNeeded: true
|
||||
},
|
||||
memberList: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Show role colors in member list role headers",
|
||||
restartNeeded: true
|
||||
},
|
||||
voiceUsers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Show role colors in the voice chat user list",
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "RoleColorEverywhere",
|
||||
authors: [Devs.KingFish, Devs.lewisakura],
|
||||
description: "Adds the top role color anywhere possible",
|
||||
patches: [
|
||||
// Chat Mentions
|
||||
{
|
||||
find: 'className:"mention"',
|
||||
replacement: [
|
||||
{
|
||||
match: /user:(\i),channelId:(\i).{0,300}?"@"\.concat\(.+?\)/,
|
||||
replace: "$&,color:$self.getUserColor($1.id,{channelId:$2})"
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.chatMentions,
|
||||
},
|
||||
// Slate
|
||||
{
|
||||
// taken from CommandsAPI
|
||||
find: ".source,children",
|
||||
replacement: [
|
||||
{
|
||||
match: /function \i\((\i)\).{5,20}id.{5,20}guildId.{5,10}channelId.{100,150}hidePersonalInformation.{5,50}jsx.{5,20},{/,
|
||||
replace: "$&color:$self.getUserColor($1.id,{guildId:$1.guildId}),"
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.chatMentions,
|
||||
},
|
||||
// Member List Role Names
|
||||
{
|
||||
find: ".memberGroupsPlaceholder",
|
||||
replacement: [
|
||||
{
|
||||
match: /(memo\(\(function\((\i)\).{300,500}CHANNEL_MEMBERS_A11Y_LABEL.{100,200}roleIcon.{5,20}null,).," \u2014 ",.\]/,
|
||||
replace: "$1$self.roleGroupColor($2)]"
|
||||
},
|
||||
],
|
||||
predicate: () => settings.store.memberList,
|
||||
},
|
||||
// Voice chat users
|
||||
{
|
||||
find: "renderPrioritySpeaker",
|
||||
replacement: [
|
||||
{
|
||||
match: /renderName=function\(\).{50,75}speaking.{50,100}jsx.{5,10}{/,
|
||||
replace: "$&...$self.getVoiceProps(this.props),"
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.voiceUsers,
|
||||
}
|
||||
],
|
||||
settings,
|
||||
|
||||
getColor(userId: string, { channelId, guildId }: { channelId?: string; guildId?: string; }) {
|
||||
if (!(guildId ??= ChannelStore.getChannel(channelId!)?.guild_id)) return null;
|
||||
return GuildMemberStore.getMember(guildId, userId)?.colorString ?? null;
|
||||
},
|
||||
getUserColor(userId: string, ids: { channelId?: string; guildId?: string; }) {
|
||||
const colorString = this.getColor(userId, ids);
|
||||
return colorString && parseInt(colorString.slice(1), 16);
|
||||
},
|
||||
roleGroupColor({ id, count, title, guildId }: { id: string; count: number; title: string; guildId: string; }) {
|
||||
const guild = GuildStore.getGuild(guildId);
|
||||
const role = guild?.roles[id];
|
||||
|
||||
return <span style={{
|
||||
color: role?.colorString,
|
||||
fontWeight: "unset",
|
||||
letterSpacing: ".05em"
|
||||
}}>{title} — {count}</span>;
|
||||
},
|
||||
getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) {
|
||||
return {
|
||||
style: {
|
||||
color: this.getColor(userId, { guildId })
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
@ -263,7 +263,7 @@ export default definePlugin({
|
||||
replacement: [
|
||||
{
|
||||
// Remove the divider and the open chat button for the HiddenChannelLockScreen
|
||||
match: /(?<=function \i\((?<props>\i)\).{1,1800}"more-options-popout"\)\);if\()/,
|
||||
match: /(?<=function \i\((?<props>\i)\).{1,2000}"more-options-popout"\)\);if\()/,
|
||||
replace: "(!$self.isHiddenChannel($<props>.channel)||$<props>.inCall)&&"
|
||||
},
|
||||
{
|
||||
|
@ -56,7 +56,7 @@ function SilentTypingToggle() {
|
||||
<div className={ButtonWrapperClasses.buttonWrapper}>
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
||||
<path fill="currentColor" d="M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z" />
|
||||
{isEnabled && <path d="M13 432L590 48" stroke="var(--status-red-500)" stroke-width="72" stroke-linecap="round" />}
|
||||
{isEnabled && <path d="M13 432L590 48" stroke="var(--red-500)" stroke-width="72" stroke-linecap="round" />}
|
||||
</svg>
|
||||
</div>
|
||||
</Button>
|
||||
|
91
src/plugins/supportHelper.tsx
Normal file
91
src/plugins/supportHelper.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { DataStore } from "@api/index";
|
||||
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
||||
import { makeCodeblock } from "@utils/misc";
|
||||
import definePlugin from "@utils/types";
|
||||
import { isOutdated } from "@utils/updater";
|
||||
import { Alerts, FluxDispatcher, Forms, UserStore } from "@webpack/common";
|
||||
|
||||
import gitHash from "~git-hash";
|
||||
import plugins from "~plugins";
|
||||
|
||||
import settings from "./settings";
|
||||
|
||||
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
||||
|
||||
export default definePlugin({
|
||||
name: "SupportHelper",
|
||||
required: true,
|
||||
description: "Helps me provide support to you",
|
||||
authors: [Devs.Ven],
|
||||
|
||||
commands: [{
|
||||
name: "vencord-debug",
|
||||
description: "Send Vencord Debug info",
|
||||
predicate: ctx => ctx.channel.id === SUPPORT_CHANNEL_ID,
|
||||
execute() {
|
||||
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
|
||||
|
||||
const debugInfo = `
|
||||
**Vencord Debug Info**
|
||||
|
||||
> Discord Branch: ${RELEASE_CHANNEL}
|
||||
> Client: ${typeof DiscordNative === "undefined" ? window.armcord ? "Armcord" : `Web (${navigator.userAgent})` : `Desktop (Electron v${settings.electronVersion})`}
|
||||
> Platform: ${window.navigator.platform}
|
||||
> Vencord Version: ${gitHash}${settings.additionalInfo}
|
||||
> Outdated: ${isOutdated}
|
||||
> Enabled Plugins:
|
||||
${makeCodeblock(Object.keys(plugins).filter(Vencord.Plugins.isPluginEnabled).join(", "))}
|
||||
`;
|
||||
|
||||
return {
|
||||
content: debugInfo.trim()
|
||||
};
|
||||
}
|
||||
}],
|
||||
|
||||
rememberDismiss() {
|
||||
DataStore.set(REMEMBER_DISMISS_KEY, gitHash);
|
||||
},
|
||||
|
||||
start() {
|
||||
FluxDispatcher.subscribe("CHANNEL_SELECT", async ({ channelId }) => {
|
||||
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
||||
|
||||
const myId = BigInt(UserStore.getCurrentUser().id);
|
||||
if (Object.values(Devs).some(d => d.id === myId)) return;
|
||||
|
||||
if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) {
|
||||
Alerts.show({
|
||||
title: "Hold on!",
|
||||
body: <div>
|
||||
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
Please first update using the Updater Page in Settings, or use the VencordInstaller (Update Vencord Button)
|
||||
to do so, in case you can't access the Updater page.
|
||||
</Forms.FormText>
|
||||
</div>,
|
||||
onCancel: this.rememberDismiss,
|
||||
onConfirm: this.rememberDismiss
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
135
src/plugins/typingIndicator.tsx
Normal file
135
src/plugins/typingIndicator.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings, Settings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { LazyComponent } from "@utils/misc";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { find, findLazy, findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, GuildMemberStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { buildSeveralUsers } from "./typingTweaks";
|
||||
|
||||
const ThreeDots = LazyComponent(() => find(m => m.type?.render?.toString()?.includes("().dots")));
|
||||
|
||||
const TypingStore = findStoreLazy("TypingStore");
|
||||
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
|
||||
|
||||
const Formatters = findLazy(m => m.Messages?.SEVERAL_USERS_TYPING);
|
||||
|
||||
function getDisplayName(guildId: string, userId: string) {
|
||||
return GuildMemberStore.getNick(guildId, userId) ?? UserStore.getUser(userId).username;
|
||||
}
|
||||
|
||||
function TypingIndicator({ channelId }: { channelId: string; }) {
|
||||
const typingUsers: Record<string, number> = useStateFromStores(
|
||||
[TypingStore],
|
||||
() => ({ ...TypingStore.getTypingUsers(channelId) as Record<string, number> }),
|
||||
null,
|
||||
(old, current) => {
|
||||
const oldKeys = Object.keys(old);
|
||||
const currentKeys = Object.keys(current);
|
||||
|
||||
return oldKeys.length === currentKeys.length && JSON.stringify(oldKeys) === JSON.stringify(currentKeys);
|
||||
}
|
||||
);
|
||||
|
||||
const guildId = ChannelStore.getChannel(channelId).guild_id;
|
||||
|
||||
if (!settings.store.includeMutedChannels) {
|
||||
const isChannelMuted = UserGuildSettingsStore.isChannelMuted(guildId, channelId);
|
||||
if (isChannelMuted) return null;
|
||||
}
|
||||
|
||||
delete typingUsers[UserStore.getCurrentUser().id];
|
||||
|
||||
const typingUsersArray = Object.keys(typingUsers);
|
||||
let tooltipText: string;
|
||||
|
||||
switch (typingUsersArray.length) {
|
||||
case 0: break;
|
||||
case 1: {
|
||||
tooltipText = Formatters.Messages.ONE_USER_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]) });
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
tooltipText = Formatters.Messages.TWO_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) });
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
tooltipText = Formatters.Messages.THREE_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
tooltipText = Settings.plugins.TypingTweaks.enabled
|
||||
? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: typingUsersArray.length - 2 })
|
||||
: Formatters.Messages.SEVERAL_USERS_TYPING;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (typingUsersArray.length > 0) {
|
||||
return (
|
||||
<Tooltip text={tooltipText!}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<div
|
||||
style={{ marginLeft: 6, zIndex: 0, cursor: "pointer" }}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<ThreeDots dotRadius={3} themed={true} />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
includeMutedChannels: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to show the typing indicator for muted channels.",
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "TypingIndicator",
|
||||
description: "Adds an indicator if someone is typing on a channel.",
|
||||
authors: [Devs.Nuckyz],
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".UNREAD_HIGHLIGHT",
|
||||
replacement: {
|
||||
match: /(?<=(?<channel>\i)=\i\.channel,.+?\(\)\.children.+?:null)/,
|
||||
replace: ",$self.TypingIndicator($<channel>.id)"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
TypingIndicator: (channelId: string) => (
|
||||
<ErrorBoundary noop>
|
||||
<TypingIndicator channelId={channelId} />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
});
|
@ -24,7 +24,7 @@ import { findByCodeLazy } from "@webpack";
|
||||
import { GuildMemberStore, React, RelationshipStore } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
const Avatar = findByCodeLazy(".Positions.TOP,spacing:");
|
||||
const Avatar = findByCodeLazy('"top",spacing:');
|
||||
|
||||
const settings = definePluginSettings({
|
||||
showAvatars: {
|
||||
@ -44,6 +44,15 @@ const settings = definePluginSettings({
|
||||
}
|
||||
});
|
||||
|
||||
export function buildSeveralUsers({ a, b, c }: { a: string, b: string, c: number; }) {
|
||||
return [
|
||||
<strong key="0">{a}</strong>,
|
||||
", ",
|
||||
<strong key="2">{b}</strong>,
|
||||
`, and ${c} others are typing...`
|
||||
];
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "TypingTweaks",
|
||||
description: "Show avatars and role colours in the typing indicator",
|
||||
@ -77,14 +86,7 @@ export default definePlugin({
|
||||
],
|
||||
settings,
|
||||
|
||||
buildSeveralUsers({ a, b, c }: { a: string, b: string, c: number; }) {
|
||||
return [
|
||||
<strong key="0">{a}</strong>,
|
||||
", ",
|
||||
<strong key="2">{b}</strong>,
|
||||
`, and ${c} others are typing...`
|
||||
];
|
||||
},
|
||||
buildSeveralUsers,
|
||||
|
||||
mutateChildren(props: any, users: User[], children: any) {
|
||||
if (!Array.isArray(children)) return children;
|
||||
@ -103,7 +105,7 @@ export default definePlugin({
|
||||
}}>
|
||||
{settings.store.showAvatars && <div style={{ marginTop: "4px" }}>
|
||||
<Avatar
|
||||
size={Avatar.Sizes.SIZE_16}
|
||||
size="SIZE_16"
|
||||
src={user.getAvatarURL(guildId, 128)} />
|
||||
</div>}
|
||||
{GuildMemberStore.getNick(guildId!, user.id) || !guildId && RelationshipStore.getNickname(user.id) || user.username}
|
||||
|
@ -48,10 +48,10 @@ export default definePlugin({
|
||||
},
|
||||
{
|
||||
// channel mentions
|
||||
find: 'className:"channelMention",iconType:(',
|
||||
find: ".shouldCloseDefaultModals",
|
||||
replacement: {
|
||||
match: /onClick:(.{1,3}),/,
|
||||
replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)(),",
|
||||
match: /onClick:(\i)(?=,.{0,30}className:"channelMention")/,
|
||||
replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)()",
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -20,10 +20,11 @@ import { Settings } from "@api/settings";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Devs } from "@utils/constants";
|
||||
import Logger from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { wordsToTitle } from "@utils/text";
|
||||
import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Button, ChannelStore, FluxDispatcher, Forms, Margins, SelectedChannelStore, useMemo, UserStore } from "@webpack/common";
|
||||
import { Button, ChannelStore, FluxDispatcher, Forms, SelectedChannelStore, useMemo, UserStore } from "@webpack/common";
|
||||
|
||||
interface VoiceState {
|
||||
userId: string;
|
||||
@ -304,7 +305,7 @@ export default definePlugin({
|
||||
</Forms.FormText>
|
||||
{hasEnglishVoices && (
|
||||
<>
|
||||
<Forms.FormTitle className={Margins.marginTop20} tag="h3">Play Example Sounds</Forms.FormTitle>
|
||||
<Forms.FormTitle className={Margins.top20} tag="h3">Play Example Sounds</Forms.FormTitle>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
|
@ -20,10 +20,11 @@ import { addButton, removeButton } from "@api/MessagePopover";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Button, ChannelStore, Forms, Margins, Parser, Text } from "@webpack/common";
|
||||
import { Button, ChannelStore, Forms, Parser, Text } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
|
||||
@ -98,7 +99,7 @@ function openViewRawModal(msg: Message) {
|
||||
<>
|
||||
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
|
||||
<CodeBlock content={msg.content} lang="" />
|
||||
<Forms.FormDivider className={Margins.marginBottom20} />
|
||||
<Forms.FormDivider className={Margins.bottom20} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -121,6 +121,13 @@ export default definePlugin({
|
||||
const reactions = getReactionsWithQueue(message, emoji, type);
|
||||
const users = Object.values(reactions).filter(Boolean) as User[];
|
||||
|
||||
for (const user of users) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "USER_UPDATE",
|
||||
user
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}
|
||||
|
@ -22,6 +22,7 @@ import gitRemote from "~git-remote";
|
||||
export const WEBPACK_CHUNK = "webpackChunkdiscord_app";
|
||||
export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
|
||||
export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`;
|
||||
export const SUPPORT_CHANNEL_ID = "1026515880080842772";
|
||||
|
||||
// Add yourself here if you made a plugin
|
||||
export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
@ -196,5 +197,13 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
whqwert: {
|
||||
name: "whqwert",
|
||||
id: 586239091520176128n
|
||||
},
|
||||
lewisakura: {
|
||||
name: "lewisakura",
|
||||
id: 96269247411400704n
|
||||
},
|
||||
cloudburst: {
|
||||
name: "cloudburst",
|
||||
id: 892128204150685769n
|
||||
}
|
||||
});
|
||||
|
25
src/utils/guards.ts
Normal file
25
src/utils/guards.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function isTruthy<T>(item: T): item is Exclude<T, 0 | "" | false | null | undefined> {
|
||||
return Boolean(item);
|
||||
}
|
||||
|
||||
export function isNonNullish<T>(item: T): item is Exclude<T, null | undefined> {
|
||||
return item != null;
|
||||
}
|
@ -141,8 +141,8 @@ export function humanFriendlyJoin(elements: any[], mapper: (e: any) => string =
|
||||
* Calls .join(" ") on the arguments
|
||||
* classes("one", "two") => "one two"
|
||||
*/
|
||||
export function classes(...classes: string[]) {
|
||||
return classes.filter(c => typeof c === "string").join(" ");
|
||||
export function classes(...classes: Array<string | null | undefined>) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,6 +117,7 @@ const ModalAPI = mapMangledModuleLazy("onCloseRequest:null!=", {
|
||||
openModal: filters.byCode("onCloseRequest:null!="),
|
||||
closeModal: filters.byCode("onCloseCallback&&"),
|
||||
openModalLazy: m => m?.length === 1 && filters.byCode(".apply(this,arguments)")(m),
|
||||
closeAllModals: filters.byCode(".value.key,")
|
||||
});
|
||||
|
||||
/**
|
||||
@ -142,3 +143,10 @@ export function openModal(render: RenderFunction, options?: ModalOptions, contex
|
||||
export function closeModal(modalKey: string, contextKey?: string): void {
|
||||
return ModalAPI.closeModal(modalKey, contextKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all open modals
|
||||
*/
|
||||
export function closeAllModals(): void {
|
||||
return ModalAPI.closeAllModals();
|
||||
}
|
||||
|
@ -27,9 +27,13 @@ export function canonicalizeMatch(match: RegExp | string) {
|
||||
return new RegExp(canonSource, match.flags);
|
||||
}
|
||||
|
||||
export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string) {
|
||||
if (typeof replace === "function") return replace;
|
||||
return replace.replaceAll("$self", `Vencord.Plugins.plugins.${pluginName}`);
|
||||
export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string): string | ReplaceFn {
|
||||
const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`;
|
||||
|
||||
if (typeof replace !== "function")
|
||||
return replace.replaceAll("$self", self);
|
||||
|
||||
return (...args) => replace(...args).replaceAll("$self", self);
|
||||
}
|
||||
|
||||
export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) {
|
||||
|
@ -75,6 +75,10 @@ export interface PluginDef {
|
||||
* Whether this plugin is required and forcefully enabled
|
||||
*/
|
||||
required?: boolean;
|
||||
/**
|
||||
* Whether this plugin should be enabled by default, but can be disabled
|
||||
*/
|
||||
enabledByDefault?: boolean;
|
||||
/**
|
||||
* Set this if your plugin only works on Browser or Desktop, not both
|
||||
*/
|
||||
@ -229,9 +233,12 @@ type PluginSettingType<O extends PluginSettingDef> = O extends PluginSettingStri
|
||||
O extends PluginSettingSliderDef ? number :
|
||||
O extends PluginSettingComponentDef ? any :
|
||||
never;
|
||||
type PluginSettingDefaultType<O extends PluginSettingDef> = O extends PluginSettingSelectDef ? (
|
||||
O["options"] extends { default?: boolean; }[] ? O["options"][number]["value"] : undefined
|
||||
) : O extends { default: infer T; } ? T : undefined;
|
||||
|
||||
type SettingsStore<D extends SettingsDefinition> = {
|
||||
[K in keyof D]: PluginSettingType<D[K]>;
|
||||
[K in keyof D]: PluginSettingType<D[K]> | PluginSettingDefaultType<D[K]>;
|
||||
};
|
||||
|
||||
/** An instance of defined plugin settings */
|
||||
|
@ -77,3 +77,25 @@ export async function rebuild() {
|
||||
return oldHashes["patcher.js"] !== newHashes["patcher.js"] ||
|
||||
oldHashes["preload.js"] !== newHashes["preload.js"];
|
||||
}
|
||||
|
||||
export async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) {
|
||||
if (IS_WEB) return;
|
||||
if (checkForDev && IS_DEV) return;
|
||||
|
||||
try {
|
||||
const isOutdated = await checkForUpdates();
|
||||
if (isOutdated) {
|
||||
const wantsUpdate = confirm(confirmMessage);
|
||||
if (wantsUpdate && isNewer) return alert("Your local copy has more recent commits. Please stash or reset them.");
|
||||
if (wantsUpdate) {
|
||||
await update();
|
||||
const needFullRestart = await rebuild();
|
||||
if (needFullRestart) DiscordNative.app.relaunch();
|
||||
else location.reload();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
UpdateLogger.error(err);
|
||||
alert("That also failed :( Try updating or re-installing with the installer!");
|
||||
}
|
||||
}
|
||||
|
@ -32,10 +32,10 @@ export const Forms = {
|
||||
FormText: waitForComponent<t.FormText>("FormText", m => m.Types?.INPUT_PLACEHOLDER),
|
||||
};
|
||||
|
||||
export const Card = waitForComponent<t.Card>("Card", m => m.Types?.PRIMARY === "cardPrimary");
|
||||
export const Card = waitForComponent<t.Card>("Card", m => m.Types?.PRIMARY && m.defaultProps);
|
||||
export const Button = waitForComponent<t.Button>("Button", ["Hovers", "Looks", "Sizes"]);
|
||||
export const Switch = waitForComponent<t.Switch>("Switch", filters.byCode("tooltipNote", "ringTarget"));
|
||||
export const Tooltip = waitForComponent<t.Tooltip>("Tooltip", ["Positions", "Colors"]);
|
||||
export const Tooltip = waitForComponent<t.Tooltip>("Tooltip", filters.byCode("shouldShowTooltip:!1", "clickableOnMobile||"));
|
||||
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
|
||||
export const TextInput = waitForComponent<t.TextInput>("TextInput", ["defaultProps", "Sizes", "contextType"]);
|
||||
export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.byCode("handleSetRef", "textArea"));
|
||||
@ -45,12 +45,12 @@ export const Text = waitForComponent<t.Text>("Text", m => {
|
||||
return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white"));
|
||||
});
|
||||
export const Select = waitForComponent<t.Select>("Select", filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
|
||||
const searchableSelectFilter = filters.byCode("autoFocus", ".Messages.SELECT");
|
||||
export const SearchableSelect = waitForComponent<t.SearchableSelect>("SearchableSelect", m =>
|
||||
m.render && searchableSelectFilter(m.render)
|
||||
);
|
||||
export const Slider = waitForComponent<t.Slider>("Slider", filters.byCode("closestMarkerIndex", "stickToMarkers"));
|
||||
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
|
||||
|
||||
export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record<string, string>;
|
||||
/**
|
||||
* @deprecated Use @utils/margins instead
|
||||
*/
|
||||
export const Margins: t.Margins = findByPropsLazy("marginTop20");
|
||||
export const ButtonLooks: t.ButtonLooks = findByPropsLazy("BLANK", "FILLED", "INVERTED");
|
||||
|
51
src/webpack/common/types/components.d.ts
vendored
51
src/webpack/common/types/components.d.ts
vendored
@ -90,16 +90,17 @@ export type Tooltip = ComponentType<{
|
||||
|
||||
/** Tooltip.Colors.BLACK */
|
||||
color?: string;
|
||||
/** Tooltip.Positions.TOP */
|
||||
/** TooltipPositions.TOP */
|
||||
position?: string;
|
||||
|
||||
tooltipClassName?: string;
|
||||
tooltipContentClassName?: string;
|
||||
}> & {
|
||||
Positions: Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>;
|
||||
Colors: Record<"BLACK" | "BRAND" | "CUSTOM" | "GREEN" | "GREY" | "PRIMARY" | "RED" | "YELLOW", string>;
|
||||
};
|
||||
|
||||
export type TooltipPositions = Record<"BOTTOM" | "CENTER" | "LEFT" | "RIGHT" | "TOP" | "WINDOW_CENTER", string>;
|
||||
|
||||
export type Card = ComponentType<PropsWithChildren<HTMLProps<HTMLDivElement> & {
|
||||
editable?: boolean;
|
||||
outline?: boolean;
|
||||
@ -234,6 +235,49 @@ export type Select = ComponentType<PropsWithChildren<{
|
||||
"aria-labelledby"?: boolean;
|
||||
}>>;
|
||||
|
||||
export type SearchableSelect = ComponentType<PropsWithChildren<{
|
||||
placeholder?: string;
|
||||
options: ReadonlyArray<SelectOption>; // TODO
|
||||
value?: SelectOption;
|
||||
|
||||
/**
|
||||
* - 0 ~ Filled
|
||||
* - 1 ~ Custom
|
||||
*/
|
||||
look?: 0 | 1;
|
||||
className?: string;
|
||||
popoutClassName?: string;
|
||||
wrapperClassName?: string;
|
||||
popoutPosition?: "top" | "left" | "right" | "bottom" | "center" | "window_center";
|
||||
optionClassName?: string;
|
||||
|
||||
autoFocus?: boolean;
|
||||
isDisabled?: boolean;
|
||||
clearable?: boolean;
|
||||
closeOnSelect?: boolean;
|
||||
clearOnSelect?: boolean;
|
||||
multi?: boolean;
|
||||
|
||||
onChange(value: any): void;
|
||||
onSearchChange?(value: string): void;
|
||||
|
||||
onClose?(): void;
|
||||
onOpen?(): void;
|
||||
onBlur?(): void;
|
||||
|
||||
renderOptionPrefix?(option: SelectOption): ReactNode;
|
||||
renderOptionSuffix?(option: SelectOption): ReactNode;
|
||||
|
||||
filter?(option: SelectOption[], query: string): SelectOption[];
|
||||
|
||||
centerCaret?: boolean;
|
||||
debounceTime?: number;
|
||||
maxVisibleItems?: number;
|
||||
popoutWidth?: number;
|
||||
|
||||
"aria-labelledby"?: boolean;
|
||||
}>>;
|
||||
|
||||
export type Slider = ComponentType<PropsWithChildren<{
|
||||
initialValue: number;
|
||||
defaultValue?: number;
|
||||
@ -278,7 +322,4 @@ export type Flex = ComponentType<PropsWithChildren<any>> & {
|
||||
Direction: Record<"VERTICAL" | "HORIZONTAL" | "HORIZONTAL_REVERSE", string>;
|
||||
Justify: Record<"START" | "END" | "CENTER" | "BETWEEN" | "AROUND", string>;
|
||||
Wrap: Record<"NO_WRAP" | "WRAP" | "WRAP_REVERSE", string>;
|
||||
|
||||
Content: ComponentType<PropsWithChildren<any>>;
|
||||
Sidebar: ComponentType<PropsWithChildren<any>>;
|
||||
};
|
||||
|
@ -307,13 +307,6 @@ export function findByPropsLazy(...props: string[]) {
|
||||
return findLazy(filters.byProps(...props));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all modules that have the specified properties
|
||||
*/
|
||||
export function findAllByProps(...props: string[]) {
|
||||
return findAll(filters.byProps(...props));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a function by its code
|
||||
*/
|
||||
|
Reference in New Issue
Block a user