Compare commits

...

62 Commits

Author SHA1 Message Date
03915b7533 Bump to v1.1.0 2023-03-02 21:19:33 +01:00
5e2ec368ad patches: Make $self more robust 2023-03-02 21:17:15 +01:00
ab8c93fbac Rewrite MessageLinkEmbeds part 2 2023-03-02 21:05:09 +01:00
d6a3edefd9 Rewrite MessageLinkEmbeds to improve Code Quality 2023-03-02 21:01:31 +01:00
727297ec4e Fix messageLinkEmbeds 2023-03-02 18:49:24 +01:00
eccc4b0be1 feat(plugins): add FixInbox plugin (#552) 2023-03-02 04:55:30 +00:00
8465140bc4 Bump to v1.0.9 2023-03-01 21:40:31 +01:00
e6ccb751a0 Fix for latest Discord Update (#550)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-03-01 21:35:08 +01:00
dfc7a15083 chore: extend description of NoDevtoolsWarning plugin (#545)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-01 18:32:58 +01:00
37003edae9 fix(Notifications): Correctly close errored notifications 2023-03-01 05:45:17 +01:00
faa90eccd3 feat: Crash Handler (#531)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-01 05:26:13 +01:00
c91b0df607 GMPolyfill: add header prop (#543) 2023-02-28 23:13:49 +01:00
Ven
f56d99e133 Update README.md 2023-02-28 22:38:02 +01:00
Ven
c690662802 Improve README 2023-02-28 22:37:09 +01:00
4918d699d5 Windows: Add Option to use native titlebar ~ Closes #537 2023-02-28 22:17:39 +01:00
5ec517875e typings for defaultless settings (#512)
* typings for defaultless settings

* fix other silly typings

* type guard utils

---------

Co-authored-by: Ven <vendicated@riseup.net>
2023-02-28 06:12:35 +01:00
cf56ad985b oop oop oop 2023-02-28 02:43:58 +01:00
c09d1558f7 Add SupportHelper plugin 2023-02-28 02:40:45 +01:00
eb190b660e Bump to v1.0.8 2023-02-28 01:50:17 +01:00
d6f9068695 feat: SearchableSelect (#518)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-28 00:48:58 +01:00
cb507babaa fix: vcDoubleClick and revealAllSpoilers patch (#517)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-28 00:41:14 +01:00
235d114193 Improve ConsoleShortcuts plugin 2023-02-28 00:38:28 +01:00
9aba70dcb1 Fix MenuItemDeobfuscator 2023-02-28 00:17:39 +01:00
0b61d29c31 Fix TypingTweaks 2023-02-28 00:17:28 +01:00
335a13a38a fix tooltip component check (#541) 2023-02-27 21:19:01 +00:00
128ee41252 ErrorBoundary: Do not use any Discord components to be more robust 2023-02-25 19:10:01 +01:00
ccca41a168 Bump to v1.0.7 2023-02-24 06:08:45 +01:00
af4c7d8a90 Fix Cards (they look ugly now, wtf Discord) 2023-02-24 05:48:37 +01:00
77c691651e ReviewDB: Show edit instead of create review where applicable (#466)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-18 03:35:51 +01:00
e14ec96e21 feat(FakeNitro): Bypass client themes and fixes (#504)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-18 03:32:02 +01:00
ff1f337699 Fix QuickCSS on electron 20+ 2023-02-17 15:37:38 +01:00
3ca87848e5 TypingIndicator: Fix a dumb (#503)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-17 01:31:55 +01:00
9420735bc7 Version 1.0.6 2023-02-16 23:40:38 +01:00
6807820f6c Badges should use ErrorBoundaries 2023-02-16 22:46:51 +01:00
3cad0d60b4 Silly Discord changed a bunch of css vars 2023-02-16 22:40:19 +01:00
fbbc198b1b Fix PlatformIndicator 2023-02-16 22:31:13 +01:00
224ae979f2 feat(plugins): Typing Indicator (#502) 2023-02-16 03:57:57 +01:00
27fc20118b feat(plugin): RoleColorEverywhere (#482)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-16 02:50:42 +01:00
60ccd8cc25 Various plugin fixes (#492)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-16 02:00:09 +01:00
5c1519156b feat(plugin): ColorSighted (#501) 2023-02-16 01:46:14 +01:00
58270ef925 bump to v1.0.5 2023-02-14 19:22:01 +01:00
68055977d2 NotificationAPI: Correctly request browser permissions 2023-02-14 19:20:10 +01:00
2b0c25b45c Feat(InvisibleChat): Add Autodecryption (#490)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-12 22:10:03 +01:00
c154965d70 TypingTweaks: Fix crash after changing language 2023-02-12 21:07:05 +01:00
Ven
614234ad20 MessageLinkEmbeds: Prevent infinite cycles (#488) 2023-02-12 19:43:57 +01:00
2489bc6831 Fix WhoReacted (#487)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-12 18:58:44 +01:00
d95be1acba refactor: update plugins to use $self (#478)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-10 22:41:49 +01:00
Ven
1d995e58f5 Notification API (#467)
Co-authored-by: Ven <vendicated@riseup.net>
Co-authored-by: afn <hey@afn.lol>
Co-authored-by: afn <afnzmn@gmail.com>
2023-02-10 22:33:34 +01:00
6114bc6b16 make proxies enumerable (#476)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-09 21:21:14 +01:00
ae98401bd3 Fix lag when alt tabbing to Discord 2023-02-09 19:36:30 +01:00
992a77e76c ShowHiddenChannels: Stage and voice channels support (#469)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-08 21:54:11 +01:00
291f38115c New webpack filter: byDisplayName (#474) 2023-02-08 21:48:26 +01:00
8a52189378 feat(plugin): richerCider (#471)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-08 21:48:12 +01:00
70278f64a9 Fix broken patches 2023-02-01 18:00:25 +01:00
7b1d03699d ci(reporter): Ignore 404/429 errors 2023-02-01 14:13:55 +01:00
8b40760187 fix(showHiddenChannels): revert lock icon to correct path (#465) 2023-02-01 13:59:58 +01:00
de0990434e feat(plugin): RevealAllSpoilers (#381)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-01 13:38:02 +01:00
369d179bbf ShowHiddenChannels: New screen for showing hidden channels (#460)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-01 12:11:05 +01:00
8f4e8d0a9b TypingTweaks: fix crash on non en-US locales (#463) 2023-01-31 06:35:52 +01:00
62f7e4d45c Add stylelint 2023-01-30 05:04:06 +01:00
fce7d6b681 Make webpack types importable from @webpack/types 2023-01-30 04:53:28 +01:00
69715070b9 browser ext: change applications to browser_specific_settings 2023-01-29 00:22:11 +01:00
118 changed files with 3726 additions and 952 deletions

View File

@ -82,7 +82,6 @@
"no-constant-condition": ["error", { "checkLoops": false }], "no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error", "no-duplicate-imports": "error",
"no-extra-semi": "error", "no-extra-semi": "error",
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
"dot-notation": "error", "dot-notation": "error",
"no-useless-escape": [ "no-useless-escape": [
"error", "error",

6
.stylelintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "stylelint-config-standard",
"rules": {
"indentation": 4
}
}

View File

@ -1,11 +1,11 @@
{ {
"recommendations": [ "recommendations": [
"EditorConfig.EditorConfig",
"pmneo.tsimporter",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"eamodio.gitlens",
"EditorConfig.EditorConfig",
"ExodiusStudios.comment-anchors",
"formulahendry.auto-rename-tag", "formulahendry.auto-rename-tag",
"GregorBiswanger.json2ts", "GregorBiswanger.json2ts",
"eamodio.gitlens", "stylelint.vscode-stylelint"
"kamikillerto.vscode-colorize"
] ]
} }

View File

@ -4,12 +4,14 @@ The cutest Discord client mod
## Features ## Features
- Super easy to install (one click installer) - Super easy to install (Download Installer, open, click install button, done)
- 90+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033) - 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 - 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 - 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) - 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 - Maintained very actively, broken plugins are usually fixed within 12 hours
## Installing / Uninstalling ## 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) [![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 ## Building from Source

View File

@ -92,6 +92,7 @@ function GM_fetch(url, opt) {
resp.arrayBuffer = () => blobTo("arrayBuffer", blob); resp.arrayBuffer = () => blobTo("arrayBuffer", blob);
resp.text = () => blobTo("text", blob); resp.text = () => blobTo("text", blob);
resp.json = async () => JSON.parse(await blobTo("text", blob)); resp.json = async () => JSON.parse(await blobTo("text", blob));
resp.headers = new Headers(parseHeaders(resp.responseHeaders));
resolve(resp); resolve(resp);
}; };
options.ontimeout = () => reject("fetch timeout"); options.ontimeout = () => reject("fetch timeout");

View File

@ -42,7 +42,7 @@
] ]
}, },
"applications": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "vencord-firefox@vendicated.dev", "id": "vencord-firefox@vendicated.dev",
"strict_min_version": "109.0" "strict_min_version": "109.0"

View File

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.0.4", "version": "1.1.0",
"description": "The cutest Discord client mod", "description": "The cutest Discord client mod",
"keywords": [], "keywords": [],
"homepage": "https://github.com/Vendicated/Vencord#readme", "homepage": "https://github.com/Vendicated/Vencord#readme",
@ -22,8 +22,9 @@
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"inject": "node scripts/runInstaller.mjs", "inject": "node scripts/runInstaller.mjs",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint-styles": "stylelint \"src/**/*.css\"",
"lint:fix": "pnpm lint --fix", "lint:fix": "pnpm lint --fix",
"test": "pnpm lint && pnpm build && pnpm testTsc", "test": "pnpm build && pnpm lint && pnpm lint-styles && pnpm testTsc",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc", "testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit", "testTsc": "tsc --noEmit",
"uninject": "node scripts/runInstaller.mjs", "uninject": "node scripts/runInstaller.mjs",
@ -56,6 +57,8 @@
"moment": "^2.29.4", "moment": "^2.29.4",
"puppeteer-core": "^19.6.0", "puppeteer-core": "^19.6.0",
"standalone-electron-types": "^1.0.0", "standalone-electron-types": "^1.0.0",
"stylelint": "^14.16.1",
"stylelint-config-standard": "^29.0.0",
"type-fest": "^3.5.3", "type-fest": "^3.5.3",
"typescript": "^4.9.4" "typescript": "^4.9.4"
}, },

727
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -95,3 +95,12 @@ async function init() {
} }
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 });
}

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ErrorBoundary from "@components/ErrorBoundary";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
import { ComponentType, HTMLProps } from "react"; import { ComponentType, HTMLProps } from "react";
@ -52,6 +53,7 @@ const Badges = new Set<ProfileBadge>();
* @param badge The badge to register * @param badge The badge to register
*/ */
export function addBadge(badge: ProfileBadge) { export function addBadge(badge: ProfileBadge) {
badge.component &&= ErrorBoundary.wrap(badge.component, { noop: true });
Badges.add(badge); Badges.add(badge);
} }

View File

@ -0,0 +1,94 @@
/*
* 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 "./styles.css";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Forms, React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
import { NotificationData } from "./Notifications";
export default ErrorBoundary.wrap(function NotificationComponent({
title,
body,
richBody,
color,
icon,
onClick,
onClose,
image
}: NotificationData) {
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
const [isHover, setIsHover] = useState(false);
const [elapsed, setElapsed] = useState(0);
const start = useMemo(() => Date.now(), [timeout, isHover, hasFocus]);
useEffect(() => {
if (isHover || !hasFocus || timeout === 0) return void setElapsed(0);
const intervalId = setInterval(() => {
const elapsed = Date.now() - start;
if (elapsed >= timeout)
onClose!();
else
setElapsed(elapsed);
}, 10);
return () => clearInterval(intervalId);
}, [timeout, isHover, hasFocus]);
const timeoutProgress = elapsed / timeout;
return (
<button
className="vc-notification-root"
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
onClick={onClick}
onContextMenu={e => {
e.preventDefault();
e.stopPropagation();
onClose!();
}}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
>
<div className="vc-notification">
{icon && <img className="vc-notification-icon" src={icon} alt="" />}
<div className="vc-notification-content">
<Forms.FormTitle tag="h2">{title}</Forms.FormTitle>
<div>
{richBody ?? <p className="vc-notification-p">{body}</p>}
</div>
</div>
</div>
{image && <img className="vc-notification-img" src={image} alt="" />}
{timeout !== 0 && (
<div
className="vc-notification-progressbar"
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }}
/>
)}
</button>
);
}, {
onError: ({ props }) => props.onClose!()
});

View File

@ -0,0 +1,99 @@
/*
* 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 { Settings } from "@api/settings";
import { Queue } from "@utils/Queue";
import { ReactDOM } from "@webpack/common";
import type { ReactNode } from "react";
import type { Root } from "react-dom/client";
import NotificationComponent from "./NotificationComponent";
const NotificationQueue = new Queue();
let reactRoot: Root;
let id = 42;
function getRoot() {
if (!reactRoot) {
const container = document.createElement("div");
container.id = "vc-notification-container";
document.body.append(container);
reactRoot = ReactDOM.createRoot(container);
}
return reactRoot;
}
export interface NotificationData {
title: string;
body: string;
/**
* Same as body but can be a custom component.
* Will be used over body if present.
* Not supported on desktop notifications, those will fall back to body */
richBody?: ReactNode;
/** Small icon. This is for things like profile pictures and should be square */
icon?: string;
/** Large image. Optimally, this should be around 16x9 but it doesn't matter much. Desktop Notifications might not support this */
image?: string;
onClick?(): void;
onClose?(): void;
color?: string;
}
function _showNotification(notification: NotificationData, id: number) {
const root = getRoot();
return new Promise<void>(resolve => {
root.render(
<NotificationComponent key={id} {...notification} onClose={() => {
notification.onClose?.();
root.render(null);
resolve();
}} />,
);
});
}
function shouldBeNative() {
const { useNative } = Settings.notifications;
if (useNative === "always") return true;
if (useNative === "not-focused") return !document.hasFocus();
return false;
}
export async function requestPermission() {
return (
Notification.permission === "granted" ||
(Notification.permission !== "denied" && (await Notification.requestPermission()) === "granted")
);
}
export async function showNotification(data: NotificationData) {
if (shouldBeNative() && await requestPermission()) {
const { title, body, icon, image, onClick = null, onClose = null } = data;
const n = new Notification(title, {
body,
icon,
image
});
n.onclick = onClick;
n.onclose = onClose;
} else {
NotificationQueue.push(() => _showNotification(data, id++));
}
}

View File

@ -0,0 +1,19 @@
/*
* 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 * from "./Notifications";

View File

@ -0,0 +1,49 @@
.vc-notification-root {
/* clear default button styles */
all: unset;
display: flex;
flex-direction: column;
width: 25vw;
min-height: 10vh;
color: var(--text-normal);
background-color: var(--background-secondary-alt);
position: absolute;
z-index: 2147483647;
right: 1rem;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
}
.vc-notification {
display: flex;
flex-direction: row;
padding: 1.25rem;
gap: 1.25rem;
}
.vc-notification-icon {
height: 4rem;
width: 4rem;
border-radius: 6px;
}
/* Discord adding 3km margin to generic tags */
.vc-notification h2 {
margin: unset;
}
.vc-notification-progressbar {
height: 0.25rem;
border-radius: 5px;
margin-top: auto;
}
.vc-notification-p {
margin: 0.5rem 0 0;
line-height: 140%;
}
.vc-notification-img {
width: 100%;
}

View File

@ -25,6 +25,7 @@ import * as $MessageDecorations from "./MessageDecorations";
import * as $MessageEventsAPI from "./MessageEvents"; import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover"; import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices"; import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList"; import * as $ServerList from "./ServerList";
import * as $Styles from "./Styles"; import * as $Styles from "./Styles";
@ -88,3 +89,7 @@ export const MemberListDecorators = $MemberListDecorators;
* a * a
*/ */
export const Styles = $Styles; export const Styles = $Styles;
/**
* An API allowing you to display notifications
*/
export const Notifications = $Notifications;

View File

@ -34,12 +34,19 @@ export interface Settings {
frameless: boolean; frameless: boolean;
transparent: boolean; transparent: boolean;
winCtrlQ: boolean; winCtrlQ: boolean;
winNativeTitleBar: boolean;
plugins: { plugins: {
[plugin: string]: { [plugin: string]: {
enabled: boolean; enabled: boolean;
[setting: string]: any; [setting: string]: any;
}; };
}; };
notifications: {
timeout: number;
position: "top-right" | "bottom-right";
useNative: "always" | "never" | "not-focused";
};
} }
const DefaultSettings: Settings = { const DefaultSettings: Settings = {
@ -51,7 +58,14 @@ const DefaultSettings: Settings = {
frameless: false, frameless: false,
transparent: false, transparent: false,
winCtrlQ: false, winCtrlQ: false,
plugins: {} winNativeTitleBar: false,
plugins: {},
notifications: {
timeout: 5000,
position: "bottom-right",
useNative: "not-focused"
}
}; };
try { try {
@ -78,7 +92,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
// Return empty for plugins with no settings // Return empty for plugins with no settings
if (path === "plugins" && p in plugins) if (path === "plugins" && p in plugins)
return target[p] = makeProxy({ return target[p] = makeProxy({
enabled: plugins[p].required ?? false enabled: plugins[p].required ?? plugins[p].enabledByDefault ?? false
}, root, `plugins.${p}`); }, root, `plugins.${p}`);
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve // Since the property is not set, check if this is a plugin's setting and if so, try to resolve

View File

@ -17,20 +17,24 @@
*/ */
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { Margins } from "@utils/margins";
import { LazyComponent } from "@utils/misc"; import { LazyComponent } from "@utils/misc";
import { Margins, React } from "@webpack/common"; import { React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard"; import { ErrorCard } from "./ErrorCard";
interface Props { interface Props<T = any> {
/** Render nothing if an error occurs */ /** Render nothing if an error occurs */
noop?: boolean; noop?: boolean;
/** Fallback component to render if an error occurs */ /** Fallback component to render if an error occurs */
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>; fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
/** called when an error occurs */ /** called when an error occurs. The props property is only available if using .wrap */
onError?(error: Error, errorInfo: React.ErrorInfo): void; onError?(data: { error: Error, errorInfo: React.ErrorInfo, props: T; }): void;
/** Custom error message */ /** Custom error message */
message?: string; message?: string;
/** The props passed to the wrapped component. Only used by wrap */
wrappedProps?: T;
} }
const color = "#e78284"; const color = "#e78284";
@ -65,7 +69,7 @@ const ErrorBoundary = LazyComponent(() => {
} }
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 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("A component threw an Error\n", error);
logger.error("Component Stack", errorInfo.componentStack); 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."; const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
return ( return (
<ErrorCard style={{ <ErrorCard style={{ overflow: "hidden" }}>
overflow: "hidden",
}}>
<h1>Oh no!</h1> <h1>Oh no!</h1>
<p>{msg}</p> <p>{msg}</p>
<code> <code>
{this.state.message} {this.state.message}
{!!this.state.stack && ( {!!this.state.stack && (
<pre className={Margins.marginTop8}> <pre className={Margins.top8}>
{this.state.stack} {this.state.stack}
</pre> </pre>
)} )}
@ -103,11 +105,11 @@ const ErrorBoundary = LazyComponent(() => {
}; };
}) as }) as
React.ComponentType<React.PropsWithChildren<Props>> & { React.ComponentType<React.PropsWithChildren<Props>> & {
wrap<T extends JSX.IntrinsicAttributes = 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.wrap = (Component, errorBoundaryProps) => props => (
<ErrorBoundary {...errorBoundaryProps}> <ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
<Component {...props} /> <Component {...props} />
</ErrorBoundary> </ErrorBoundary>
); );

View 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);
}

View File

@ -16,24 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Card } from "@webpack/common"; import "./ErrorCard.css";
interface Props { import { classes } from "@utils/misc";
style?: React.CSSProperties; import type { HTMLProps } from "react";
className?: string;
} export function ErrorCard(props: React.PropsWithChildren<HTMLProps<HTMLDivElement>>) {
export function ErrorCard(props: React.PropsWithChildren<Props>) {
return ( return (
<Card className={props.className} style={ <div {...props} className={classes(props.className, "vc-error-card")}>
{
padding: "2em",
backgroundColor: "#e7828430",
borderColor: "#e78284",
color: "var(--text-normal)",
...props.style
}
}>
{props.children} {props.children}
</Card> </div>
); );
} }

View File

@ -17,10 +17,11 @@
*/ */
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import { Margins } from "@utils/margins";
import { makeCodeblock } from "@utils/misc"; import { makeCodeblock } from "@utils/misc";
import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplace, ReplaceFn } from "@utils/patches";
import { search } from "@webpack"; 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 { CheckedTextInput } from "./CheckedTextInput";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
@ -128,7 +129,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
)} )}
{!!diff?.length && ( {!!diff?.length && (
<Button className={Margins.marginTop20} onClick={() => { <Button className={Margins.top20} onClick={() => {
try { try {
Function(patchedCode.replace(/^function\(/, "function patchedModule(")); Function(patchedCode.replace(/^function\(/, "function patchedModule("));
setCompileResult([true, "Compiled successfully"]); setCompileResult([true, "Compiled successfully"]);
@ -202,7 +203,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
)} )}
<Switch <Switch
className={Margins.marginTop8} className={Margins.top8}
value={isFunc} value={isFunc}
onChange={setIsFunc} onChange={setIsFunc}
note="'replacement' will be evaled if this is toggled" note="'replacement' will be evaled if this is toggled"
@ -256,7 +257,7 @@ function PatchHelper() {
return ( return (
<Forms.FormSection> <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> <Forms.FormTitle>find</Forms.FormTitle>
<TextInput <TextInput
type="text" type="text"
@ -296,7 +297,7 @@ function PatchHelper() {
{!!(find && match && replacement) && ( {!!(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> <div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button> <Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
</> </>

View File

@ -30,11 +30,12 @@ import PluginModal from "@components/PluginSettings/PluginModal";
import { Switch } from "@components/Switch"; import { Switch } from "@components/Switch";
import { ChangeList } from "@utils/ChangeList"; import { ChangeList } from "@utils/ChangeList";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes, LazyComponent, useAwaiter } from "@utils/misc"; import { classes, LazyComponent, useAwaiter } from "@utils/misc";
import { openModalLazy } from "@utils/modal"; import { openModalLazy } from "@utils/modal";
import { Plugin } from "@utils/types"; import { Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack"; 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"; import Plugins from "~plugins";
@ -222,7 +223,7 @@ export default ErrorBoundary.wrap(function PluginSettings() {
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status })); const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => { const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
const enabled = settings.plugins[plugin.name]?.enabled || plugin.started; const enabled = settings.plugins[plugin.name]?.enabled;
if (enabled && searchValue.status === SearchStatus.DISABLED) return false; if (enabled && searchValue.status === SearchStatus.DISABLED) return false;
if (!enabled && searchValue.status === SearchStatus.ENABLED) return false; if (!enabled && searchValue.status === SearchStatus.ENABLED) return false;
if (!searchValue.value.length) return true; if (!searchValue.value.length) return true;
@ -296,15 +297,15 @@ export default ErrorBoundary.wrap(function PluginSettings() {
} }
return ( return (
<Forms.FormSection className={Margins.marginTop16}> <Forms.FormSection className={Margins.top16}>
<ReloadRequiredCard required={changes.hasChanges} /> <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 Filters
</Forms.FormTitle> </Forms.FormTitle>
<div className={cl("filter-controls")}> <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}> <div className={InputStyles.inputWrapper}>
<Select <Select
className={InputStyles.inputDefault} className={InputStyles.inputDefault}
@ -321,15 +322,15 @@ export default ErrorBoundary.wrap(function PluginSettings() {
</div> </div>
</div> </div>
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle> <Forms.FormTitle className={Margins.top20}>Plugins</Forms.FormTitle>
<div className={cl("grid")}> <div className={cl("grid")}>
{plugins} {plugins}
</div> </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 Required Plugins
</Forms.FormTitle> </Forms.FormTitle>
<div className={cl("grid")}> <div className={cl("grid")}>

View File

@ -94,6 +94,7 @@
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2; line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
/* stylelint-disable-next-line property-no-unknown */
box-orient: vertical; box-orient: vertical;
} }
@ -132,6 +133,6 @@
margin-top: 0.5em; margin-top: 0.5em;
} }
.vc-plugins-info-button svg:not(:hover):not(:focus) { .vc-plugins-info-button svg:not(:hover, :focus) {
color: var(--text-muted); color: var(--text-muted);
} }

View File

@ -26,8 +26,8 @@ interface SwitchProps {
disabled?: boolean; disabled?: boolean;
} }
const SWITCH_ON = "var(--status-green-600)"; const SWITCH_ON = "var(--green-360)";
const SWITCH_OFF = "var(--primary-dark-400)"; const SWITCH_OFF = "var(--primary-400)";
const SwitchClasses = findByPropsLazy("slider", "input", "container"); const SwitchClasses = findByPropsLazy("slider", "input", "container");
export function Switch({ checked, onChange, disabled }: SwitchProps) { export function Switch({ checked, onChange, disabled }: SwitchProps) {

View File

@ -18,25 +18,26 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync"; 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() { function BackupRestoreTab() {
return ( 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")}> <Card className={classes("vc-settings-card", "vc-backup-restore-card")}>
<Flex flexDirection="column"> <Flex flexDirection="column">
<strong>Warning</strong> <strong>Warning</strong>
<span>Importing a settings file will overwrite your current settings.</span> <span>Importing a settings file will overwrite your current settings.</span>
</Flex> </Flex>
</Card> </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. You can import and export your Vencord settings as a JSON file.
This allows you to easily transfer your settings to another device, This allows you to easily transfer your settings to another device,
or recover your settings after reinstalling Vencord or Discord. or recover your settings after reinstalling Vencord or Discord.
</Text> </Text>
<Text variant="text-md/normal" className={Margins.marginBottom8}> <Text variant="text-md/normal" className={Margins.bottom8}>
Settings Export contains: Settings Export contains:
<ul> <ul>
<li>&mdash; Custom QuickCSS</li> <li>&mdash; Custom QuickCSS</li>

View File

@ -19,9 +19,10 @@
import { useSettings } from "@api/settings"; import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { useAwaiter } from "@utils/misc"; import { useAwaiter } from "@utils/misc";
import { findLazy } from "@webpack"; 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"); const TextAreaProps = findLazy(m => typeof m.textarea === "string");
@ -51,7 +52,7 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) {
return ( 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> <Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div> <div>
{themeLinks.map(link => ( {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.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText> <Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>Make sure to use the raw links or github.io links!</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> <Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
<div style={{ marginBottom: ".5em" }}> <div style={{ marginBottom: ".5em" }}>
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes"> <Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">

View File

@ -22,9 +22,10 @@ import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed"; import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { classes, useAwaiter } from "@utils/misc"; import { classes, useAwaiter } from "@utils/misc";
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater"; 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"; import gitHash from "~git-hash";
@ -109,14 +110,14 @@ function Updatable(props: CommonProps) {
</ErrorCard> </ErrorCard>
</> </>
) : ( ) : (
<Forms.FormText className={Margins.marginBottom8}> <Forms.FormText className={Margins.bottom8}>
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"} {isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
</Forms.FormText> </Forms.FormText>
)} )}
{isOutdated && <Changes updates={updates} {...props} />} {isOutdated && <Changes updates={updates} {...props} />}
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}> <Flex className={classes(Margins.bottom8, Margins.top8)}>
{isOutdated && <Button {isOutdated && <Button
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking} disabled={isUpdating || isChecking}
@ -175,7 +176,7 @@ function Updatable(props: CommonProps) {
function Newer(props: CommonProps) { function Newer(props: CommonProps) {
return ( return (
<> <>
<Forms.FormText className={Margins.marginBottom8}> <Forms.FormText className={Margins.bottom8}>
Your local copy has more recent commits. Please stash or reset them. Your local copy has more recent commits. Please stash or reset them.
</Forms.FormText> </Forms.FormText>
<Changes {...props} updates={changes} /> <Changes {...props} updates={changes} />
@ -199,7 +200,7 @@ function Updater() {
}; };
return ( return (
<Forms.FormSection className={Margins.marginTop16}> <Forms.FormSection className={Margins.top16}>
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle> <Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
<Switch <Switch
value={settings.notifyAboutUpdates} value={settings.notifyAboutUpdates}
@ -225,7 +226,7 @@ function Updater() {
</Link> </Link>
)} (<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)</Forms.FormText> )} (<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> <Forms.FormTitle tag="h5">Updates</Forms.FormTitle>

View File

@ -21,23 +21,69 @@ import { useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton"; import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import IpcEvents from "@utils/IpcEvents"; import IpcEvents from "@utils/IpcEvents";
import { useAwaiter } from "@utils/misc"; import { Margins } from "@utils/margins";
import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common"; import { identity, useAwaiter } from "@utils/misc";
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
const cl = classNameFactory("vc-settings-"); const cl = classNameFactory("vc-settings-");
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png"; const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png"; const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
type KeysOfType<Object, Type> = {
[K in keyof Object]: Object[K] extends Type ? K : never;
}[keyof Object];
function VencordSettings() { function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), { const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
fallbackValue: "Loading..." fallbackValue: "Loading..."
}); });
const settings = useSettings(); const settings = useSettings();
const notifSettings = settings.notifications;
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []); const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
const isWindows = navigator.platform.toLowerCase().startsWith("win");
const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>;
title: string;
note: string;
}> =
[
{
key: "useQuickCss",
title: "Enable Custom CSS",
note: "Loads your Custom CSS"
},
!IS_WEB && {
key: "enableReactDevtools",
title: "Enable React Developer Tools",
note: "Requires a full restart"
},
!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",
note: "Requires a full restart"
},
!IS_WEB && isWindows && {
key: "winCtrlQ",
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
note: "Requires a full restart"
}
];
return ( return (
<React.Fragment> <React.Fragment>
<DonateCard image={donateImage} /> <DonateCard image={donateImage} />
@ -82,52 +128,76 @@ function VencordSettings() {
<Forms.FormDivider /> <Forms.FormDivider />
<Forms.FormSection className={Margins.marginTop16} title="Settings"> <Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
<Forms.FormText className={Margins.marginBottom20}> <Forms.FormText className={Margins.bottom20}>
Hint: You can change the position of this settings section in the settings of the "Settings" plugin! Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
</Forms.FormText> </Forms.FormText>
{Switches.map(s => s && (
<Switch <Switch
value={settings.useQuickCss} key={s.key}
onChange={(v: boolean) => settings.useQuickCss = v} value={settings[s.key]}
note="Loads styles from your QuickCSS file"> onChange={v => settings[s.key] = v}
Use QuickCSS note={s.note}
</Switch>
{!IS_WEB && (
<React.Fragment>
<Switch
value={settings.enableReactDevtools}
onChange={(v: boolean) => settings.enableReactDevtools = v}
note="Requires a full restart"
> >
Enable React Developer Tools {s.title}
</Switch> </Switch>
<Switch ))}
value={settings.frameless}
onChange={(v: boolean) => settings.frameless = v}
note="Requires a full restart"
>
Disable the window frame
</Switch>
<Switch
value={settings.transparent}
onChange={(v: boolean) => settings.transparent = v}
note="Requires a full restart"
>
Enable window transparency
</Switch>
{navigator.platform.toLowerCase().startsWith("win") && (
<Switch
value={settings.winCtrlQ}
onChange={(v: boolean) => settings.winCtrlQ = v}
note="Requires a full restart"
>
Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)
</Switch>
)}
</React.Fragment>
)}
</Forms.FormSection> </Forms.FormSection>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{notifSettings.useNative !== "never" && Notification.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
</ErrorCard>
)}
<Forms.FormText className={Margins.bottom8}>
Some plugins may show you notifications. These come in two styles:
<ul>
<li><strong>Vencord Notifications</strong>: These are in-app notifications</li>
<li><strong>Desktop Notifications</strong>: Native Desktop notifications (like when you get a ping)</li>
</ul>
</Forms.FormText>
<Select
placeholder="Notification Style"
options={[
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => notifSettings.useNative = v}
isSelected={v => v === notifSettings.useNative}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select
isDisabled={notifSettings.useNative === "always"}
placeholder="Notification Position"
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
select={v => notifSettings.position = v}
isSelected={v => v === notifSettings.position}
serialize={identity}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
<Slider
disabled={notifSettings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0}
maxValue={20_000}
initialValue={notifSettings.timeout}
onValueChange={v => notifSettings.timeout = v}
onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
</React.Fragment> </React.Fragment>
); );
} }

View File

@ -20,6 +20,7 @@ import "./settingsStyles.css";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { findByCodeLazy } from "@webpack"; import { findByCodeLazy } from "@webpack";
import { Forms, SettingsRouter, Text } from "@webpack/common"; 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> <Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
<TabBar <TabBar
type={TabBar.Types.TOP} type="top"
look={TabBar.Looks.BRAND} look="brand"
className={cl("tab-bar")} className={cl("tab-bar")}
selectedItem={tab} selectedItem={tab}
onItemSelect={SettingsRouter.open} onItemSelect={SettingsRouter.open}
@ -83,7 +84,7 @@ function Settings(props: SettingsProps) {
} }
export default function (props: SettingsProps) { export default function (props: SettingsProps) {
return <ErrorBoundary> return <ErrorBoundary onError={handleComponentFailed}>
<Settings tab={props.tab} /> <Settings tab={props.tab} />
</ErrorBoundary>; </ErrorBoundary>;
} }

View File

@ -16,9 +16,8 @@
gap: 1em; gap: 1em;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
flex-grow: 1; flex-grow: 1;
flex-direction: row; flex-flow: row wrap;
margin-bottom: 1em; margin-bottom: 1em;
} }

View File

@ -16,29 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { isOutdated, rebuild, update } from "@utils/updater"; import { maybePromptToUpdate } from "@utils/updater";
export async function handleComponentFailed() { export function handleComponentFailed() {
if (isOutdated) { maybePromptToUpdate(
setImmediate(async () => {
const wantsUpdate = confirm(
"Uh Oh! Failed to render this Page." + "Uh Oh! Failed to render this Page." +
" However, there is an update available that might fix it." + " However, there is an update available that might fix it." +
" Would you like to update and restart now?" " 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!");
}
}
});
}
} }

View File

@ -91,7 +91,8 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
webPreferences: { webPreferences: {
preload: join(__dirname, "preload.js"), preload: join(__dirname, "preload.js"),
contextIsolation: true, contextIsolation: true,
nodeIntegration: false nodeIntegration: false,
sandbox: false
} }
}); });
await win.loadURL(`data:text/html;base64,${monacoHtml}`); await win.loadURL(`data:text/html;base64,${monacoHtml}`);

3
src/modules.d.ts vendored
View File

@ -38,7 +38,8 @@ declare module "~fileContent/*" {
export default content; export default content;
} }
declare module "*.css" { } declare module "*.css";
declare module "*.css?managed" { declare module "*.css?managed" {
const name: string; const name: string;
export default name; export default name;

View File

@ -79,7 +79,10 @@ if (!process.argv.includes("--vanilla")) {
options.webPreferences.sandbox = false; options.webPreferences.sandbox = false;
if (settings.frameless) { if (settings.frameless) {
options.frame = false; options.frame = false;
} else if (process.platform === "win32" && settings.winNativeTitleBar) {
delete options.frame;
} }
if (settings.transparent) { if (settings.transparent) {
options.transparent = true; options.transparent = true;
options.backgroundColor = "#00000000"; options.backgroundColor = "#00000000";

56
src/plugins/FixInbox.tsx Normal file
View 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>
);
}
});

View File

@ -36,7 +36,7 @@ export default definePlugin({
replacement: { replacement: {
match: /uploadFiles:(.{1,2}),/, match: /uploadFiles:(.{1,2}),/,
replace: replace:
"uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=Vencord.Plugins.plugins.AnonymiseFileNames.anonymise(f.filename)),$1(...args)),", "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),",
}, },
}, },
], ],

View File

@ -24,9 +24,10 @@ import { Heart } from "@components/Heart";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import IpcEvents from "@utils/IpcEvents"; import IpcEvents from "@utils/IpcEvents";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { Margins } from "@utils/margins";
import { closeModal, Modals, openModal } from "@utils/modal"; import { closeModal, Modals, openModal } from "@utils/modal";
import definePlugin from "@utils/types"; 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"; const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
@ -150,7 +151,7 @@ export default definePlugin({
<Forms.FormText> <Forms.FormText>
This Badge is a special perk for Vencord Donors This Badge is a special perk for Vencord Donors
</Forms.FormText> </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!! Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
</Forms.FormText> </Forms.FormText>
</div> </div>

View File

@ -43,7 +43,7 @@ export default definePlugin({
{ {
find: '"Menu API', find: '"Menu API',
replacement: { 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) => { replace: (m, mod) => {
let nicenNames = ""; let nicenNames = "";
const redefines = [] as string[]; const redefines = [] as string[];

View File

@ -22,12 +22,17 @@ import definePlugin from "@utils/types";
export default definePlugin({ export default definePlugin({
name: "MessagePopoverAPI", name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.", description: "API to add buttons to message popovers.",
authors: [Devs.KingFish], authors: [Devs.KingFish, Devs.Ven],
patches: [{ patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: { replacement: {
match: /\?(?<makeButton>\i)\(.{1,35}\.Messages\.CONFIGURE.+?message:(?<message>\i).+?children:\[/, // foo && !bar ? createElement(blah,...makeElement(addReactionData))
replace: "$&...Vencord.Api.MessagePopover._buildPopoverElements($<message>,$<makeButton>)," match: /(\i&&!\i)\?\(0,\i\.jsxs?\)\(.{0,20}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
replace: (m, bools, makeElement) => {
const msg = m.match(/message:(.{1,3}),/)?.[1];
if (!msg) throw new Error("Could not find message variable");
return `...(${bools}?Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}):[]),${m}`;
}
} }
}], }],
}); });

View File

@ -34,8 +34,8 @@ export default definePlugin({
";if(Vencord.Api.Notices.currentNotice)return false$&" ";if(Vencord.Api.Notices.currentNotice)return false$&"
}, },
{ {
match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/, match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
replace: '{if($1?.id=="VencordNotice")return ($1=null,Vencord.Api.Notices.nextNotice(),true);' replace: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);'
} }
] ]
} }

View File

@ -31,7 +31,7 @@ export default definePlugin({
replacement: { replacement: {
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/, match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
replace: replace:
"Vencord.Plugins.plugins.BetterGifAltText.altify(e);$1", "$self.altify(e);$1",
}, },
}, },
{ {
@ -39,7 +39,7 @@ export default definePlugin({
replacement: { replacement: {
match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/, match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/,
replace: replace:
"?($1.alt='GIF',Vencord.Plugins.plugins.BetterGifAltText.altify($1))", "?($1.alt='GIF',$self.altify($1))",
}, },
}, },
], ],

View File

@ -33,7 +33,7 @@ export default definePlugin({
find: "M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z", find: "M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z",
replacement: { replacement: {
match: /viewBox:"0 0 20 20"/, match: /viewBox:"0 0 20 20"/,
replace: "$&,onClick:()=>Vencord.Plugins.plugins.BetterRoleDot.copyToClipBoard(e.color),style:{cursor:'pointer'}", replace: "$&,onClick:()=>$self.copyToClipBoard(e.color),style:{cursor:'pointer'}",
}, },
}, },
{ {

View File

@ -75,7 +75,7 @@ export default definePlugin({
find: ".renderConnectionStatus=", find: ".renderConnectionStatus=",
replacement: { replacement: {
match: /(?<=renderConnectionStatus=.+\.channel,children:)\w/, match: /(?<=renderConnectionStatus=.+\.channel,children:)\w/,
replace: "[$&, Vencord.Plugins.plugins.CallTimer.renderTimer(this.props.channel.id)]" replace: "[$&, $self.renderTimer(this.props.channel.id)]"
} }
}], }],
renderTimer(channelId: string) { renderTimer(channelId: string) {

View 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"
}
}
]
});

View File

@ -18,6 +18,9 @@
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; 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) => () => { const WEB_ONLY = (f: string) => () => {
throw new Error(`'${f}' is Discord Desktop only.`); throw new Error(`'${f}' is Discord Desktop only.`);
@ -29,19 +32,48 @@ export default definePlugin({
authors: [Devs.Ven], authors: [Devs.Ven],
getShortcuts() { 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 { return {
toClip: IS_WEB ? WEB_ONLY("toClip") : window.DiscordNative.clipboard.copy,
fromClip: IS_WEB ? WEB_ONLY("fromClip") : window.DiscordNative.clipboard.read,
wp: Vencord.Webpack, wp: Vencord.Webpack,
wpc: Vencord.Webpack.wreq.c, wpc: Webpack.wreq.c,
wreq: Vencord.Webpack.wreq, wreq: Webpack.wreq,
wpsearch: Vencord.Webpack.search, wpsearch: search,
wpex: Vencord.Webpack.extract, wpex: extract,
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!), wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
findByProps: Vencord.Webpack.findByProps, find: newFindWrapper(f => f),
find: Vencord.Webpack.find, findAll,
Plugins: Vencord.Plugins, findByProps: newFindWrapper(filters.byProps),
React: Vencord.Webpack.Common.React, 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, Settings: Vencord.Settings,
Api: Vencord.Api, Api: Vencord.Api,
reload: () => location.reload(), reload: () => location.reload(),

134
src/plugins/crashHandler.ts Normal file
View 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);
}
}
});

View File

@ -19,6 +19,7 @@
import { definePluginSettings } from "@api/settings"; import { definePluginSettings } from "@api/settings";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards";
import { useAwaiter } from "@utils/misc"; import { useAwaiter } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
@ -56,11 +57,11 @@ interface ActivityAssets {
} }
interface Activity { interface Activity {
state: string; state?: string;
details?: string; details?: string;
timestamps?: { timestamps?: {
start?: Number; start?: number;
end?: Number; end?: number;
}; };
assets?: ActivityAssets; assets?: ActivityAssets;
buttons?: Array<string>; buttons?: Array<string>;
@ -70,7 +71,7 @@ interface Activity {
button_urls?: Array<string>; button_urls?: Array<string>;
}; };
type: ActivityType; type: ActivityType;
flags: Number; flags: number;
} }
enum ActivityType { enum ActivityType {
@ -93,13 +94,13 @@ const numOpt = (description: string) => ({
onChange: setRpc onChange: setRpc
}) as const; }) as const;
const choice = (label: string, value: any, _default?: Boolean) => ({ const choice = (label: string, value: any, _default?: boolean) => ({
label, label,
value, value,
default: _default default: _default
}) as const; }) as const;
const choiceOpt = (description: string, options) => ({ const choiceOpt = <T,>(description: string, options: T) => ({
type: OptionType.SELECT, type: OptionType.SELECT,
description, description,
onChange: setRpc, onChange: setRpc,
@ -173,13 +174,13 @@ async function createActivity(): Promise<Activity | undefined> {
activity.buttons = [ activity.buttons = [
buttonOneText, buttonOneText,
buttonTwoText buttonTwoText
].filter(Boolean); ].filter(isTruthy);
activity.metadata = { activity.metadata = {
button_urls: [ button_urls: [
buttonOneURL, buttonOneURL,
buttonTwoURL buttonTwoURL
].filter(Boolean) ].filter(isTruthy)
}; };
} }
@ -206,12 +207,10 @@ async function createActivity(): Promise<Activity | undefined> {
delete activity[k]; delete activity[k];
} }
// WHAT DO YOU WANT FROM ME
// eslint-disable-next-line consistent-return
return activity; return activity;
} }
async function setRpc(disable?: Boolean) { async function setRpc(disable?: boolean) {
const activity: Activity | undefined = await createActivity(); const activity: Activity | undefined = await createActivity();
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({

View File

@ -20,11 +20,12 @@ import { migratePluginSettings, Settings } from "@api/settings";
import { CheckedTextInput } from "@components/CheckedTextInput"; import { CheckedTextInput } from "@components/CheckedTextInput";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { Margins } from "@utils/margins";
import { makeLazy } from "@utils/misc"; import { makeLazy } from "@utils/misc";
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal"; import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack"; 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; const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
@ -96,7 +97,7 @@ function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: str
return ( return (
<> <>
<Forms.FormTitle className={Margins.marginTop20}>Custom Name</Forms.FormTitle> <Forms.FormTitle className={Margins.top20}>Custom Name</Forms.FormTitle>
<CheckedTextInput <CheckedTextInput
value={name} value={name}
onChange={setName} onChange={setName}
@ -187,7 +188,7 @@ export default definePlugin({
find: "open-native-link", find: "open-native-link",
replacement: { replacement: {
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/, match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
replace: "$&,Vencord.Plugins.plugins.EmoteCloner.makeMenu(arguments[2])" replace: "$&,$self.makeMenu(arguments[2])"
}, },
}, },

View File

@ -22,11 +22,25 @@ import { Devs } from "@utils/constants";
import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies"; import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { ChannelStore, UserStore } from "@webpack/common"; import { ChannelStore, PermissionStore, UserStore } from "@webpack/common";
const DRAFT_TYPE = 0; const DRAFT_TYPE = 0;
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR"); 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 { interface BaseSticker {
available: boolean; available: boolean;
description: string; description: string;
@ -58,26 +72,39 @@ migratePluginSettings("FakeNitro", "NitroBypass");
export default definePlugin({ export default definePlugin({
name: "FakeNitro", name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity], authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain],
description: "Allows you to stream in nitro quality and send fake emojis/stickers.", description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
dependencies: ["MessageEventsAPI"], dependencies: ["MessageEventsAPI"],
patches: [ patches: [
{ {
find: "canUseAnimatedEmojis:function", find: ".PREMIUM_LOCKED;",
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true, predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
replacement: [ replacement: [
"canUseAnimatedEmojis", {
"canUseEmojisEverywhere" match: /(?<=(?<intention>\i)=\i\.intention)/,
].map(func => { replace: ",fakeNitroIntention=$<intention>"
return { },
match: new RegExp(`${func}:function\\(.+?\\{`), {
replace: "$&return true;" 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", 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, predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
replacement: { replacement: {
match: /canUseStickersEverywhere:function\(.+?\{/, match: /canUseStickersEverywhere:function\(.+?\{/,
@ -93,7 +120,7 @@ export default definePlugin({
} }
}, },
{ {
find: "canUseAnimatedEmojis:function", find: "canStreamHighQuality:function",
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true, predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
replacement: [ replacement: [
"canUseHighVideoUploadQuality", "canUseHighVideoUploadQuality",
@ -114,6 +141,13 @@ export default definePlugin({
replace: "" replace: ""
} }
}, },
{
find: "canUseClientThemes:function",
replacement: {
match: /(?<=canUseClientThemes:function\(\i\){)/,
replace: "return true;"
}
}
], ],
options: { options: {
@ -161,6 +195,22 @@ export default definePlugin({
return (UserStore.getCurrentUser().premiumType ?? 0) > 1; 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) { getStickerLink(stickerId: string) {
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`; return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`;
}, },
@ -245,7 +295,7 @@ export default definePlugin({
if (!sticker) if (!sticker)
break stickerBypass; 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; break stickerBypass;
let link = this.getStickerLink(sticker.id); 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) { for (const emoji of messageObj.validNonShortcutEmojis) {
if (!emoji.require_colons) continue; if (!emoji.require_colons) continue;
if (emoji.guildId === guildId && !emoji.animated) continue; if (emoji.guildId === guildId && !emoji.animated) continue;
@ -284,8 +334,9 @@ export default definePlugin({
return { cancel: false }; return { cancel: false };
}); });
if (!this.canUseEmotes && settings.enableEmojiBypass) { this.preEdit = addPreEditListener((channelId, __, messageObj) => {
this.preEdit = addPreEditListener((_, __, messageObj) => { if (this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId)) return;
const { guildId } = this; const { guildId } = this;
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) { for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
@ -299,7 +350,6 @@ export default definePlugin({
}); });
} }
}); });
}
}, },
stop() { stop() {

View File

@ -30,7 +30,7 @@ export default definePlugin({
find: ".renderOwner=", find: ".renderOwner=",
replacement: { replacement: {
match: /isOwner;return null!=(\w+)?&&/g, match: /isOwner;return null!=(\w+)?&&/g,
replace: "isOwner;if(Vencord.Plugins.plugins.ForceOwnerCrown.isGuildOwner(this.props)){$1=true;}return null!=$1&&" replace: "isOwner;if($self.isGuildOwner(this.props)){$1=true;}return null!=$1&&"
} }
}, },
], ],

View File

@ -146,19 +146,19 @@ export default definePlugin({
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY", find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
replacement: { replacement: {
match: /var .=(?<props>.)\.overlay.+?"aria-label":.\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY.+?}}\)/, match: /var .=(?<props>.)\.overlay.+?"aria-label":.\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY.+?}}\)/,
replace: "$&,Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton($<props>)" replace: "$&,$self.renderToggleGameActivityButton($<props>)"
} }
}, { }, {
find: ".overlayBadge", find: ".overlayBadge",
replacement: { replacement: {
match: /.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/, match: /.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/,
replace: "$&,Vencord.Plugins.plugins.IgnoreActivities.renderToggleActivityButton($<props>)" replace: "$&,$self.renderToggleActivityButton($<props>)"
} }
}, { }, {
find: '.displayName="LocalActivityStore"', find: '.displayName="LocalActivityStore"',
replacement: { replacement: {
match: /(?<activities>.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?\)\)/, match: /(?<activities>.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?\)\)/,
replace: "$&;$<activities>=$<activities>.filter(Vencord.Plugins.plugins.IgnoreActivities.isActivityNotIgnored);" replace: "$&;$<activities>=$<activities>.filter($self.isActivityNotIgnored);"
} }
}], }],

View File

@ -51,7 +51,7 @@ export function DecModal(props: any) {
<Button <Button
color={Button.Colors.GREEN} color={Button.Colors.GREEN}
onClick={() => { onClick={() => {
const toSend = decrypt(secret, password); const toSend = decrypt(secret, password, true);
if (!toSend || !props?.message) return; if (!toSend || !props?.message) return;
// @ts-expect-error // @ts-expect-error
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend); Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);

View File

@ -17,11 +17,13 @@
*/ */
import { addButton, removeButton } from "@api/MessagePopover"; import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getStegCloak } from "@utils/dependencies"; import { getStegCloak } from "@utils/dependencies";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Button, ButtonLooks, ButtonWrapperClasses, ChannelStore, FluxDispatcher, Tooltip } from "@webpack/common"; import { Button, ButtonLooks, ButtonWrapperClasses, ChannelStore, FluxDispatcher, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general";
import { buildDecModal } from "./components/DecryptionModal"; import { buildDecModal } from "./components/DecryptionModal";
import { buildEncModal } from "./components/EncryptionModal"; import { buildEncModal } from "./components/EncryptionModal";
@ -105,6 +107,13 @@ function ChatBarIcon() {
); );
} }
const settings = definePluginSettings({
savedPasswords: {
type: OptionType.STRING,
default: "password, Password",
description: "Saved Passwords (Seperated with a , )"
}
});
export default definePlugin({ export default definePlugin({
name: "InvisibleChat", name: "InvisibleChat",
@ -133,7 +142,7 @@ export default definePlugin({
URL_REGEX: new RegExp( URL_REGEX: new RegExp(
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/, /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/,
), ),
settings,
async start() { async start() {
const { default: StegCloak } = await getStegCloak(); const { default: StegCloak } = await getStegCloak();
steggo = new StegCloak(true, false); steggo = new StegCloak(true, false);
@ -145,7 +154,12 @@ export default definePlugin({
icon: this.popOverIcon, icon: this.popOverIcon,
message: message, message: message,
channel: ChannelStore.getChannel(message.channel_id), channel: ChannelStore.getChannel(message.channel_id),
onClick: () => buildDecModal({ message }) onClick: async () => {
await iteratePasswords(message).then((res: string | false) => {
if (res) return void this.buildEmbed(message, res);
return void buildDecModal({ message });
});
}
} }
: null; : null;
}); });
@ -213,7 +227,31 @@ export function encrypt(secret: string, password: string, cover: string): string
return steggo.hide(secret + "\u200b", password, cover); return steggo.hide(secret + "\u200b", password, cover);
} }
export function decrypt(secret: string, password: string): string { export function decrypt(secret: string, password: string, removeIndicator: boolean): string {
return steggo.reveal(secret, password).replace("\u200b", ""); const decrypted = steggo.reveal(secret, password);
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
} }
export function isCorrectPassword(result: string): boolean {
return result.endsWith("\u200b");
}
export async function iteratePasswords(message: Message): Promise<string | false> {
const passwords = settings.store.savedPasswords.split(",").map(s => s.trim());
if (!message?.content || !passwords?.length) return false;
let { content } = message;
// we use an extra variable so we dont have to edit the message content directly
if (/^\W/.test(message.content)) content = `d ${message.content}d`;
for (let i = 0; i < passwords.length; i++) {
const result = decrypt(content, passwords[i], false);
if (isCorrectPassword(result)) {
return result;
}
}
return false;
}

View File

@ -34,7 +34,7 @@ interface Activity {
state: string; state: string;
details?: string; details?: string;
timestamps?: { timestamps?: {
start?: Number; start?: number;
}; };
assets?: ActivityAssets; assets?: ActivityAssets;
buttons?: Array<string>; buttons?: Array<string>;
@ -43,8 +43,8 @@ interface Activity {
metadata?: { metadata?: {
button_urls?: Array<string>; button_urls?: Array<string>;
}; };
type: Number; type: number;
flags: Number; flags: number;
} }
interface TrackData { interface TrackData {

View File

@ -68,7 +68,7 @@ export default definePlugin({
find: ".LOADING_DID_YOU_KNOW", find: ".LOADING_DID_YOU_KNOW",
replacement: { replacement: {
match: /\._loadingText=.+?random\(.+?;/s, match: /\._loadingText=.+?random\(.+?;/s,
replace: "._loadingText=Vencord.Plugins.plugins.LoadingQuotes.quote;", replace: "._loadingText=$self.quote;",
}, },
}, },
], ],

View File

@ -56,7 +56,7 @@ function MemberCount() {
<div {...props}> <div {...props}>
<span <span
style={{ style={{
backgroundColor: "var(--status-green-600)", backgroundColor: "var(--green-360)",
width: "12px", width: "12px",
height: "12px", height: "12px",
borderRadius: "50%", borderRadius: "50%",
@ -64,7 +64,7 @@ function MemberCount() {
marginRight: "0.5em" marginRight: "0.5em"
}} }}
/> />
<span style={{ color: "var(--status-green-600)" }}>{online}</span> <span style={{ color: "var(--green-360)" }}>{online}</span>
</div> </div>
)} )}
</Tooltip> </Tooltip>
@ -76,13 +76,13 @@ function MemberCount() {
width: "6px", width: "6px",
height: "6px", height: "6px",
borderRadius: "50%", borderRadius: "50%",
border: "3px solid var(--status-grey-500)", border: "3px solid var(--primary-400)",
display: "inline-block", display: "inline-block",
marginRight: "0.5em", marginRight: "0.5em",
marginLeft: "1em" marginLeft: "1em"
}} }}
/> />
<span style={{ color: "var(--status-grey-500)" }}>{total}</span> <span style={{ color: "var(--primary-400)" }}>{total}</span>
</div> </div>
)} )}
</Tooltip> </Tooltip>
@ -99,7 +99,7 @@ export default definePlugin({
find: ".isSidebarVisible,", find: ".isSidebarVisible,",
replacement: { replacement: {
match: /(var (.)=.\.className.+?children):\[(.\.useMemo[^}]+"aria-multiselectable")/, match: /(var (.)=.\.className.+?children):\[(.\.useMemo[^}]+"aria-multiselectable")/,
replace: "$1:[$2.startsWith('members')?Vencord.Plugins.plugins.MemberCount.render():null,$3" replace: "$1:[$2.startsWith('members')?$self.render():null,$3"
} }
}], }],

View File

@ -17,11 +17,13 @@
*/ */
import { addAccessory } from "@api/MessageAccessories"; 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 { Devs } from "@utils/constants.js";
import { classes, LazyComponent } from "@utils/misc";
import { Queue } from "@utils/Queue"; import { Queue } from "@utils/Queue";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { filters, findByPropsLazy, waitFor } from "@webpack"; import { find, findByCode, findByPropsLazy } from "@webpack";
import { import {
Button, Button,
ChannelStore, ChannelStore,
@ -36,41 +38,20 @@ import {
} from "@webpack/common"; } from "@webpack/common";
import { Channel, Guild, Message } from "discord-types/general"; 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>, const Embed = LazyComponent(() => findByCode(".inlineMediaEmbed"));
Embed: React.ComponentType<any>, const ChannelMessage = LazyComponent(() => find(m => m.type?.toString()?.includes('["message","compact","className",')));
ChannelMessage: React.ComponentType<any>,
Endpoints: Record<string, any>;
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 SearchResultClasses = findByPropsLazy("message", "searchResult");
const messageFetchQueue = new Queue(); let AutoModEmbed: React.ComponentType<any> = () => null;
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);
messageCache[messageID] = { fetched: false }; const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
const res = await RestAPI.get({ const tenorRegex = /https:\/\/(?:www.)?tenor\.com/;
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);
}
interface Attachment { interface Attachment {
height: number; height: number;
@ -79,84 +60,15 @@ interface Attachment {
proxyURL?: string; 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 { interface MessageEmbedProps {
message: Message; message: Message;
channel: Channel; channel: Channel;
guildID: string; guildID: string;
} }
export default definePlugin({ const messageFetchQueue = new Queue();
name: "MessageLinkEmbeds",
description: "Adds a preview to messages that link another message", const settings = definePluginSettings({
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: { messageBackgroundColor: {
description: "Background color for messages in rich embeds", description: "Background color for messages in rich embeds",
type: OptionType.BOOLEAN type: OptionType.BOOLEAN
@ -164,53 +76,152 @@ export default definePlugin({
automodEmbeds: { automodEmbeds: {
description: "Use automod embeds instead of rich embeds (smaller but less info)", description: "Use automod embeds instead of rich embeds (smaller but less info)",
type: OptionType.SELECT, type: OptionType.SELECT,
options: [{ options: [
{
label: "Always use automod embeds", label: "Always use automod embeds",
value: "always" value: "always"
}, { },
{
label: "Prefer automod embeds, but use rich embeds if some content can't be shown", label: "Prefer automod embeds, but use rich embeds if some content can't be shown",
value: "prefer" value: "prefer"
}, { },
{
label: "Never use automod embeds", label: "Never use automod embeds",
value: "never", value: "never",
default: true default: true
}] }
]
}, },
clearMessageCache: { clearMessageCache: {
type: OptionType.COMPONENT, type: OptionType.COMPONENT,
description: "Clear the linked message cache", description: "Clear the linked message cache",
component: () => component: () =>
<Button onClick={() => messageCache = {}}> <Button onClick={() => messageCache.clear()}>
Clear the linked message cache Clear the linked message cache
</Button> </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);
start() { const msg = res?.body?.[0];
addAccessory("messageLinkEmbed", props => this.messageEmbedAccessory(props), 4 /* just above rich embeds*/); if (!msg) return;
},
messageLinkRegex: /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(\d{17,19}|@me)\/(\d{17,19})\/(\d{17,19})/g, const message: Message = MessageStore.getMessages(msg.channel_id).receiveMessage(msg).get(msg.id);
messageEmbedAccessory(props) { messageCache.set(message.id, {
const { message }: { message: Message; } = props; 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) {
if (prop === "vencordEmbeddedBy") return embeddedBy;
// @ts-ignore ts so bad
return Reflect.get(...arguments);
}
});
}
function MessageEmbedAccessory({ message }: { message: Message; }) {
// @ts-ignore
const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];
const accessories = [] as (JSX.Element | null)[]; const accessories = [] as (JSX.Element | null)[];
let match = null as RegExpMatchArray | 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; const [_, guildID, channelID, messageID] = match;
if (embeddedBy.includes(messageID)) {
continue;
}
const linkedChannel = ChannelStore.getChannel(channelID); const linkedChannel = ChannelStore.getChannel(channelID);
if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) { if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) {
continue; continue;
} }
let linkedMessage = messageCache[messageID]?.message as Message; let linkedMessage = messageCache.get(messageID)?.message;
if (!linkedMessage) { if (!linkedMessage) {
linkedMessage ??= MessageStore.getMessage(channelID, messageID); linkedMessage ??= MessageStore.getMessage(channelID, messageID);
if (linkedMessage) messageCache[messageID] = { message: linkedMessage, fetched: true }; if (linkedMessage) {
else { messageCache.set(messageID, { message: linkedMessage, fetched: true });
} else {
const msg = { ...message } as any; const msg = { ...message } as any;
delete msg.embeds; delete msg.embeds;
messageFetchQueue.push(() => fetchMessage(channelID, messageID) messageFetchQueue.push(() => fetchMessage(channelID, messageID)
@ -222,30 +233,30 @@ export default definePlugin({
continue; continue;
} }
} }
const messageProps: MessageEmbedProps = { const messageProps: MessageEmbedProps = {
message: linkedMessage, message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
channel: linkedChannel, channel: linkedChannel,
guildID guildID
}; };
const type = Settings.plugins[this.name].automodEmbeds; const type = settings.store.automodEmbeds;
accessories.push( accessories.push(
type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage)) type === "always" || (type === "prefer" && !requiresRichEmbed(linkedMessage))
? this.automodEmbedAccessory(messageProps) ? <AutomodEmbedAccessory {...messageProps} />
: this.channelMessageEmbedAccessory(messageProps) : <ChannelMessageEmbedAccessory {...messageProps} />
); );
} }
return accessories;
},
channelMessageEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { return accessories.length ? <>{accessories}</> : null;
const { message, channel, guildID } = props; }
function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbedProps): JSX.Element | null {
const isDM = guildID === "@me"; const isDM = guildID === "@me";
const guild = !isDM && GuildStore.getGuild(channel.guild_id); const guild = !isDM && GuildStore.getGuild(channel.guild_id);
const dmReceiver = UserStore.getUser(ChannelStore.getChannel(channel.id).recipients?.[0]); 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 return <Embed
embed={{ embed={{
@ -253,62 +264,105 @@ export default definePlugin({
color: "var(--background-secondary)", color: "var(--background-secondary)",
author: { author: {
name: <Text variant="text-xs/medium" tag="span"> name: <Text variant="text-xs/medium" tag="span">
{[
<span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>, <span>{isDM ? "Direct Message - " : (guild as Guild).name + " - "}</span>,
...(isDM {isDM
? Parser.parse(`<@${dmReceiver.id}>`) ? Parser.parse(`<@${dmReceiver.id}>`)
: Parser.parse(`<#${channel.id}>`) : Parser.parse(`<#${channel.id}>`)
) }
]}
</Text>, </Text>,
iconProxyURL: guild iconProxyURL: guild
? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png` ? `https://${window.GLOBAL_ENV.CDN_HOST}/icons/${guild.id}/${guild.icon}.png`
: `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}` : `https://${window.GLOBAL_ENV.CDN_HOST}/avatars/${dmReceiver.id}/${dmReceiver.avatar}`
} }
}} }}
renderDescription={() => { renderDescription={() => (
return <div key={message.id} className={classNames.join(" ")} > <div key={message.id} className={classes(SearchResultClasses.message, settings.store.messageBackgroundColor && SearchResultClasses.searchResult)}>
<ChannelMessage <ChannelMessage
id={`message-link-embeds-${message.id}`} id={`message-link-embeds-${message.id}`}
message={message} message={message}
channel={channel} channel={channel}
subscribeToComponentDispatch={false} subscribeToComponentDispatch={false}
/> />
</div >; </div>
}} )}
/>; />;
}, }
automodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null { function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
const { message, channel, guildID } = props; const { message, channel, guildID } = props;
const isDM = guildID === "@me"; const isDM = guildID === "@me";
const images = getImages(message); const images = getImages(message);
const { parse } = Parser; const { parse } = Parser;
return <AutomodEmbed return <AutoModEmbed
channel={channel} channel={channel}
childrenAccessories={<Text color="text-muted" variant="text-xs/medium" tag="span"> childrenAccessories={
{[ <Text color="text-muted" variant="text-xs/medium" tag="span">
...(isDM ? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`) : parse(`<#${channel.id}>`)), {isDM
? parse(`<@${ChannelStore.getChannel(channel.id).recipients[0]}>`)
: parse(`<#${channel.id}>`)
},
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span> <span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
]} </Text>
</Text>} }
compact={false} compact={false}
content={[ content={
...(message.content || !(message.attachments.length > images.length) <>
{message.content || message.attachments.length <= images.length
? parse(message.content) ? parse(message.content)
: [noContent(message.attachments.length, message.embeds.length)] : [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} hideTimestamp={false}
message={message} message={message}
_messageEmbed="automod" _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 */);
}, },
}); });

View File

@ -0,0 +1,3 @@
.messagelogger-deleted {
background-color: rgba(240 71 71 / 15%);
}

View File

@ -0,0 +1,3 @@
.messagelogger-deleted div {
color: #f04747;
}

View File

@ -19,19 +19,23 @@
import "./messageLogger.css"; import "./messageLogger.css";
import { Settings } from "@api/settings"; import { Settings } from "@api/settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { moment, Parser, Timestamp, UserStore } from "@webpack/common"; import { moment, Parser, Timestamp, UserStore } from "@webpack/common";
function addDeleteStyleClass() { import overlayStyle from "./deleteStyleOverlay.css?managed";
import textStyle from "./deleteStyleText.css?managed";
function addDeleteStyle() {
if (Settings.plugins.MessageLogger.deleteStyle === "text") { if (Settings.plugins.MessageLogger.deleteStyle === "text") {
document.body.classList.remove("messagelogger-red-overlay"); enableStyle(textStyle);
document.body.classList.add("messagelogger-red-text"); disableStyle(overlayStyle);
} else { } else {
document.body.classList.remove("messagelogger-red-text"); disableStyle(textStyle);
document.body.classList.add("messagelogger-red-overlay"); enableStyle(overlayStyle);
} }
} }
@ -41,12 +45,12 @@ export default definePlugin({
authors: [Devs.rushii, Devs.Ven], authors: [Devs.rushii, Devs.Ven],
start() { start() {
addDeleteStyleClass(); addDeleteStyle();
}, },
stop() { stop() {
document.querySelectorAll(".messageLogger-deleted").forEach(e => e.remove()); document.querySelectorAll(".messagelogger-deleted").forEach(e => e.remove());
document.querySelectorAll(".messageLogger-edited").forEach(e => e.remove()); document.querySelectorAll(".messagelogger-edited").forEach(e => e.remove());
document.body.classList.remove("messagelogger-red-overlay"); document.body.classList.remove("messagelogger-red-overlay");
document.body.classList.remove("messagelogger-red-text"); document.body.classList.remove("messagelogger-red-text");
}, },
@ -54,7 +58,7 @@ export default definePlugin({
renderEdit(edit: { timestamp: any, content: string; }) { renderEdit(edit: { timestamp: any, content: string; }) {
return ( return (
<ErrorBoundary noop> <ErrorBoundary noop>
<div className="messageLogger-edited"> <div className="messagelogger-edited">
{Parser.parse(edit.content)} {Parser.parse(edit.content)}
<Timestamp <Timestamp
timestamp={edit.timestamp} timestamp={edit.timestamp}
@ -84,7 +88,7 @@ export default definePlugin({
{ label: "Red text", value: "text", default: true }, { label: "Red text", value: "text", default: true },
{ label: "Red overlay", value: "overlay" } { label: "Red overlay", value: "overlay" }
], ],
onChange: () => addDeleteStyleClass() onChange: () => addDeleteStyle()
}, },
ignoreBots: { ignoreBots: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -147,7 +151,7 @@ export default definePlugin({
replace: replace:
"MESSAGE_DELETE:function($1){" + "MESSAGE_DELETE:function($1){" +
" var cache = $2getOrCreate($1.channelId);" + " var cache = $2getOrCreate($1.channelId);" +
" cache = Vencord.Plugins.plugins.MessageLogger.handleDelete(cache, $1, false);" + " cache = $self.handleDelete(cache, $1, false);" +
" $2commit(cache);" + " $2commit(cache);" +
"}," "},"
}, },
@ -157,7 +161,7 @@ export default definePlugin({
replace: replace:
"MESSAGE_DELETE_BULK:function($1){" + "MESSAGE_DELETE_BULK:function($1){" +
" var cache = $2getOrCreate($1.channelId);" + " var cache = $2getOrCreate($1.channelId);" +
" cache = Vencord.Plugins.plugins.MessageLogger.handleDelete(cache, $1, true);" + " cache = $self.handleDelete(cache, $1, true);" +
" $2commit(cache);" + " $2commit(cache);" +
"}," "},"
}, },
@ -167,7 +171,7 @@ export default definePlugin({
replace: "$1" + replace: "$1" +
".update($3,m =>" + ".update($3,m =>" +
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" + " $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
" m.set('editHistory',[...(m.editHistory || []), Vencord.Plugins.plugins.MessageLogger.makeEdit($2.message, m)]) :" + " m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
" m" + " m" +
")" + ")" +
".update($3" ".update($3"
@ -252,7 +256,7 @@ export default definePlugin({
}, },
{ {
match: /\["className","attachment","inlineMedia".+?className:/, match: /\["className","attachment","inlineMedia".+?className:/,
replace: "$& (deleted ? 'messageLogger-deleted-attachment ' : '') +" replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
} }
] ]
}, },
@ -268,9 +272,9 @@ export default definePlugin({
replace: "var $1=$2.id,deleted=$2.message.deleted," replace: "var $1=$2.id,deleted=$2.message.deleted,"
}, },
{ {
// Append messageLogger-deleted to classNames if deleted // Append messagelogger-deleted to classNames if deleted
match: /\)\("li",\{(.+?),className:/, match: /\)\("li",\{(.+?),className:/,
replace: ")(\"li\",{$1,className:(deleted ? \"messageLogger-deleted \" : \"\")+" replace: ")(\"li\",{$1,className:(deleted ? \"messagelogger-deleted \" : \"\")+"
} }
] ]
}, },
@ -283,7 +287,7 @@ export default definePlugin({
{ {
// Render editHistory in the deepest div for message content // Render editHistory in the deepest div for message content
match: /(\)\("div",\{id:.+?children:\[)/, match: /(\)\("div",\{id:.+?children:\[)/,
replace: "$1 (arguments[0].message.editHistory.length > 0 ? arguments[0].message.editHistory.map(edit => Vencord.Plugins.plugins.MessageLogger.renderEdit(edit)) : null), " replace: "$1 (arguments[0].message.editHistory.length > 0 ? arguments[0].message.editHistory.map(edit => $self.renderEdit(edit)) : null), "
} }
] ]
}, },

View File

@ -1,27 +1,20 @@
.messagelogger-red-overlay .messageLogger-deleted { .messagelogger-deleted [class^="buttons"] {
background-color: rgba(240, 71, 71, 0.15);
}
.messagelogger-red-text .messageLogger-deleted div {
color: #f04747;
}
.messageLogger-deleted [class^="buttons"] {
display: none; display: none;
} }
.messageLogger-deleted-attachment { .messagelogger-deleted-attachment {
filter: grayscale(1); filter: grayscale(1);
} }
.messageLogger-deleted-attachment:hover { .messagelogger-deleted-attachment:hover {
filter: grayscale(0); filter: grayscale(0);
transition: 250ms filter linear; transition: 250ms filter linear;
} }
.theme-dark .messageLogger-edited { .theme-dark .messagelogger-edited {
filter: brightness(80%); filter: brightness(80%);
} }
.theme-light .messageLogger-edited { .theme-light .messagelogger-edited {
opacity: 0.5; opacity: 0.5;
} }

View File

@ -43,7 +43,7 @@ export default definePlugin({
replacement: [ replacement: [
{ {
match: /(?<=MESSAGE_CREATE:function\((\w)\){var \w=\w\.channelId,\w=\w\.message,\w=\w\.isPushNotification,\w=\w\.\w\.getOrCreate\(\w\));/, match: /(?<=MESSAGE_CREATE:function\((\w)\){var \w=\w\.channelId,\w=\w\.message,\w=\w\.isPushNotification,\w=\w\.\w\.getOrCreate\(\w\));/,
replace: ";if(Vencord.Plugins.plugins.NoBlockedMessages.isBlocked(n))return;" replace: ";if($self.isBlocked(n))return;"
} }
] ]
} }

View File

@ -24,7 +24,7 @@ migratePluginSettings("NoDevtoolsWarning", "STFU");
export default definePlugin({ export default definePlugin({
name: "NoDevtoolsWarning", 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], authors: [Devs.Ven],
patches: [{ patches: [{
find: "setDevtoolsCallbacks", find: "setDevtoolsCallbacks",

View File

@ -16,9 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
migratePluginSettings("NoF1", "No F1");
export default definePlugin({ export default definePlugin({
name: "NoF1", name: "NoF1",
description: "Disables F1 help bind.", description: "Disables F1 help bind.",

View File

@ -16,9 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
migratePluginSettings("NoRPC", "No RPC");
export default definePlugin({ export default definePlugin({
name: "NoRPC", name: "NoRPC",
description: "Disables Discord's RPC server.", description: "Disables Discord's RPC server.",

View File

@ -51,7 +51,7 @@ export default definePlugin({
replacement: { replacement: {
match: /CREATE_PENDING_REPLY:function\((.{1,2})\){/, match: /CREATE_PENDING_REPLY:function\((.{1,2})\){/,
replace: replace:
"CREATE_PENDING_REPLY:function($1){$1.shouldMention=Vencord.Plugins.plugins.NoReplyMention.shouldMention($1);", "CREATE_PENDING_REPLY:function($1){$1.shouldMention=$self.shouldMention($1);",
}, },
}, },
], ],

View File

@ -55,7 +55,7 @@ const Icons = {
}; };
type Platform = keyof typeof 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 PlatformIcon = ({ platform, status }: { platform: Platform, status: string; }) => {
const tooltip = platform[0].toUpperCase() + platform.slice(1); const tooltip = platform[0].toUpperCase() + platform.slice(1);

View File

@ -38,7 +38,7 @@ export default definePlugin({
find: "showCommunicationDisabledStyles", find: "showCommunicationDisabledStyles",
replacement: { replacement: {
match: /(?<=return\s*\(0,\w{1,3}\.jsxs?\)\(.+!\w{1,3}&&)(\(0,\w{1,3}.jsxs?\)\(.+?\{.+?\}\))/, match: /(?<=return\s*\(0,\w{1,3}\.jsxs?\)\(.+!\w{1,3}&&)(\(0,\w{1,3}.jsxs?\)\(.+?\{.+?\}\))/,
replace: "[$1, Vencord.Plugins.plugins.PronounDB.PronounsChatComponent(e)]" replace: "[$1, $self.PronounsChatComponent(e)]"
} }
}, },
// Hijack the discord pronouns section (hidden without experiment) and add a wrapper around the text section // Hijack the discord pronouns section (hidden without experiment) and add a wrapper around the text section
@ -46,7 +46,7 @@ export default definePlugin({
find: ".Messages.BOT_PROFILE_SLASH_COMMANDS", find: ".Messages.BOT_PROFILE_SLASH_COMMANDS",
replacement: { replacement: {
match: /\(0,.\.jsx\)\((?<PronounComponent>.{1,2}\..),(?<pronounProps>{currentPronouns.+?:(?<fullProps>.{1,2})\.pronouns.+?})\)/, match: /\(0,.\.jsx\)\((?<PronounComponent>.{1,2}\..),(?<pronounProps>{currentPronouns.+?:(?<fullProps>.{1,2})\.pronouns.+?})\)/,
replace: "$<fullProps>&&Vencord.Plugins.plugins.PronounDB.PronounsProfileWrapper($<PronounComponent>,$<pronounProps>,$<fullProps>)" replace: "$<fullProps>&&$self.PronounsProfileWrapper($<PronounComponent>,$<pronounProps>,$<fullProps>)"
} }
}, },
// Make pronouns experiment be enabled by default // Make pronouns experiment be enabled by default

View File

@ -0,0 +1,58 @@
/*
* 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 { findByPropsLazy } from "@webpack";
const SpoilerClasses = findByPropsLazy("spoilerText");
const MessagesClasses = findByPropsLazy("messagesWrapper", "messages");
export default definePlugin({
name: "RevealAllSpoilers",
description: "Reveal all spoilers in a message by Ctrl-clicking a spoiler, or in the chat with Ctrl+Shift-click",
authors: [Devs.whqwert],
patches: [
{
find: ".removeObscurity=function",
replacement: {
match: /\.removeObscurity=function\((\i)\){/,
replace: ".removeObscurity=function($1){$self.reveal($1);"
}
}
],
reveal(event: MouseEvent) {
const { ctrlKey, shiftKey, target } = event;
if (!ctrlKey) { return; }
const { spoilerText, hidden } = SpoilerClasses;
const { messagesWrapper } = MessagesClasses;
const parent = shiftKey
? document.querySelector(`div.${messagesWrapper}`)
: (target as HTMLSpanElement).parentElement;
for (const spoiler of parent!.querySelectorAll(`span.${spoilerText}.${hidden}`)) {
(spoiler as HTMLSpanElement).click();
}
}
});

View File

@ -43,17 +43,18 @@ export default definePlugin({
} }
}, { }, {
// pass the target to the open link menu so we can check if it's an image // pass the target to the open link menu so we can check if it's an image
find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,", find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",
replacement: { replacement: [
// url1 = url2 = props.attachment.url {
// ... match: /ariaLabel:\i\.Z\.Messages\.MESSAGE_ACTIONS_MENU_LABEL/,
// OpenLinks(url2 != null ? url2 : url1, someStuffs) replace: "$&,_vencordTarget:arguments[0].target"
// },
// the back references are needed because the code is like Z(a!=null?b:c,d), no way to match that {
// otherwise // var f = props.itemHref, .... MakeNativeMenu(null != f ? f : blah)
match: /(?<props>.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/, match: /(\i)=\i\.itemHref,.+?\(null!=\1\?\1:.{1,10}(?=\))/,
replace: "$&,$<props>.target" replace: "$&,arguments[0]._vencordTarget"
} }
]
}], }],
makeMenu(src: string, target: HTMLElement) { makeMenu(src: string, target: HTMLElement) {

View File

@ -32,6 +32,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
fallbackValue: [], fallbackValue: [],
deps: [refetchCount], deps: [refetchCount],
}); });
const username = UserStore.getUser(userId)?.username ?? "";
const dirtyRefetch = () => setRefetchCount(refetchCount + 1); const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
@ -79,7 +80,7 @@ export default function ReviewsView({ userId }: { userId: string; }) {
<textarea <textarea
className={classes(Classes.textarea.replace("textarea", ""), "enter-comment")} className={classes(Classes.textarea.replace("textarea", ""), "enter-comment")}
// this produces something like '-_59yqs ...' but since no class exists with that name its fine // 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} onKeyDown={onKeyPress}
style={{ style={{
marginTop: "6px", marginTop: "6px",

View File

@ -37,7 +37,7 @@ export default definePlugin({
find: "disableBorderColor:!0", find: "disableBorderColor:!0",
replacement: { replacement: {
match: /\(.{0,10}\{user:(.),setNote:.,canDM:.,.+?\}\)/, match: /\(.{0,10}\{user:(.),setNote:.,canDM:.,.+?\}\)/,
replace: "$&,Vencord.Plugins.plugins.ReviewDB.getReviewsComponent($1)" replace: "$&,$self.getReviewsComponent($1)"
}, },
} }
], ],

View File

@ -0,0 +1,67 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 OpenAsar
*
* 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 { Link } from "@components/Link";
import definePlugin from "@utils/types";
import { Forms } from "@webpack/common";
const appIds = [
"911790844204437504",
"886578863147192350",
"1020414178047041627",
"1032800329332445255"
];
export default definePlugin({
name: "richerCider",
description: "Enhances Cider (More details in info button) by adding the \"Listening to\" type prefix to the user's rich presence when an applicable ID is found.",
authors: [{
id: 191621342473224192n,
name: "cryptofyre",
}],
patches: [
{
find: '.displayName="LocalActivityStore"',
replacement: {
match: /LOCAL_ACTIVITY_UPDATE:function\((\i)\)\{/,
replace: "$&$self.patchActivity($1.activity);",
}
}
],
settingsAboutComponent: () => (
<>
<Forms.FormTitle tag="h3">Install Cider to use this Plugin</Forms.FormTitle>
<Forms.FormText>
<Link href="https://cider.sh">Follow the link to our website</Link> to get Cider up and running, and then enable the plugin.
</Forms.FormText>
<br></br>
<Forms.FormTitle tag="h3">What is Cider?</Forms.FormTitle>
<Forms.FormText>
Cider is an open-source and community oriented Apple Music client for Windows, macOS, and Linux.
</Forms.FormText>
<br></br>
<Forms.FormTitle tag="h3">Recommended Optional Plugins</Forms.FormTitle>
<Forms.FormText>
I'd recommend using TimeBarAllActivities alongside this plugin to give off a much better visual to the eye (Keep in mind this only affects your client and will not show for other users)
</Forms.FormText>
</>
),
patchActivity(activity: any) {
if (appIds.includes(activity.application_id)) {
activity.type = 2; /* LISTENING type */
}
},
});

View 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} &mdash; {count}</span>;
},
getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) {
return {
style: {
color: this.getColor(userId, { guildId })
}
};
}
});

View File

@ -1 +1 @@
@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css'); @import url("https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css");

View File

@ -12,7 +12,6 @@
overflow-x: auto; overflow-x: auto;
padding: 0.5em; padding: 0.5em;
position: relative; position: relative;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.125rem; line-height: 1.125rem;
text-indent: 0; text-indent: 0;
@ -57,7 +56,7 @@
.shiki-spinner-container { .shiki-spinner-container {
align-items: center; align-items: center;
background-color: rgba(0, 0, 0, 0.6); background-color: rgb(0 0 0 / 60%);
display: flex; display: flex;
position: absolute; position: absolute;
justify-content: center; justify-content: center;

View File

@ -1,291 +0,0 @@
/*
* 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 { Badge } from "@components/Badge";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import { ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { proxyLazy } from "@utils/proxyLazy";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findLazy } from "@webpack";
import { Button, ChannelStore, moment, Parser, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common";
import { Channel } from "discord-types/general";
const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer");
const Permissions = findLazy(m => typeof m.VIEW_CHANNEL === "bigint");
const ChannelTypes = findByPropsLazy("GUILD_TEXT", "GUILD_FORUM");
const ChannelTypesToChannelName = proxyLazy(() => ({
[ChannelTypes.GUILD_TEXT]: "TEXT",
[ChannelTypes.GUILD_ANNOUNCEMENT]: "ANNOUNCEMENT",
[ChannelTypes.GUILD_FORUM]: "FORUM"
}));
enum ShowMode {
LockIcon,
HiddenIconWithMutedStyle
}
const settings = definePluginSettings({
hideUnreads: {
description: "Hide Unreads",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
},
showMode: {
description: "The mode used to display hidden channels.",
type: OptionType.SELECT,
options: [
{ label: "Plain style with Lock Icon instead", value: ShowMode.LockIcon, default: true },
{ label: "Muted style with hidden eye icon on the right", value: ShowMode.HiddenIconWithMutedStyle },
],
restartNeeded: true
}
});
export default definePlugin({
name: "ShowHiddenChannels",
description: "Show channels that you do not have access to view.",
authors: [Devs.BigDuck, Devs.AverageReactEnjoyer, Devs.D3SOX, Devs.Ven, Devs.Nuckyz, Devs.Nickyux, Devs.dzshn],
settings,
patches: [
{
// RenderLevel defines if a channel is hidden, collapsed in category, visible, etc
find: ".CannotShow",
// These replacements only change the necessary CannotShow's
replacement: [
{
match: /(?<=isChannelGatedAndVisible\(this\.record\.guild_id,this\.record\.id\).+?renderLevel:)(?<RenderLevels>\i)\..+?(?=,)/,
replace: "this.category.isCollapsed?$<RenderLevels>.WouldShowIfUncollapsed:$<RenderLevels>.Show"
},
// Move isChannelGatedAndVisible renderLevel logic to the bottom to not show hidden channels in case they are muted
{
match: /(?<=(?<permissionCheck>if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{)if\(this\.id===\i\).+?};)(?<isChannelGatedAndVisibleCondition>if\(!\i\.\i\.isChannelGatedAndVisible\(.+?})(?<restOfFunction>.+?)(?=return{renderLevel:\i\.Show.{1,40}return \i)/,
replace: "$<restOfFunction>$<permissionCheck>$<isChannelGatedAndVisibleCondition>}"
},
{
match: /(?<=renderLevel:(?<renderLevelExpression>\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/,
replace: "$<renderLevelExpression>"
},
{
match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(?<RenderLevels>\i)\..+?(?=,)/,
replace: "$<RenderLevels>.Show"
},
{
match: /(?<=getRenderLevel=function.+?return ).+?\?(?<renderLevelExpressionWithoutPermCheck>.+?):\i\.CannotShow(?=})/,
replace: "$<renderLevelExpressionWithoutPermCheck>"
}
]
},
{
// inside the onMouseDown handler, we check if the channel is hidden and open the modal if it is
find: "VoiceChannel.renderPopout: There must always be something to render",
replacement: [
{
match: /(?=(?<this>\i)\.handleThreadsPopoutClose\(\))/,
replace: "if($self.isHiddenChannel($<this>.props.channel)&&arguments[0].button===0){"
+ "$self.onHiddenChannelSelected($<this>.props.channel);"
+ "return;"
+ "}"
},
// Do nothing when trying to join a voice channel if the channel is hidden
{
match: /(?<=handleClick=function\(\){)(?=.{1,80}(?<this>\i)\.handleVoiceConnect\(\))/,
replace: "if($self.isHiddenChannel($<this>.props.channel))return;"
},
// Render null instead of the buttons if the channel is hidden
...[
"renderEditButton",
"renderInviteButton",
"renderOpenChatButton"
].map(func => ({
match: new RegExp(`(?<=\\i\\.${func}=function\\(\\){)`, "g"), // Global because Discord has multiple declarations of the same functions
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
}))
]
},
{
find: ".Messages.CHANNEL_TOOLTIP_DIRECTORY",
predicate: () => settings.store.showMode === ShowMode.LockIcon,
replacement: {
// Lock Icon
match: /(?=switch\((?<channel>\i)\.type\).{1,30}\.GUILD_ANNOUNCEMENT.{1,30}\(0,\i\.\i\))/,
replace: "if($self.isHiddenChannel($<channel>))return $self.LockIcon;"
}
},
{
find: ".UNREAD_HIGHLIGHT",
predicate: () => settings.store.hideUnreads === true,
replacement: [{
// Hide unreads
match: /(?<=\i\.connected,\i=)(?=(?<props>\i)\.unread)/,
replace: "$self.isHiddenChannel($<props>.channel)?false:"
}]
},
{
find: ".UNREAD_HIGHLIGHT",
predicate: () => settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
replacement: [
// Make the channel appear as muted if it's hidden
{
match: /(?<=\i\.name,\i=)(?=(?<props>\i)\.muted)/,
replace: "$self.isHiddenChannel($<props>.channel)?true:"
},
// Add the hidden eye icon if the channel is hidden
{
match: /(?<=(?<channel>\i)=\i\.channel,.+?\(\)\.children.+?:null)/,
replace: ",$self.isHiddenChannel($<channel>)?$self.HiddenChannelIcon():null"
},
// Make voice channels also appear as muted if they are muted
{
match: /(?<=\i\(\)\.wrapper:\i\(\)\.notInteractive,)(?<otherClasses>.+?)(?<mutedClassExpression>(?<isMuted>\i)\?\i\.MUTED)/,
replace: "$<mutedClassExpression>:\"\",$<otherClasses>$<isMuted>?\"\""
}
]
},
// Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden
{
find: ".UNREAD_HIGHLIGHT",
predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
replacement: {
match: /(?<=(?<channel>\i)=\i\.channel,.+?\.LOCKED:\i)/,
replace: "&&!($self.settings.store.hideUnreads===false&&$self.isHiddenChannel($<channel>))"
}
},
{
// Hide New unreads box for hidden channels
find: '.displayName="ChannelListUnreadsStore"',
replacement: {
match: /(?<=return null!=(?<channel>\i))(?=.{1,130}hasRelevantUnread\(\i\))/,
replace: "&&!$self.isHiddenChannel($<channel>)"
}
},
// Patch keybind handlers so you can't accidentally jump to hidden channels
{
find: '"alt+shift+down"',
replacement: {
match: /(?<=getChannel\(\i\);return null!=(?<channel>\i))(?=.{1,130}hasRelevantUnread\(\i\))/,
replace: "&&!$self.isHiddenChannel($<channel>)"
}
},
{
find: '"alt+down"',
replacement: {
match: /(?<=getState\(\)\.channelId.{1,30}\(0,\i\.\i\)\(\i\))(?=\.map\()/,
replace: ".filter(ch=>!$self.isHiddenChannel(ch))"
}
},
],
isHiddenChannel(channel: Channel & { channelId?: string; }) {
if (!channel) return false;
if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;
return !PermissionStore.can(Permissions.VIEW_CHANNEL, channel);
},
onHiddenChannelSelected(channel: Channel) {
// Check for type, otherwise it would attempt to show the modal for stage channels
if ([ChannelTypes.GUILD_TEXT, ChannelTypes.GUILD_ANNOUNCEMENT, ChannelTypes.GUILD_FORUM].includes(channel.type)) {
openModal(modalProps => (
<ModalRoot size={ModalSize.SMALL} {...modalProps}>
<ModalHeader>
<Flex>
<Text variant="heading-md/bold">#{channel.name}</Text>
{<Badge text={ChannelTypesToChannelName[channel.type]} color="var(--brand-experiment)" />}
{channel.isNSFW() && <Badge text="NSFW" color="var(--status-danger)" />}
</Flex>
</ModalHeader>
<ModalContent style={{ margin: "10px 8px" }}>
<Text variant="text-md/normal">You don't have permission to view {channel.type === ChannelTypes.GUILD_FORUM ? "posts" : "messages"} in this channel.</Text>
{(channel.topic ?? "").length > 0 && (
<>
<Text variant="text-md/bold" style={{ marginTop: 10 }}>
{channel.type === ChannelTypes.GUILD_FORUM ? "Guidelines:" : "Topic:"}
</Text>
<div style={{ color: "var(--text-normal)", marginTop: 10 }}>
{Parser.parseTopic(channel.topic, false, { channelId: channel.id })}
</div>
</>
)}
{channel.lastMessageId && (
<>
<Text variant="text-md/bold" style={{ marginTop: 10 }}>
{channel.type === ChannelTypes.GUILD_FORUM ? "Last Post Created" : "Last Message Sent:"}
</Text>
<div style={{ color: "var(--text-normal)", marginTop: 10 }}>
<Timestamp timestamp={moment(SnowflakeUtils.extractTimestamp(channel.lastMessageId))} />
</div>
</>
)}
</ModalContent>
<ModalFooter>
<Flex>
<Button
onClick={modalProps.onClose}
size={Button.Sizes.SMALL}
color={Button.Colors.PRIMARY}
>
Close
</Button>
</Flex>
</ModalFooter>
</ModalRoot>
));
}
},
LockIcon: () => (
<svg
className={ChannelListClasses.icon}
height="18"
width="20"
viewBox="0 0 24 24"
aria-hidden={true}
role="img"
>
<path fillRule="evenodd" fill="currentColor" d="M17 11V7C17 4.243 14.756 2 12 2C9.242 2 7 4.243 7 7V11C5.897 11 5 11.896 5 13V20C5 21.103 5.897 22 7 22H17C18.103 22 19 21.103 19 20V13C19 11.896 18.103 11 17 11ZM12 18C11.172 18 10.5 17.328 10.5 16.5C10.5 15.672 11.172 15 12 15C12.828 15 13.5 15.672 13.5 16.5C13.5 17.328 12.828 18 12 18ZM15 11H9V7C9 5.346 10.346 4 12 4C13.654 4 15 5.346 15 7V11Z" />
</svg>
),
HiddenChannelIcon: () => (
<Tooltip text="Hidden Channel">
{({ onMouseLeave, onMouseEnter }) => (
<svg
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
className={ChannelListClasses.icon}
width="24"
height="24"
viewBox="0 0 24 24"
aria-hidden={true}
role="img"
style={{ marginLeft: 6, zIndex: 0, cursor: "not-allowed" }}
>
<path fillRule="evenodd" fill="currentColor" d="m19.8 22.6-4.2-4.15q-.875.275-1.762.413Q12.95 19 12 19q-3.775 0-6.725-2.087Q2.325 14.825 1 11.5q.525-1.325 1.325-2.463Q3.125 7.9 4.15 7L1.4 4.2l1.4-1.4 18.4 18.4ZM12 16q.275 0 .512-.025.238-.025.513-.1l-5.4-5.4q-.075.275-.1.513-.025.237-.025.512 0 1.875 1.312 3.188Q10.125 16 12 16Zm7.3.45-3.175-3.15q.175-.425.275-.862.1-.438.1-.938 0-1.875-1.312-3.188Q13.875 7 12 7q-.5 0-.938.1-.437.1-.862.3L7.65 4.85q1.025-.425 2.1-.638Q10.825 4 12 4q3.775 0 6.725 2.087Q21.675 8.175 23 11.5q-.575 1.475-1.512 2.738Q20.55 15.5 19.3 16.45Zm-4.625-4.6-3-3q.7-.125 1.288.112.587.238 1.012.688.425.45.613 1.038.187.587.087 1.162Z" />
</svg>
)}
</Tooltip>
)
});

View File

@ -0,0 +1,263 @@
/*
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { LazyComponent } from "@utils/misc";
import { formatDuration } from "@utils/text";
import { find, findByCode, findByPropsLazy } from "@webpack";
import { FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common";
import { Channel } from "discord-types/general";
enum SortOrderTypes {
LATEST_ACTIVITY = 0,
CREATION_DATE = 1
}
enum ForumLayoutTypes {
DEFAULT = 0,
LIST = 1,
GRID = 2
}
interface DefaultReaction {
emojiId: string | null;
emojiName: string | null;
}
interface Tag {
id: string;
name: string;
emojiId: string | null;
emojiName: string | null;
moderated: boolean;
}
interface ExtendedChannel extends Channel {
defaultThreadRateLimitPerUser?: number;
defaultSortOrder?: SortOrderTypes | null;
defaultForumLayout?: ForumLayoutTypes;
defaultReactionEmoji?: DefaultReaction | null;
availableTags?: Array<Tag>;
}
enum ChannelTypes {
GUILD_TEXT = 0,
GUILD_VOICE = 2,
GUILD_ANNOUNCEMENT = 5,
GUILD_STAGE_VOICE = 13,
GUILD_FORUM = 15
}
enum VideoQualityModes {
AUTO = 1,
FULL = 2
}
enum ChannelFlags {
PINNED = 1 << 1,
REQUIRE_TAG = 1 << 4
}
const ChatScrollClasses = findByPropsLazy("auto", "content", "scrollerBase");
const TagComponent = LazyComponent(() => find(m => {
if (typeof m !== "function") return false;
const code = Function.prototype.toString.call(m);
// Get the component which doesn't include increasedActivity logic
return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill");
}));
const EmojiComponent = LazyComponent(() => findByCode('.jumboable?"jumbo":"default"'));
// The component for the beggining of a channel, but we patched it so it only returns the allowed users and roles components for hidden channels
const ChannelBeginHeader = LazyComponent(() => findByCode(".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE"));
const ChannelTypesToChannelNames = {
[ChannelTypes.GUILD_TEXT]: "text",
[ChannelTypes.GUILD_ANNOUNCEMENT]: "announcement",
[ChannelTypes.GUILD_FORUM]: "forum",
[ChannelTypes.GUILD_VOICE]: "voice",
[ChannelTypes.GUILD_STAGE_VOICE]: "stage"
};
const SortOrderTypesToNames = {
[SortOrderTypes.LATEST_ACTIVITY]: "Latest activity",
[SortOrderTypes.CREATION_DATE]: "Creation date"
};
const ForumLayoutTypesToNames = {
[ForumLayoutTypes.DEFAULT]: "Not set",
[ForumLayoutTypes.LIST]: "List view",
[ForumLayoutTypes.GRID]: "Gallery view"
};
const VideoQualityModesToNames = {
[VideoQualityModes.AUTO]: "Automatic",
[VideoQualityModes.FULL]: "720p"
};
// Icon from the modal when clicking a message link you don't have access to view
const HiddenChannelLogo = "/assets/433e3ec4319a9d11b0cbe39342614982.svg";
function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
const {
type,
topic,
lastMessageId,
defaultForumLayout,
lastPinTimestamp,
defaultAutoArchiveDuration,
availableTags,
id: channelId,
rateLimitPerUser,
defaultThreadRateLimitPerUser,
defaultSortOrder,
defaultReactionEmoji,
bitrate,
rtcRegion,
videoQualityMode,
permissionOverwrites
} = channel;
const membersToFetch: Array<string> = [];
const guildOwnerId = GuildStore.getGuild(channel.guild_id).ownerId;
if (!GuildMemberStore.getMember(channel.guild_id, guildOwnerId)) membersToFetch.push(guildOwnerId);
Object.values(permissionOverwrites).forEach(({ type, id: userId }) => {
if (type === 1) {
if (!GuildMemberStore.getMember(channel.guild_id, userId)) membersToFetch.push(userId);
}
});
if (membersToFetch.length > 0) {
FluxDispatcher.dispatch({
type: "GUILD_MEMBERS_REQUEST",
guildIds: [channel.guild_id],
userIds: membersToFetch
});
}
return (
<div className={ChatScrollClasses.auto + " " + "shc-lock-screen-outer-container"}>
<div className="shc-lock-screen-container">
<img className="shc-lock-screen-logo" src={HiddenChannelLogo} />
<div className="shc-lock-screen-heading-container">
<Text variant="heading-xxl/bold">This is a hidden {ChannelTypesToChannelNames[type]} channel.</Text>
{channel.isNSFW() &&
<Tooltip text="NSFW">
{({ onMouseLeave, onMouseEnter }) => (
<svg
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
className="shc-lock-screen-heading-nsfw-icon"
width="32"
height="32"
viewBox="0 0 48 48"
aria-hidden={true}
role="img"
>
<path d="M.7 43.05 24 2.85l23.3 40.2Zm23.55-6.25q.75 0 1.275-.525.525-.525.525-1.275 0-.75-.525-1.3t-1.275-.55q-.8 0-1.325.55-.525.55-.525 1.3t.55 1.275q.55.525 1.3.525Zm-1.85-6.1h3.65V19.4H22.4Z" />
</svg>
)}
</Tooltip>
}
</div>
{(!channel.isGuildVoice() && !channel.isGuildStageVoice()) && (
<Text variant="text-lg/normal">
You can not see the {channel.isForumChannel() ? "posts" : "messages"} of this channel.
{channel.isForumChannel() && topic && topic.length > 0 && "However you may see its guidelines:"}
</Text >
)}
{channel.isForumChannel() && topic && topic.length > 0 && (
<div className="shc-lock-screen-topic-container">
{Parser.parseTopic(topic, false, { channelId })}
</div>
)}
{lastMessageId &&
<Text variant="text-md/normal">
Last {channel.isForumChannel() ? "post" : "message"} created:
<Timestamp timestamp={moment(SnowflakeUtils.extractTimestamp(lastMessageId))} />
</Text>
}
{lastPinTimestamp &&
<Text variant="text-md/normal">Last message pin: <Timestamp timestamp={moment(lastPinTimestamp)} /></Text>
}
{(rateLimitPerUser ?? 0) > 0 &&
<Text variant="text-md/normal">Slowmode: {formatDuration(rateLimitPerUser!, "seconds")}</Text>
}
{(defaultThreadRateLimitPerUser ?? 0) > 0 &&
<Text variant="text-md/normal">
Default thread slowmode: {formatDuration(defaultThreadRateLimitPerUser!, "seconds")}
</Text>
}
{((channel.isGuildVoice() || channel.isGuildStageVoice()) && bitrate != null) &&
<Text variant="text-md/normal">Bitrate: {bitrate} bits</Text>
}
{rtcRegion !== undefined &&
<Text variant="text-md/normal">Region: {rtcRegion ?? "Automatic"}</Text>
}
{(channel.isGuildVoice() || channel.isGuildStageVoice()) &&
<Text variant="text-md/normal">Video quality mode: {VideoQualityModesToNames[videoQualityMode ?? VideoQualityModes.AUTO]}</Text>
}
{(defaultAutoArchiveDuration ?? 0) > 0 &&
<Text variant="text-md/normal">
Default inactivity duration before archiving {channel.isForumChannel() ? "posts" : "threads"}:
{" " + formatDuration(defaultAutoArchiveDuration!, "minutes")}
</Text>
}
{defaultForumLayout != null &&
<Text variant="text-md/normal">Default layout: {ForumLayoutTypesToNames[defaultForumLayout]}</Text>
}
{defaultSortOrder != null &&
<Text variant="text-md/normal">Default sort order: {SortOrderTypesToNames[defaultSortOrder]}</Text>
}
{defaultReactionEmoji != null &&
<div className="shc-lock-screen-default-emoji-container">
<Text variant="text-md/normal">Default reaction emoji:</Text>
<EmojiComponent node={{
type: defaultReactionEmoji.emojiName ? "emoji" : "customEmoji",
name: defaultReactionEmoji.emojiName ?? "",
emojiId: defaultReactionEmoji.emojiId
}} />
</div>
}
{channel.hasFlag(ChannelFlags.REQUIRE_TAG) &&
<Text variant="text-md/normal">Posts on this forum require a tag to be set.</Text>
}
{availableTags && availableTags.length > 0 &&
<div className="shc-lock-screen-tags-container">
<Text variant="text-lg/bold">Available tags:</Text>
<div className="shc-lock-screen-tags">
{availableTags.map(tag => <TagComponent tag={tag} />)}
</div>
</div>
}
<div className="shc-lock-screen-allowed-users-and-roles-container">
<Text variant="text-lg/bold">Allowed users and roles:</Text>
<ChannelBeginHeader channel={channel} />
</div>
</div>
</div>
);
}
export default ErrorBoundary.wrap(HiddenChannelLockScreen);

View File

@ -0,0 +1,370 @@
/*
* 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 "./style.css";
import { definePluginSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common";
import { Channel } from "discord-types/general";
import HiddenChannelLockScreen from "./components/HiddenChannelLockScreen";
const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer");
const VIEW_CHANNEL = 1n << 10n;
enum ShowMode {
LockIcon,
HiddenIconWithMutedStyle
}
const settings = definePluginSettings({
hideUnreads: {
description: "Hide Unreads",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
},
showMode: {
description: "The mode used to display hidden channels.",
type: OptionType.SELECT,
options: [
{ label: "Plain style with Lock Icon instead", value: ShowMode.LockIcon, default: true },
{ label: "Muted style with hidden eye icon on the right", value: ShowMode.HiddenIconWithMutedStyle },
],
restartNeeded: true
}
});
export default definePlugin({
name: "ShowHiddenChannels",
description: "Show channels that you do not have access to view.",
authors: [Devs.BigDuck, Devs.AverageReactEnjoyer, Devs.D3SOX, Devs.Ven, Devs.Nuckyz, Devs.Nickyux, Devs.dzshn],
settings,
patches: [
{
// RenderLevel defines if a channel is hidden, collapsed in category, visible, etc
find: ".CannotShow",
// These replacements only change the necessary CannotShow's
replacement: [
{
match: /(?<=isChannelGatedAndVisible\(this\.record\.guild_id,this\.record\.id\).+?renderLevel:)(?<RenderLevels>\i)\..+?(?=,)/,
replace: "this.category.isCollapsed?$<RenderLevels>.WouldShowIfUncollapsed:$<RenderLevels>.Show"
},
// Move isChannelGatedAndVisible renderLevel logic to the bottom to not show hidden channels in case they are muted
{
match: /(?<=(?<permissionCheck>if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{)if\(this\.id===\i\).+?};)(?<isChannelGatedAndVisibleCondition>if\(!\i\.\i\.isChannelGatedAndVisible\(.+?})(?<restOfFunction>.+?)(?=return{renderLevel:\i\.Show.{1,40}return \i)/,
replace: "$<restOfFunction>$<permissionCheck>$<isChannelGatedAndVisibleCondition>}"
},
{
match: /(?<=renderLevel:(?<renderLevelExpression>\i\(this,\i\)\?\i\.Show:\i\.WouldShowIfUncollapsed).+?renderLevel:).+?(?=,)/,
replace: "$<renderLevelExpression>"
},
{
match: /(?<=activeJoinedRelevantThreads.+?renderLevel:.+?,threadIds:\i\(this.record.+?renderLevel:)(?<RenderLevels>\i)\..+?(?=,)/,
replace: "$<RenderLevels>.Show"
},
{
match: /(?<=getRenderLevel=function.+?return ).+?\?(?<renderLevelExpressionWithoutPermCheck>.+?):\i\.CannotShow(?=})/,
replace: "$<renderLevelExpressionWithoutPermCheck>"
}
]
},
{
find: "VoiceChannel, transitionTo: Channel does not have a guildId",
replacement: [
{
// Do not show confirmation to join a voice channel when already connected to another if clicking on a hidden voice channel
match: /(?<=getCurrentClientVoiceChannelId\(\i\.guild_id\);if\()(?=.+?\((?<channel>\i)\))/,
replace: "!$self.isHiddenChannel($<channel>)&&"
},
{
// Make Discord think we are connected to a voice channel so it shows us inside it
match: /(?=\|\|\i\.default\.selectVoiceChannel\((?<channel>\i)\.id\))/,
replace: "||$self.isHiddenChannel($<channel>)"
},
{
// Make Discord think we are connected to a voice channel so it shows us inside it
match: /(?<=\|\|\i\.default\.selectVoiceChannel\((?<channel>\i)\.id\);!__OVERLAY__&&\()/,
replace: "$self.isHiddenChannel($<channel>)||"
}
]
},
{
find: "VoiceChannel.renderPopout: There must always be something to render",
replacement: [
// Render null instead of the buttons if the channel is hidden
...[
"renderEditButton",
"renderInviteButton",
"renderOpenChatButton"
].map(func => ({
match: new RegExp(`(?<=\\i\\.${func}=function\\(\\){)`, "g"), // Global because Discord has multiple declarations of the same functions
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
}))
]
},
{
find: ".Messages.CHANNEL_TOOLTIP_DIRECTORY",
predicate: () => settings.store.showMode === ShowMode.LockIcon,
replacement: {
// Lock Icon
match: /(?=switch\((?<channel>\i)\.type\).{1,30}\.GUILD_ANNOUNCEMENT.{1,30}\(0,\i\.\i\))/,
replace: "if($self.isHiddenChannel($<channel>))return $self.LockIcon;"
}
},
{
find: ".UNREAD_HIGHLIGHT",
predicate: () => settings.store.hideUnreads === true,
replacement: {
// Hide unreads
match: /(?<=\i\.connected,\i=)(?=(?<props>\i)\.unread)/,
replace: "$self.isHiddenChannel($<props>.channel)?false:"
}
},
{
find: ".UNREAD_HIGHLIGHT",
predicate: () => settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
replacement: [
// Make the channel appear as muted if it's hidden
{
match: /(?<=\i\.name,\i=)(?=(?<props>\i)\.muted)/,
replace: "$self.isHiddenChannel($<props>.channel)?true:"
},
// Add the hidden eye icon if the channel is hidden
{
match: /(?<=(?<channel>\i)=\i\.channel,.+?\(\)\.children.+?:null)/,
replace: ",$self.isHiddenChannel($<channel>)?$self.HiddenChannelIcon():null"
},
// Make voice channels also appear as muted if they are muted
{
match: /(?<=\i\(\)\.wrapper:\i\(\)\.notInteractive,)(?<otherClasses>.+?)(?<mutedClassExpression>(?<isMuted>\i)\?\i\.MUTED)/,
replace: "$<mutedClassExpression>:\"\",$<otherClasses>$<isMuted>?\"\""
}
]
},
// Make muted channels also appear as unread if hide unreads is false, using the HiddenIconWithMutedStyle and the channel is hidden
{
find: ".UNREAD_HIGHLIGHT",
predicate: () => settings.store.hideUnreads === false && settings.store.showMode === ShowMode.HiddenIconWithMutedStyle,
replacement: {
match: /(?<=(?<channel>\i)=\i\.channel,.+?\.LOCKED:\i)/,
replace: "&&!($self.settings.store.hideUnreads===false&&$self.isHiddenChannel($<channel>))"
}
},
{
// Hide New unreads box for hidden channels
find: '.displayName="ChannelListUnreadsStore"',
replacement: {
match: /(?<=return null!=(?<channel>\i))(?=.{1,130}hasRelevantUnread\(\i\))/g, // Global because Discord has multiple methods like that in the same module
replace: "&&!$self.isHiddenChannel($<channel>)"
}
},
// Only render the channel header and buttons that work when transitioning to a hidden channel
{
find: "Missing channel in Channel.renderHeaderToolbar",
replacement: [
{
match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_TEXT:)(?=.+?;(?<pushNotificationButtonExpression>.+?{channel:(?<channel>\i)},"notifications"\)\);))/,
replace: "if($self.isHiddenChannel($<channel>)){$<pushNotificationButtonExpression>break;}"
},
{
match: /(?<=renderHeaderToolbar=function.+?case \i\.\i\.GUILD_FORUM:if\(!\i\){)(?=.+?;(?<pushNotificationButtonExpression>.+?{channel:(?<channel>\i)},"notifications"\)\)))/,
replace: "if($self.isHiddenChannel($<channel>)){$<pushNotificationButtonExpression>;break;}"
},
{
match: /(?<=(?<this>\i)\.renderMobileToolbar=function.+?case \i\.\i\.GUILD_FORUM:)/,
replace: "if($self.isHiddenChannel($<this>.props.channel))break;"
},
{
match: /(?<=renderHeaderBar=function.+?hideSearch:(?<channel>\i)\.isDirectory\(\))/,
replace: "||$self.isHiddenChannel($<channel>)"
},
{
match: /(?<=renderSidebar=function\(\){)/,
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
},
{
match: /(?<=renderChat=function\(\){)/,
replace: "if($self.isHiddenChannel(this.props.channel))return $self.HiddenChannelLockScreen(this.props.channel);"
}
]
},
// Avoid trying to fetch messages from hidden channels
{
find: '"MessageManager"',
replacement: [
{
match: /(?<=if\(null!=(?<channelId>\i)\).{1,100}"Skipping fetch because channelId is a static route".{1,10}else{)/,
replace: "if($self.isHiddenChannel({channelId:$<channelId>}))return;"
},
]
},
// Patch keybind handlers so you can't accidentally jump to hidden channels
{
find: '"alt+shift+down"',
replacement: {
match: /(?<=getChannel\(\i\);return null!=(?<channel>\i))(?=.{1,130}hasRelevantUnread\(\i\))/,
replace: "&&!$self.isHiddenChannel($<channel>)"
}
},
{
find: '"alt+down"',
replacement: {
match: /(?<=getState\(\)\.channelId.{1,30}\(0,\i\.\i\)\(\i\))(?=\.map\()/,
replace: ".filter(ch=>!$self.isHiddenChannel(ch))"
}
},
// Export the emoji component used on the lock screen
{
find: 'jumboable?"jumbo":"default"',
replacement: {
match: /(?<=\i:\(\)=>\i)(?=}.+?(?<component>\i)=function.{1,20}node,\i=\i.isInteracting)/,
replace: ",hc1:()=>$<component>" // Blame Ven length check for the small name :pensive_cry:
}
},
{
find: ".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE",
replacement: [
{
// Export the channel beggining header
match: /(?<=\i:\(\)=>\i)(?=}.+?function (?<component>\i).{1,600}computePermissionsForRoles)/,
replace: ",hc2:()=>$<component>"
},
{
// Patch the header to only return allowed users and roles if it's a hidden channel (Like when it's used on the HiddenChannelLockScreen)
match: /(?<=MANAGE_ROLES.{1,60}return)(?=\(.+?(?<component>\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(?<channel>\i)\.guild_id.+?roleColor.+?]}\)))/,
replace: " $self.isHiddenChannel($<channel>)?$<component>:"
}
]
},
{
find: ".Messages.SHOW_CHAT",
replacement: [
{
// Remove the divider and the open chat button for the HiddenChannelLockScreen
match: /(?<=function \i\((?<props>\i)\).{1,2000}"more-options-popout"\)\);if\()/,
replace: "(!$self.isHiddenChannel($<props>.channel)||$<props>.inCall)&&"
},
{
// Render our HiddenChannelLockScreen component instead of the main voice channel component
match: /(?<=renderContent=function.{1,1700}children:)/,
replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?$self.HiddenChannelLockScreen(this.props.channel):"
},
{
// Disable gradients for the HiddenChannelLockScreen of voice channels
match: /(?<=renderContent=function.{1,1600}disableGradients:)/,
replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)||"
},
{
// Disable useless components for the HiddenChannelLockScreen of voice channels
match: /(?<=renderContent=function.{1,800}render(?!Header).{0,30}:)(?!void)/g,
replace: "!this.props.inCall&&$self.isHiddenChannel(this.props.channel)?null:"
}
]
},
{
find: "Guild voice channel without guild id.",
replacement: [
{
// Render our HiddenChannelLockScreen component instead of the main stage channel component
match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,1400}children:)(?=.{1,20}}\)}function)/,
replace: "$self.isHiddenChannel($<channel>)?$self.HiddenChannelLockScreen($<channel>):"
},
{
// Disable useless components for the HiddenChannelLockScreen of stage channels
match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,1000}render(?!Header).{0,30}:)/g,
replace: "$self.isHiddenChannel($<channel>)?null:"
},
// Prevent Discord from replacing our route if we aren't connected to the stage channel
{
match: /(?<=if\()(?=!\i&&!\i&&!\i.{1,80}(?<channel>\i)\.getGuildId\(\).{1,50}Guild voice channel without guild id\.)/,
replace: "!$self.isHiddenChannel($<channel>)&&"
},
{
// Disable gradients for the HiddenChannelLockScreen of stage channels
match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,600}disableGradients:)/,
replace: "$self.isHiddenChannel($<channel>)||"
},
{
// Disable strange styles applied to the header for the HiddenChannelLockScreen of stage channels
match: /(?<=(?<channel>\i)\.getGuildId\(\).{1,30}Guild voice channel without guild id\..{1,600}style:)/,
replace: "$self.isHiddenChannel($<channel>)?undefined:"
},
{
// Remove the divider and amount of users in stage channel components for the HiddenChannelLockScreen
match: /\(0,\i\.jsx\)\(\i\.\i\.Divider.+?}\)]}\)(?=.+?:(?<channel>\i)\.guild_id)/,
replace: "$self.isHiddenChannel($<channel>)?null:($&)"
},
{
// Remove the open chat button for the HiddenChannelLockScreen
match: /(?<=null,)(?=.{1,120}channelId:(?<channel>\i)\.id,.+?toggleRequestToSpeakSidebar:\i,iconClassName:\i\(\)\.buttonIcon)/,
replace: "!$self.isHiddenChannel($<channel>)&&"
}
],
}
],
isHiddenChannel(channel: Channel & { channelId?: string; }) {
if (!channel) return false;
if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;
return !PermissionStore.can(VIEW_CHANNEL, channel);
},
HiddenChannelLockScreen: (channel: any) => <HiddenChannelLockScreen channel={channel} />,
LockIcon: () => (
<svg
className={ChannelListClasses.icon}
height="18"
width="20"
viewBox="0 0 24 24"
aria-hidden={true}
role="img"
>
<path className="shc-evenodd-fill-current-color" d="M17 11V7C17 4.243 14.756 2 12 2C9.242 2 7 4.243 7 7V11C5.897 11 5 11.896 5 13V20C5 21.103 5.897 22 7 22H17C18.103 22 19 21.103 19 20V13C19 11.896 18.103 11 17 11ZM12 18C11.172 18 10.5 17.328 10.5 16.5C10.5 15.672 11.172 15 12 15C12.828 15 13.5 15.672 13.5 16.5C13.5 17.328 12.828 18 12 18ZM15 11H9V7C9 5.346 10.346 4 12 4C13.654 4 15 5.346 15 7V11Z" />
</svg>
),
HiddenChannelIcon: ErrorBoundary.wrap(() => (
<Tooltip text="Hidden Channel">
{({ onMouseLeave, onMouseEnter }) => (
<svg
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
className={ChannelListClasses.icon + " " + "shc-hidden-channel-icon"}
width="24"
height="24"
viewBox="0 0 24 24"
aria-hidden={true}
role="img"
>
<path className="shc-evenodd-fill-current-color" d="m19.8 22.6-4.2-4.15q-.875.275-1.762.413Q12.95 19 12 19q-3.775 0-6.725-2.087Q2.325 14.825 1 11.5q.525-1.325 1.325-2.463Q3.125 7.9 4.15 7L1.4 4.2l1.4-1.4 18.4 18.4ZM12 16q.275 0 .512-.025.238-.025.513-.1l-5.4-5.4q-.075.275-.1.513-.025.237-.025.512 0 1.875 1.312 3.188Q10.125 16 12 16Zm7.3.45-3.175-3.15q.175-.425.275-.862.1-.438.1-.938 0-1.875-1.312-3.188Q13.875 7 12 7q-.5 0-.938.1-.437.1-.862.3L7.65 4.85q1.025-.425 2.1-.638Q10.825 4 12 4q3.775 0 6.725 2.087Q21.675 8.175 23 11.5q-.575 1.475-1.512 2.738Q20.55 15.5 19.3 16.45Zm-4.625-4.6-3-3q.7-.125 1.288.112.587.238 1.012.688.425.45.613 1.038.187.587.087 1.162Z" />
</svg>
)}
</Tooltip>
), { noop: true })
});

View File

@ -0,0 +1,106 @@
.shc-lock-screen-outer-container {
background-color: var(--background-primary);
overflow: hidden scroll;
flex: 1 1 auto;
height: 100%;
width: 100%;
}
.shc-lock-screen-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
min-height: 100%;
}
.shc-lock-screen-container > * {
margin: 5px;
}
.shc-lock-screen-logo {
width: 180px;
height: 180px;
}
.shc-lock-screen-heading-container {
display: flex;
flex-direction: row;
align-items: center;
}
.shc-lock-screen-heading-container > * {
margin: inherit;
}
.shc-lock-screen-heading-nsfw-icon > path {
fill: var(--text-normal);
fill-rule: evenodd;
}
.shc-lock-screen-topic-container {
color: var(--text-normal);
background-color: var(--background-secondary);
border-radius: 5px;
padding: 10px;
max-width: 70vw;
}
.shc-lock-screen-tags-container {
background-color: var(--background-secondary);
border-radius: 5px;
padding: 10px;
max-width: 70vw;
}
.shc-lock-screen-tags-container > * {
margin: inherit;
}
.shc-lock-screen-tags {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
}
.shc-evenodd-fill-current-color {
fill-rule: evenodd;
fill: currentcolor;
}
.shc-hidden-channel-icon {
margin-left: 6px;
z-index: 0;
cursor: not-allowed;
}
.shc-lock-screen-default-emoji-container {
display: flex;
flex-direction: row;
align-items: center;
}
.shc-lock-screen-default-emoji-container > [class^="emojiContainer"] {
background-color: var(--background-secondary);
border-radius: 8px;
padding: 3px 4px;
margin-left: 5px;
}
.shc-lock-screen-allowed-users-and-roles-container {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--background-secondary);
border-radius: 5px;
padding: 10px;
max-width: 70vw;
}
.shc-lock-screen-allowed-users-and-roles-container > [class^="members"] {
margin-left: 10px;
flex-wrap: wrap;
}

View File

@ -56,7 +56,7 @@ function SilentTypingToggle() {
<div className={ButtonWrapperClasses.buttonWrapper}> <div className={ButtonWrapperClasses.buttonWrapper}>
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"> <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" /> <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> </svg>
</div> </div>
</Button> </Button>

View File

@ -23,8 +23,8 @@ import { Flex } from "@components/Flex";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
import { classes, LazyComponent } from "@utils/misc"; import { classes, LazyComponent } from "@utils/misc";
import { filters, find, findByCodeLazy } from "@webpack"; import { filters, find } from "@webpack";
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState } from "@webpack/common"; import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
import { SpotifyStore, Track } from "./SpotifyStore"; import { SpotifyStore, Track } from "./SpotifyStore";
@ -37,14 +37,6 @@ function msToHuman(ms: number) {
return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
} }
const useStateFromStores: <T>(
stores: typeof SpotifyStore[],
mapper: () => T,
idk?: null,
compare?: (old: T, newer: T) => boolean
) => T
= findByCodeLazy("useStateFromStores");
function Svg(path: string, label: string) { function Svg(path: string, label: string) {
return () => ( return () => (
<svg <svg

View File

@ -55,7 +55,7 @@ export default definePlugin({
// return React.createElement(AccountPanel, { ..., showTaglessAccountPanel: blah }) // return React.createElement(AccountPanel, { ..., showTaglessAccountPanel: blah })
match: /return ?(.{0,30}\(.{1,3},\{[^}]+?,showTaglessAccountPanel:.+?\}\))/, match: /return ?(.{0,30}\(.{1,3},\{[^}]+?,showTaglessAccountPanel:.+?\}\))/,
// return [Player, Panel] // return [Player, Panel]
replace: "return [Vencord.Plugins.plugins.SpotifyControls.renderPlayer(),$1]" replace: "return [$self.renderPlayer(),$1]"
} }
}, },
// Adds POST and a Marker to the SpotifyAPI (so we can easily find it) // Adds POST and a Marker to the SpotifyAPI (so we can easily find it)

View File

@ -1,20 +1,22 @@
#vc-spotify-player { #vc-spotify-player {
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
border-bottom: 1px solid var(--background-modifier-accent); border-bottom: 1px solid var(--background-modifier-accent);
--vc-spotify-green: #1db954; /* so cusotm themes can easily change it */ --vc-spotify-green: #1db954; /* so cusotm themes can easily change it */
} }
.vc-spotify-button { .vc-spotify-button {
background: none; background: none;
color: var(--interactive-normal); color: var(--interactive-normal);
padding: 0; padding: 0;
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: 100%; border-radius: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.vc-spotify-button:hover { .vc-spotify-button:hover {
color: var(--interactive-hover); color: var(--interactive-hover);
background-color: var(--background-modifier-selected); background-color: var(--background-modifier-selected);
@ -24,15 +26,18 @@
height: 24px; height: 24px;
width: 24px; width: 24px;
} }
[class*="vc-spotify-shuffle"] > svg, [class*="vc-spotify-shuffle"] > svg,
[class*="vc-spotify-repeat"] > svg { [class*="vc-spotify-repeat"] > svg {
width: 22px; width: 22px;
height: 22px; height: 22px;
} }
.vc-spotify-button svg path { .vc-spotify-button svg path {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
/* .vc-spotify-button:hover { /* .vc-spotify-button:hover {
filter: brightness(1.3); filter: brightness(1.3);
} */ } */
@ -51,7 +56,9 @@
white-space: nowrap; white-space: nowrap;
padding-right: 0.2em; padding-right: 0.2em;
max-width: 100%; max-width: 100%;
margin: unset;
} }
.vc-spotify-repeat-1 { .vc-spotify-repeat-1 {
font-size: 70%; font-size: 70%;
position: absolute; position: absolute;
@ -92,15 +99,12 @@
overflow: hidden; overflow: hidden;
} }
.vc-spotify-tooltip-text {
margin: unset;
}
#vc-spotify-song-title { #vc-spotify-song-title {
color: var(--header-primary); color: var(--header-primary);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
} }
.vc-spotify-ellipoverflow { .vc-spotify-ellipoverflow {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -137,7 +141,6 @@
#vc-spotify-progress-bar { #vc-spotify-progress-bar {
position: relative; position: relative;
color: var(--text-normal); color: var(--text-normal);
width: 100%; width: 100%;
margin: 0.5em 0; margin: 0.5em 0;
@ -153,6 +156,7 @@
#vc-spotify-progress-bar > [class^="slider"] [class^="bar-"] { #vc-spotify-progress-bar > [class^="slider"] [class^="bar-"] {
height: 4px !important; height: 4px !important;
} }
#vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] { #vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] {
/* these importants are neccessary, it applies a width and height through inline styles */ /* these importants are neccessary, it applies a width and height through inline styles */
height: 10px !important; height: 10px !important;
@ -168,7 +172,6 @@
.vc-spotify-progress-time { .vc-spotify-progress-time {
font-size: 12px; font-size: 12px;
top: 10px; top: 10px;
position: absolute; position: absolute;
} }
@ -176,6 +179,7 @@
.vc-spotify-time-left { .vc-spotify-time-left {
left: 0; left: 0;
} }
.vc-spotify-time-right { .vc-spotify-time-right {
right: 0; right: 0;
} }

View File

@ -29,7 +29,7 @@ export default definePlugin({
find: "PAYMENT_FLOW_MODAL_TEST_PAGE,", find: "PAYMENT_FLOW_MODAL_TEST_PAGE,",
replacement: { replacement: {
match: /{section:.{1,2}\..{1,3}\.PAYMENT_FLOW_MODAL_TEST_PAGE/, match: /{section:.{1,2}\..{1,3}\.PAYMENT_FLOW_MODAL_TEST_PAGE/,
replace: '{section:"StartupTimings",label:"Startup Timings",element:Vencord.Plugins.plugins.StartupTimings.StartupTimingPage},$&' replace: '{section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage},$&'
} }
}], }],
StartupTimingPage: LazyComponent(() => require("./StartupTimingPage").default) StartupTimingPage: LazyComponent(() => require("./StartupTimingPage").default)

View 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
});
}
});
}
});

View 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>
),
});

View File

@ -21,9 +21,10 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy } from "@webpack"; import { findByCodeLazy } from "@webpack";
import { GuildMemberStore, React } from "@webpack/common"; 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({ const settings = definePluginSettings({
showAvatars: { showAvatars: {
@ -43,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({ export default definePlugin({
name: "TypingTweaks", name: "TypingTweaks",
description: "Show avatars and role colours in the typing indicator", description: "Show avatars and role colours in the typing indicator",
@ -64,28 +74,21 @@ export default definePlugin({
replace: "return $1" replace: "return $1"
} }
}, },
// Changes indicator to format message with the typing users
{
find: ',"SEVERAL_USERS_TYPING","',
replacement: {
match: /(\i)\((\i),"SEVERAL_USERS_TYPING",".+?"\)/,
replace: "$1($2,\"SEVERAL_USERS_TYPING\",\"**!!{a}!!**, **!!{b}!!**, and {c} others are typing...\")"
},
predicate: () => settings.store.alternativeFormatting
},
// Adds the alternative formatting for several users typing // Adds the alternative formatting for several users typing
{ {
find: "getCooldownTextStyle", find: "getCooldownTextStyle",
replacement: { replacement: {
match: /(\i)\.length\?.\..\.Messages\.THREE_USERS_TYPING.format\(\{a:(\i),b:(\i),c:.}\).+?SEVERAL_USERS_TYPING/, match: /((\i)\.length\?.\..\.Messages\.THREE_USERS_TYPING.format\(\{a:(\i),b:(\i),c:.}\)):.+?SEVERAL_USERS_TYPING/,
replace: "$&.format({a:$2,b:$3,c:$1.length})" replace: "$1:$self.buildSeveralUsers({a:$3,b:$4,c:$2.length-2})"
}, },
predicate: () => settings.store.alternativeFormatting predicate: () => settings.store.alternativeFormatting
} }
], ],
settings, settings,
mutateChildren(props, users, children) { buildSeveralUsers,
mutateChildren(props: any, users: User[], children: any) {
if (!Array.isArray(children)) return children; if (!Array.isArray(children)) return children;
let element = 0; let element = 0;
@ -93,7 +96,7 @@ export default definePlugin({
return children.map(c => c.type === "strong" ? <this.TypingUser {...props} user={users[element++]} /> : c); return children.map(c => c.type === "strong" ? <this.TypingUser {...props} user={users[element++]} /> : c);
}, },
TypingUser: ErrorBoundary.wrap(({ user, guildId }) => { TypingUser: ErrorBoundary.wrap(({ user, guildId }: { user: User, guildId: string; }) => {
return <strong style={{ return <strong style={{
display: "grid", display: "grid",
gridAutoFlow: "column", gridAutoFlow: "column",
@ -102,10 +105,10 @@ export default definePlugin({
}}> }}>
{settings.store.showAvatars && <div style={{ marginTop: "4px" }}> {settings.store.showAvatars && <div style={{ marginTop: "4px" }}>
<Avatar <Avatar
size={Avatar.Sizes.SIZE_16} size="SIZE_16"
src={user.getAvatarURL(guildId, 128)} /> src={user.getAvatarURL(guildId, 128)} />
</div>} </div>}
{user.username} {GuildMemberStore.getNick(guildId!, user.id) || !guildId && RelationshipStore.getNickname(user.id) || user.username}
</strong>; </strong>;
}, { noop: true }) }, { noop: true })
}); });

View File

@ -42,16 +42,16 @@ export default definePlugin({
// voice/stage channels // voice/stage channels
{ {
match: /onClick:function\(\)\{(e\.handleClick.+?)}/g, match: /onClick:function\(\)\{(e\.handleClick.+?)}/g,
replace: "onClick:function(){Vencord.Plugins.plugins.VoiceChatDoubleClick.schedule(()=>{$1},e)}", replace: "onClick:function(){$self.schedule(()=>{$1},e)}",
}, },
], ],
}, },
{ {
// channel mentions // channel mentions
find: 'className:"channelMention",iconType:(', find: ".shouldCloseDefaultModals",
replacement: { replacement: {
match: /onClick:(.{1,3}),/, match: /onClick:(\i)(?=,.{0,30}className:"channelMention")/,
replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)(),", replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)()",
} }
} }
], ],

View File

@ -20,10 +20,11 @@ import { Settings } from "@api/settings";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { Margins } from "@utils/margins";
import { wordsToTitle } from "@utils/text"; import { wordsToTitle } from "@utils/text";
import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types"; import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types";
import { findByPropsLazy } from "@webpack"; 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 { interface VoiceState {
userId: string; userId: string;
@ -304,7 +305,7 @@ export default definePlugin({
</Forms.FormText> </Forms.FormText>
{hasEnglishVoices && ( {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 <div
style={{ style={{
display: "grid", display: "grid",

View File

@ -79,7 +79,7 @@ export default new class ViewIcons implements PluginDef {
}, },
{ {
match: /(id:"leave-guild".{0,200}),(\(0,.{1,3}\.jsxs?\).{0,200}function)/, match: /(id:"leave-guild".{0,200}),(\(0,.{1,3}\.jsxs?\).{0,200}function)/,
replace: "$1,Vencord.Plugins.plugins.ViewIcons.buildGuildContextMenuEntries(_guild),$2" replace: "$1,$self.buildGuildContextMenuEntries(_guild),$2"
} }
] ]
} }

View File

@ -20,10 +20,11 @@ import { addButton, removeButton } from "@api/MessagePopover";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc"; import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin from "@utils/types"; 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"; import { Message } from "discord-types/general";
@ -98,7 +99,7 @@ function openViewRawModal(msg: Message) {
<> <>
<Forms.FormTitle tag="h5">Content</Forms.FormTitle> <Forms.FormTitle tag="h5">Content</Forms.FormTitle>
<CodeBlock content={msg.content} lang="" /> <CodeBlock content={msg.content} lang="" />
<Forms.FormDivider className={Margins.marginBottom20} /> <Forms.FormDivider className={Margins.bottom20} />
</> </>
)} )}

View File

@ -32,12 +32,13 @@ const ReactionStore = findByPropsLazy("getReactions");
const queue = new Queue(); const queue = new Queue();
function fetchReactions(msg: Message, emoji: ReactionEmoji) { function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
const key = emoji.name + (emoji.id ? `:${emoji.id}` : ""); const key = emoji.name + (emoji.id ? `:${emoji.id}` : "");
return RestAPI.get({ return RestAPI.get({
url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`, url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`,
query: { query: {
limit: 100 limit: 100,
type
}, },
oldFormErrors: true oldFormErrors: true
}) })
@ -46,18 +47,19 @@ function fetchReactions(msg: Message, emoji: ReactionEmoji) {
channelId: msg.channel_id, channelId: msg.channel_id,
messageId: msg.id, messageId: msg.id,
users: res.body, users: res.body,
emoji emoji,
reactionType: type
})) }))
.catch(console.error) .catch(console.error)
.finally(() => sleep(250)); .finally(() => sleep(250));
} }
function getReactionsWithQueue(msg: Message, e: ReactionEmoji) { function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) {
const key = `${msg.id}:${e.name}:${e.id ?? ""}`; const key = `${msg.id}:${e.name}:${e.id ?? ""}:${type}`;
const cache = ReactionStore.__getLocalVars().reactions[key] ??= { fetched: false, users: {} }; const cache = ReactionStore.__getLocalVars().reactions[key] ??= { fetched: false, users: {} };
if (!cache.fetched) { if (!cache.fetched) {
queue.unshift(() => queue.unshift(() =>
fetchReactions(msg, e) fetchReactions(msg, e, type)
); );
cache.fetched = true; cache.fetched = true;
} }
@ -92,7 +94,7 @@ export default definePlugin({
find: ",reactionRef:", find: ",reactionRef:",
replacement: { replacement: {
match: /((.)=(.{1,3})\.hideCount)(,.+?reactionCount.+?\}\))/, match: /((.)=(.{1,3})\.hideCount)(,.+?reactionCount.+?\}\))/,
replace: "$1,whoReactedProps=$3$4,$2?null:Vencord.Plugins.plugins.WhoReacted.renderUsers(whoReactedProps)" replace: "$1,whoReactedProps=$3$4,$2?null:$self.renderUsers(whoReactedProps)"
} }
}], }],
@ -104,7 +106,7 @@ export default definePlugin({
); );
}, },
_renderUsers({ message, emoji }: RootObject) { _renderUsers({ message, emoji, type }: RootObject) {
const forceUpdate = useForceUpdater(); const forceUpdate = useForceUpdater();
React.useEffect(() => { React.useEffect(() => {
const cb = (e: any) => { const cb = (e: any) => {
@ -116,9 +118,16 @@ export default definePlugin({
return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb); return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb);
}, [message.id]); }, [message.id]);
const reactions = getReactionsWithQueue(message, emoji); const reactions = getReactionsWithQueue(message, emoji, type);
const users = Object.values(reactions).filter(Boolean) as User[]; const users = Object.values(reactions).filter(Boolean) as User[];
for (const user of users) {
FluxDispatcher.dispatch({
type: "USER_UPDATE",
user
});
}
return ( return (
<div <div
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }} style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}

View File

@ -27,7 +27,7 @@ export class Queue {
* @param maxSize The maximum amount of functions that can be queued at once. * @param maxSize The maximum amount of functions that can be queued at once.
* If the queue is full, the oldest function will be removed. * If the queue is full, the oldest function will be removed.
*/ */
constructor(public maxSize = Infinity) { } constructor(public readonly maxSize = Infinity) { }
private queue = [] as Array<() => Promisable<unknown>>; private queue = [] as Array<() => Promisable<unknown>>;

View File

@ -22,6 +22,7 @@ import gitRemote from "~git-remote";
export const WEBPACK_CHUNK = "webpackChunkdiscord_app"; export const WEBPACK_CHUNK = "webpackChunkdiscord_app";
export const REACT_GLOBAL = "Vencord.Webpack.Common.React"; export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`; 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 // Add yourself here if you made a plugin
export const Devs = /* #__PURE__*/ Object.freeze({ export const Devs = /* #__PURE__*/ Object.freeze({
@ -192,5 +193,17 @@ export const Devs = /* #__PURE__*/ Object.freeze({
captain: { captain: {
name: "Captain", name: "Captain",
id: 347366054806159360n id: 347366054806159360n
},
whqwert: {
name: "whqwert",
id: 586239091520176128n
},
lewisakura: {
name: "lewisakura",
id: 96269247411400704n
},
cloudburst: {
name: "cloudburst",
id: 892128204150685769n
} }
}); });

25
src/utils/guards.ts Normal file
View 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;
}

View File

@ -22,9 +22,11 @@ export * from "./debounce";
export * as Discord from "./discord"; export * as Discord from "./discord";
export { default as IpcEvents } from "./IpcEvents"; export { default as IpcEvents } from "./IpcEvents";
export { default as Logger } from "./Logger"; export { default as Logger } from "./Logger";
export * from "./margins";
export * from "./misc"; export * from "./misc";
export * as Modals from "./modal"; export * as Modals from "./modal";
export * from "./onceDefined"; export * from "./onceDefined";
export * from "./proxyLazy"; export * from "./proxyLazy";
export * from "./Queue"; export * from "./Queue";
export * from "./text";

35
src/utils/margins.ts Normal file
View File

@ -0,0 +1,35 @@
/*
* 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/>.
*/
let styleStr = "";
export const Margins: Record<`${"top" | "bottom" | "left" | "right"}${8 | 16 | 20}`, string> = {} as any;
for (const dir of ["top", "bottom", "left", "right"] as const) {
for (const size of [8, 16, 20] as const) {
const cl = `vc-m-${dir}-${size}`;
Margins[`${dir}${size}`] = cl;
styleStr += `.${cl}{margin-${dir}:${size}px;}`;
}
}
document.addEventListener("DOMContentLoaded", () =>
document.head.append(Object.assign(document.createElement("style"), {
textContent: styleStr,
id: "vencord-margins"
})), { once: true });

View File

@ -141,8 +141,8 @@ export function humanFriendlyJoin(elements: any[], mapper: (e: any) => string =
* Calls .join(" ") on the arguments * Calls .join(" ") on the arguments
* classes("one", "two") => "one two" * classes("one", "two") => "one two"
*/ */
export function classes(...classes: string[]) { export function classes(...classes: Array<string | null | undefined>) {
return classes.filter(c => typeof c === "string").join(" "); return classes.filter(Boolean).join(" ");
} }
/** /**
@ -200,3 +200,7 @@ export const checkIntersecting = (el: Element) => {
const documentHeight = Math.max(document.documentElement.clientHeight, window.innerHeight); const documentHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
return !(elementBox.bottom < 0 || elementBox.top - documentHeight >= 0); return !(elementBox.bottom < 0 || elementBox.top - documentHeight >= 0);
}; };
export function identity<T>(value: T): T {
return value;
}

View File

@ -117,6 +117,7 @@ const ModalAPI = mapMangledModuleLazy("onCloseRequest:null!=", {
openModal: filters.byCode("onCloseRequest:null!="), openModal: filters.byCode("onCloseRequest:null!="),
closeModal: filters.byCode("onCloseCallback&&"), closeModal: filters.byCode("onCloseCallback&&"),
openModalLazy: m => m?.length === 1 && filters.byCode(".apply(this,arguments)")(m), 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 { export function closeModal(modalKey: string, contextKey?: string): void {
return ModalAPI.closeModal(modalKey, contextKey); return ModalAPI.closeModal(modalKey, contextKey);
} }
/**
* Close all open modals
*/
export function closeAllModals(): void {
return ModalAPI.closeAllModals();
}

View File

@ -27,9 +27,13 @@ export function canonicalizeMatch(match: RegExp | string) {
return new RegExp(canonSource, match.flags); return new RegExp(canonSource, match.flags);
} }
export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string) { export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string): string | ReplaceFn {
if (typeof replace === "function") return replace; const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`;
return replace.replaceAll("$self", `Vencord.Plugins.plugins.${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) { export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) {

Some files were not shown because too many files have changed in this diff Show More