Compare commits

..

38 Commits

Author SHA1 Message Date
Rie Takahashi
0e06b8d34c grammar lol 2023-02-22 03:45:56 +00:00
Rie Takahashi
b972aa1663 fix some labels in settings 2023-02-22 03:44:47 +00:00
Rie Takahashi
3bf81ee0fa make each notification type toggleable 2023-02-22 03:42:19 +00:00
Rie Takahashi
486230a335 feat(plugins): add relationship notifier plugin 2023-02-22 03:13:39 +00:00
nick
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
Nuckyz
e14ec96e21 feat(FakeNitro): Bypass client themes and fixes (#504)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-18 03:32:02 +01:00
Vendicated
ff1f337699 Fix QuickCSS on electron 20+ 2023-02-17 15:37:38 +01:00
Nuckyz
3ca87848e5 TypingIndicator: Fix a dumb (#503)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-17 01:31:55 +01:00
Vendicated
9420735bc7 Version 1.0.6 2023-02-16 23:40:38 +01:00
Vendicated
6807820f6c Badges should use ErrorBoundaries 2023-02-16 22:46:51 +01:00
Vendicated
3cad0d60b4 Silly Discord changed a bunch of css vars 2023-02-16 22:40:19 +01:00
Vendicated
fbbc198b1b Fix PlatformIndicator 2023-02-16 22:31:13 +01:00
Nuckyz
224ae979f2 feat(plugins): Typing Indicator (#502) 2023-02-16 03:57:57 +01:00
Lewis Crichton
27fc20118b feat(plugin): RoleColorEverywhere (#482)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-16 02:50:42 +01:00
Nuckyz
60ccd8cc25 Various plugin fixes (#492)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-16 02:00:09 +01:00
Lewis Crichton
5c1519156b feat(plugin): ColorSighted (#501) 2023-02-16 01:46:14 +01:00
Vendicated
58270ef925 bump to v1.0.5 2023-02-14 19:22:01 +01:00
Vendicated
68055977d2 NotificationAPI: Correctly request browser permissions 2023-02-14 19:20:10 +01:00
Sammy
2b0c25b45c Feat(InvisibleChat): Add Autodecryption (#490)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-12 22:10:03 +01:00
Vendicated
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
Nuckyz
2489bc6831 Fix WhoReacted (#487)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-12 18:58:44 +01:00
fawn
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
Justice Almanzar
6114bc6b16 make proxies enumerable (#476)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-09 21:21:14 +01:00
Vendicated
ae98401bd3 Fix lag when alt tabbing to Discord 2023-02-09 19:36:30 +01:00
Nuckyz
992a77e76c ShowHiddenChannels: Stage and voice channels support (#469)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-08 21:54:11 +01:00
Nuckyz
291f38115c New webpack filter: byDisplayName (#474) 2023-02-08 21:48:26 +01:00
cryptofyre
8a52189378 feat(plugin): richerCider (#471)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-08 21:48:12 +01:00
Vendicated
70278f64a9 Fix broken patches 2023-02-01 18:00:25 +01:00
Vendicated
7b1d03699d ci(reporter): Ignore 404/429 errors 2023-02-01 14:13:55 +01:00
Nico
8b40760187 fix(showHiddenChannels): revert lock icon to correct path (#465) 2023-02-01 13:59:58 +01:00
whqwert
de0990434e feat(plugin): RevealAllSpoilers (#381)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-01 13:38:02 +01:00
Nuckyz
369d179bbf ShowHiddenChannels: New screen for showing hidden channels (#460)
Co-authored-by: Ven <vendicated@riseup.net>
2023-02-01 12:11:05 +01:00
Nick
8f4e8d0a9b TypingTweaks: fix crash on non en-US locales (#463) 2023-01-31 06:35:52 +01:00
Vendicated
62f7e4d45c Add stylelint 2023-01-30 05:04:06 +01:00
Vendicated
fce7d6b681 Make webpack types importable from @webpack/types 2023-01-30 04:53:28 +01:00
Vendicated
69715070b9 browser ext: change applications to browser_specific_settings 2023-01-29 00:22:11 +01:00
89 changed files with 3121 additions and 573 deletions

View File

@ -82,7 +82,6 @@
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"no-extra-semi": "error",
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
"dot-notation": "error",
"no-useless-escape": [
"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": [
"EditorConfig.EditorConfig",
"pmneo.tsimporter",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
"EditorConfig.EditorConfig",
"ExodiusStudios.comment-anchors",
"formulahendry.auto-rename-tag",
"GregorBiswanger.json2ts",
"eamodio.gitlens",
"kamikillerto.vscode-colorize"
"stylelint.vscode-stylelint"
]
}

View File

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

View File

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

727
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,92 @@
/*
* 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>
);
});

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 $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
import * as $Styles from "./Styles";
@ -88,3 +89,7 @@ export const MemberListDecorators = $MemberListDecorators;
* a
*/
export const Styles = $Styles;
/**
* An API allowing you to display notifications
*/
export const Notifications = $Notifications;

View File

@ -40,6 +40,12 @@ export interface Settings {
[setting: string]: any;
};
};
notifications: {
timeout: number;
position: "top-right" | "bottom-right";
useNative: "always" | "never" | "not-focused";
};
}
const DefaultSettings: Settings = {
@ -51,7 +57,13 @@ const DefaultSettings: Settings = {
frameless: false,
transparent: false,
winCtrlQ: false,
plugins: {}
plugins: {},
notifications: {
timeout: 5000,
position: "bottom-right",
useNative: "not-focused"
}
};
try {

View File

@ -103,7 +103,7 @@ const ErrorBoundary = LazyComponent(() => {
};
}) as
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?: Props): React.ComponentType<T>;
};
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (

View File

@ -222,7 +222,7 @@ export default ErrorBoundary.wrap(function PluginSettings() {
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
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.ENABLED) return false;
if (!searchValue.value.length) return true;

View File

@ -94,6 +94,7 @@
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
/* stylelint-disable-next-line property-no-unknown */
box-orient: vertical;
}
@ -132,6 +133,6 @@
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);
}

View File

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

View File

@ -21,23 +21,65 @@ import { useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import IpcEvents from "@utils/IpcEvents";
import { useAwaiter } from "@utils/misc";
import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common";
import { Margins } from "@utils/margins";
import { identity, useAwaiter } from "@utils/misc";
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
const cl = classNameFactory("vc-settings-");
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.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() {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
fallbackValue: "Loading..."
});
const settings = useSettings();
const notifSettings = settings.notifications;
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"
},
!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 (
<React.Fragment>
<DonateCard image={donateImage} />
@ -82,52 +124,76 @@ function VencordSettings() {
<Forms.FormDivider />
<Forms.FormSection className={Margins.marginTop16} title="Settings">
<Forms.FormText className={Margins.marginBottom20}>
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
<Forms.FormText className={Margins.bottom20}>
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
</Forms.FormText>
<Switch
value={settings.useQuickCss}
onChange={(v: boolean) => settings.useQuickCss = v}
note="Loads styles from your QuickCSS file">
Use QuickCSS
</Switch>
{!IS_WEB && (
<React.Fragment>
<Switch
value={settings.enableReactDevtools}
onChange={(v: boolean) => settings.enableReactDevtools = v}
note="Requires a full restart"
>
Enable React Developer Tools
</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>
)}
{Switches.map(s => s && (
<Switch
key={s.key}
value={settings[s.key]}
onChange={v => settings[s.key] = v}
note={s.note}
>
{s.title}
</Switch>
))}
</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>
);
}

View File

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

View File

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

3
src/modules.d.ts vendored
View File

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

View File

@ -36,7 +36,7 @@ export default definePlugin({
replacement: {
match: /uploadFiles:(.{1,2}),/,
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

@ -22,12 +22,17 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.",
authors: [Devs.KingFish],
authors: [Devs.KingFish, Devs.Ven],
patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
match: /\?(?<makeButton>\i)\(.{1,35}\.Messages\.CONFIGURE.+?message:(?<message>\i).+?children:\[/,
replace: "$&...Vencord.Api.MessagePopover._buildPopoverElements($<message>,$<makeButton>),"
// foo && !bar ? createElement(blah,...makeElement(addReactionData))
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$&"
},
{
match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/,
replace: '{if($1?.id=="VencordNotice")return ($1=null,Vencord.Api.Notices.nextNotice(),true);'
match: /(?<=,NOTICE_DISMISS:function\(\i\){)(?=if\(null==(\i)\))/,
replace: 'if($1?.id=="VencordNotice")return($1=null,Vencord.Api.Notices.nextNotice(),true);'
}
]
}

View File

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

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

@ -187,7 +187,7 @@ export default definePlugin({
find: "open-native-link",
replacement: {
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 definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { ChannelStore, UserStore } from "@webpack/common";
import { ChannelStore, PermissionStore, UserStore } from "@webpack/common";
const DRAFT_TYPE = 0;
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
const USE_EXTERNAL_EMOJIS = 1n << 18n;
const USE_EXTERNAL_STICKERS = 1n << 37n;
enum EmojiIntentions {
REACTION = 0,
STATUS = 1,
COMMUNITY_CONTENT = 2,
CHAT = 3,
GUILD_STICKER_RELATED_EMOJI = 4,
GUILD_ROLE_BENEFIT_EMOJI = 5,
COMMUNITY_CONTENT_ONLY = 6,
SOUNDBOARD = 7
}
interface BaseSticker {
available: boolean;
description: string;
@ -58,26 +72,39 @@ migratePluginSettings("FakeNitro", "NitroBypass");
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity],
description: "Allows you to stream in nitro quality and send fake emojis/stickers.",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity, Devs.captain],
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
dependencies: ["MessageEventsAPI"],
patches: [
{
find: "canUseAnimatedEmojis:function",
find: ".PREMIUM_LOCKED;",
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
replacement: [
"canUseAnimatedEmojis",
"canUseEmojisEverywhere"
].map(func => {
return {
match: new RegExp(`${func}:function\\(.+?\\{`),
replace: "$&return true;"
};
})
{
match: /(?<=(?<intention>\i)=\i\.intention)/,
replace: ",fakeNitroIntention=$<intention>"
},
{
match: /(?<=\.(?:canUseEmojisEverywhere|canUseAnimatedEmojis)\(\i)(?=\))/g,
replace: ",fakeNitroIntention"
},
{
match: /(?<=&&!\i&&)!(?<canUseExternal>\i)(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,
replace: `(!$<canUseExternal>&&![${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention))`
}
]
},
{
find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
replacement: {
match: /(?<=(?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\((?<user>\i))\){(?<premiumCheck>.+?\))/g,
replace: `,fakeNitroIntention){$<premiumCheck>||fakeNitroIntention===undefined||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
}
},
{
find: "canUseStickersEverywhere:function",
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
replacement: {
match: /canUseStickersEverywhere:function\(.+?\{/,
@ -93,7 +120,7 @@ export default definePlugin({
}
},
{
find: "canUseAnimatedEmojis:function",
find: "canStreamHighQuality:function",
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
replacement: [
"canUseHighVideoUploadQuality",
@ -114,6 +141,13 @@ export default definePlugin({
replace: ""
}
},
{
find: "canUseClientThemes:function",
replacement: {
match: /(?<=canUseClientThemes:function\(\i\){)/,
replace: "return true;"
}
}
],
options: {
@ -161,6 +195,22 @@ export default definePlugin({
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
},
hasPermissionToUseExternalEmojis(channelId: string) {
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
return PermissionStore.can(USE_EXTERNAL_EMOJIS, channel);
},
hasPermissionToUseExternalStickers(channelId: string) {
const channel = ChannelStore.getChannel(channelId);
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return true;
return PermissionStore.can(USE_EXTERNAL_STICKERS, channel);
},
getStickerLink(stickerId: string) {
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`;
},
@ -245,7 +295,7 @@ export default definePlugin({
if (!sticker)
break stickerBypass;
if (sticker.available !== false && (this.canUseStickers || (sticker as GuildSticker)?.guild_id === guildId))
if (sticker.available !== false && ((this.canUseStickers && this.hasPermissionToUseExternalStickers(channelId)) || (sticker as GuildSticker)?.guild_id === guildId))
break stickerBypass;
let link = this.getStickerLink(sticker.id);
@ -268,7 +318,7 @@ export default definePlugin({
}
}
if (!this.canUseEmotes && settings.enableEmojiBypass) {
if ((!this.canUseEmotes || !this.hasPermissionToUseExternalEmojis(channelId)) && settings.enableEmojiBypass) {
for (const emoji of messageObj.validNonShortcutEmojis) {
if (!emoji.require_colons) continue;
if (emoji.guildId === guildId && !emoji.animated) continue;
@ -284,22 +334,22 @@ export default definePlugin({
return { cancel: false };
});
if (!this.canUseEmotes && settings.enableEmojiBypass) {
this.preEdit = addPreEditListener((_, __, messageObj) => {
const { guildId } = this;
this.preEdit = addPreEditListener((channelId, __, messageObj) => {
if (this.canUseEmotes && this.hasPermissionToUseExternalEmojis(channelId)) return;
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
const emoji = EmojiStore.getCustomEmojiById(emojiId);
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
if (!emoji.require_colons) continue;
const { guildId } = this;
const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
});
}
});
}
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
const emoji = EmojiStore.getCustomEmojiById(emojiId);
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
if (!emoji.require_colons) continue;
const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
});
}
});
},
stop() {

View File

@ -30,7 +30,7 @@ export default definePlugin({
find: ".renderOwner=",
replacement: {
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",
replacement: {
match: /var .=(?<props>.)\.overlay.+?"aria-label":.\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY.+?}}\)/,
replace: "$&,Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton($<props>)"
replace: "$&,$self.renderToggleGameActivityButton($<props>)"
}
}, {
find: ".overlayBadge",
replacement: {
match: /.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?<props>.)\.name}\):null/,
replace: "$&,Vencord.Plugins.plugins.IgnoreActivities.renderToggleActivityButton($<props>)"
replace: "$&,$self.renderToggleActivityButton($<props>)"
}
}, {
find: '.displayName="LocalActivityStore"',
replacement: {
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
color={Button.Colors.GREEN}
onClick={() => {
const toSend = decrypt(secret, password);
const toSend = decrypt(secret, password, true);
if (!toSend || !props?.message) return;
// @ts-expect-error
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);

View File

@ -17,11 +17,13 @@
*/
import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
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 { Message } from "discord-types/general";
import { buildDecModal } from "./components/DecryptionModal";
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({
name: "InvisibleChat",
@ -133,7 +142,7 @@ export default definePlugin({
URL_REGEX: new RegExp(
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/,
),
settings,
async start() {
const { default: StegCloak } = await getStegCloak();
steggo = new StegCloak(true, false);
@ -145,7 +154,12 @@ export default definePlugin({
icon: this.popOverIcon,
message: message,
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;
});
@ -213,7 +227,31 @@ export function encrypt(secret: string, password: string, cover: string): string
return steggo.hide(secret + "\u200b", password, cover);
}
export function decrypt(secret: string, password: string): string {
return steggo.reveal(secret, password).replace("\u200b", "");
export function decrypt(secret: string, password: string, removeIndicator: boolean): string {
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

@ -68,7 +68,7 @@ export default definePlugin({
find: ".LOADING_DID_YOU_KNOW",
replacement: {
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}>
<span
style={{
backgroundColor: "var(--status-green-600)",
backgroundColor: "var(--green-360)",
width: "12px",
height: "12px",
borderRadius: "50%",
@ -64,7 +64,7 @@ function MemberCount() {
marginRight: "0.5em"
}}
/>
<span style={{ color: "var(--status-green-600)" }}>{online}</span>
<span style={{ color: "var(--green-360)" }}>{online}</span>
</div>
)}
</Tooltip>
@ -76,13 +76,13 @@ function MemberCount() {
width: "6px",
height: "6px",
borderRadius: "50%",
border: "3px solid var(--status-grey-500)",
border: "3px solid var(--primary-400)",
display: "inline-block",
marginRight: "0.5em",
marginLeft: "1em"
}}
/>
<span style={{ color: "var(--status-grey-500)" }}>{total}</span>
<span style={{ color: "var(--primary-400)" }}>{total}</span>
</div>
)}
</Tooltip>
@ -99,7 +99,7 @@ export default definePlugin({
find: ".isSidebarVisible,",
replacement: {
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

@ -139,6 +139,16 @@ interface MessageEmbedProps {
guildID: string;
}
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);
}
});
}
export default definePlugin({
name: "MessageLinkEmbeds",
description: "Adds a preview to messages that link another message",
@ -194,19 +204,24 @@ export default definePlugin({
messageEmbedAccessory(props) {
const { message }: { message: Message; } = props;
// @ts-ignore
const embeddedBy: string[] = message.vencordEmbeddedBy ?? [];
const accessories = [] as (JSX.Element | null)[];
let match = null as RegExpMatchArray | null;
while ((match = this.messageLinkRegex.exec(message.content!)) !== null) {
const [_, guildID, channelID, messageID] = match;
if (embeddedBy.includes(messageID)) {
continue;
}
const linkedChannel = ChannelStore.getChannel(channelID);
if (!linkedChannel || (guildID !== "@me" && !PermissionStore.can(1024n /* view channel */, linkedChannel))) {
continue;
}
let linkedMessage = messageCache[messageID]?.message as Message;
let linkedMessage = messageCache[messageID]?.message;
if (!linkedMessage) {
linkedMessage ??= MessageStore.getMessage(channelID, messageID);
if (linkedMessage) messageCache[messageID] = { message: linkedMessage, fetched: true };
@ -223,7 +238,7 @@ export default definePlugin({
}
}
const messageProps: MessageEmbedProps = {
message: linkedMessage,
message: withEmbeddedBy(linkedMessage, [...embeddedBy, message.id]),
channel: linkedChannel,
guildID
};
@ -267,7 +282,7 @@ export default definePlugin({
}
}}
renderDescription={() => {
return <div key={message.id} className={classNames.join(" ")} >
return <div key={message.id} className={classNames.join(" ")}>
<ChannelMessage
id={`message-link-embeds-${message.id}`}
message={message}

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 { Settings } from "@api/settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import Logger from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
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") {
document.body.classList.remove("messagelogger-red-overlay");
document.body.classList.add("messagelogger-red-text");
enableStyle(textStyle);
disableStyle(overlayStyle);
} else {
document.body.classList.remove("messagelogger-red-text");
document.body.classList.add("messagelogger-red-overlay");
disableStyle(textStyle);
enableStyle(overlayStyle);
}
}
@ -41,12 +45,12 @@ export default definePlugin({
authors: [Devs.rushii, Devs.Ven],
start() {
addDeleteStyleClass();
addDeleteStyle();
},
stop() {
document.querySelectorAll(".messageLogger-deleted").forEach(e => e.remove());
document.querySelectorAll(".messageLogger-edited").forEach(e => e.remove());
document.querySelectorAll(".messagelogger-deleted").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-text");
},
@ -54,7 +58,7 @@ export default definePlugin({
renderEdit(edit: { timestamp: any, content: string; }) {
return (
<ErrorBoundary noop>
<div className="messageLogger-edited">
<div className="messagelogger-edited">
{Parser.parse(edit.content)}
<Timestamp
timestamp={edit.timestamp}
@ -84,7 +88,7 @@ export default definePlugin({
{ label: "Red text", value: "text", default: true },
{ label: "Red overlay", value: "overlay" }
],
onChange: () => addDeleteStyleClass()
onChange: () => addDeleteStyle()
},
ignoreBots: {
type: OptionType.BOOLEAN,
@ -147,7 +151,7 @@ export default definePlugin({
replace:
"MESSAGE_DELETE:function($1){" +
" var cache = $2getOrCreate($1.channelId);" +
" cache = Vencord.Plugins.plugins.MessageLogger.handleDelete(cache, $1, false);" +
" cache = $self.handleDelete(cache, $1, false);" +
" $2commit(cache);" +
"},"
},
@ -157,7 +161,7 @@ export default definePlugin({
replace:
"MESSAGE_DELETE_BULK:function($1){" +
" var cache = $2getOrCreate($1.channelId);" +
" cache = Vencord.Plugins.plugins.MessageLogger.handleDelete(cache, $1, true);" +
" cache = $self.handleDelete(cache, $1, true);" +
" $2commit(cache);" +
"},"
},
@ -167,7 +171,7 @@ export default definePlugin({
replace: "$1" +
".update($3,m =>" +
" $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" +
")" +
".update($3"
@ -252,7 +256,7 @@ export default definePlugin({
},
{
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,"
},
{
// Append messageLogger-deleted to classNames if deleted
// Append messagelogger-deleted to classNames if deleted
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
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 {
background-color: rgba(240, 71, 71, 0.15);
}
.messagelogger-red-text .messageLogger-deleted div {
color: #f04747;
}
.messageLogger-deleted [class^="buttons"] {
.messagelogger-deleted [class^="buttons"] {
display: none;
}
.messageLogger-deleted-attachment {
.messagelogger-deleted-attachment {
filter: grayscale(1);
}
.messageLogger-deleted-attachment:hover {
.messagelogger-deleted-attachment:hover {
filter: grayscale(0);
transition: 250ms filter linear;
}
.theme-dark .messageLogger-edited {
.theme-dark .messagelogger-edited {
filter: brightness(80%);
}
.theme-light .messageLogger-edited {
.theme-light .messagelogger-edited {
opacity: 0.5;
}

View File

@ -43,7 +43,7 @@ export default definePlugin({
replacement: [
{
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

@ -51,7 +51,7 @@ export default definePlugin({
replacement: {
match: /CREATE_PENDING_REPLY:function\((.{1,2})\){/,
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;
const getStatusColor = findByCodeLazy("STATUS_YELLOW", "TWITCH", "STATUS_GREY");
const getStatusColor = findByCodeLazy(".TWITCH", ".STREAMING", ".INVISIBLE");
const PlatformIcon = ({ platform, status }: { platform: Platform, status: string; }) => {
const tooltip = platform[0].toUpperCase() + platform.slice(1);

View File

@ -38,7 +38,7 @@ export default definePlugin({
find: "showCommunicationDisabledStyles",
replacement: {
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
@ -46,7 +46,7 @@ export default definePlugin({
find: ".Messages.BOT_PROFILE_SLASH_COMMANDS",
replacement: {
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

View File

@ -0,0 +1,250 @@
/*
* 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 { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher, UserUtils } from "@webpack/common";
import { User } from "discord-types/general";
enum RelationshipType {
NONE = 0,
FRIEND = 1,
BLOCKED = 2,
PENDING_INCOMING = 3,
PENDING_OUTGOING = 4,
IMPLICIT = 5
}
interface RelationshipPayload {
type: "RELATIONSHIP_ADD" | "RELATIONSHIP_REMOVE" | "RELATIONSHIP_UPDATE";
relationship: {
id: string;
type: RelationshipType;
since?: Date;
nickname?: string;
user?: User;
};
}
const settings = definePluginSettings({
friend: {
type: OptionType.SELECT,
description: "Show a notification when a friend is added or removed",
options: [{
label: "Friend added and removed",
value: "ALL",
default: true,
}, {
label: "Only when added",
value: "CREATE",
}, {
label: "Only when removed",
value: "REMOVE",
}, {
label: "No notifications",
value: "NONE",
}]
},
outgoingRequest: {
type: OptionType.SELECT,
description: "Show a notification when you send or cancel a friend request",
options: [{
label: "Request sent and cancelled",
value: "ALL",
default: true,
}, {
label: "Only when sent",
value: "CREATE",
}, {
label: "Only when cancelled",
value: "REMOVE",
}, {
label: "No notifications",
value: "NONE",
}]
},
incomingRequest: {
type: OptionType.SELECT,
description: "Show a notification when an incoming request is received or cancelled",
options: [{
label: "Request received and cancelled",
value: "ALL",
default: true,
}, {
label: "Only when received",
value: "CREATE",
}, {
label: "Only when cancelled",
value: "REMOVE",
}, {
label: "No notifications",
value: "NONE",
}]
},
block: {
type: OptionType.SELECT,
description: "Show a notification when you block or unblock a user",
options: [{
label: "Blocking and unblocking",
value: "ALL",
default: true,
}, {
label: "Only when blocking",
value: "CREATE",
}, {
label: "Only when unblocking",
value: "REMOVE",
}, {
label: "No notifications",
value: "NONE",
}]
},
});
export default definePlugin({
name: "RelationshipNotifier",
authors: [Devs.Megu],
description: "Receive notifications for friend requests, removals, blocks, etc.",
settings,
start() {
FluxDispatcher.subscribe("RELATIONSHIP_ADD", onRelationshipUpdate);
FluxDispatcher.subscribe("RELATIONSHIP_UPDATE", onRelationshipUpdate);
FluxDispatcher.subscribe("RELATIONSHIP_REMOVE", onRelationshipRemove);
},
stop() {
FluxDispatcher.unsubscribe("RELATIONSHIP_ADD", onRelationshipUpdate);
FluxDispatcher.unsubscribe("RELATIONSHIP_UPDATE", onRelationshipUpdate);
FluxDispatcher.unsubscribe("RELATIONSHIP_REMOVE", onRelationshipRemove);
}
});
async function onRelationshipUpdate({ relationship }: RelationshipPayload) {
if (!relationship.id) return;
const user = await UserUtils.fetchUser(relationship.id);
if (!user) return;
function onClick() {
FluxDispatcher.dispatch({
type: "USER_PROFILE_MODAL_OPEN",
userId: user.id
});
}
switch (relationship.type) {
case RelationshipType.FRIEND: {
if (!["ALL", "CREATE"].includes(settings.store.friend)) break;
showNotification({
title: "Friend Added",
body: `${user.username} is now your friend.`,
icon: user.getAvatarURL(),
onClick,
});
break;
}
case RelationshipType.PENDING_INCOMING: {
if (!["ALL", "CREATE"].includes(settings.store.incomingRequest)) break;
showNotification({
title: "Friend Request Received",
body: `${user.username} sent you a friend request.`,
icon: user.getAvatarURL(),
onClick,
});
break;
}
case RelationshipType.PENDING_OUTGOING: {
if (!["ALL", "CREATE"].includes(settings.store.outgoingRequest)) break;
showNotification({
title: "Friend Request Sent",
body: `You sent a friend request to ${user.username}`,
icon: user.getAvatarURL(),
onClick
});
break;
}
case RelationshipType.BLOCKED: {
if (!["ALL", "CREATE"].includes(settings.store.block)) break;
showNotification({
title: "User Blocked",
body: `You just blocked ${user.username}`,
icon: user.getAvatarURL(),
onClick
});
break;
}
}
}
async function onRelationshipRemove({ relationship }: RelationshipPayload) {
if (!relationship.id) return;
const user = await UserUtils.fetchUser(relationship.id);
if (!user) return;
function onClick() {
FluxDispatcher.dispatch({
type: "USER_PROFILE_MODAL_OPEN",
userId: user.id
});
}
switch (relationship.type) {
case RelationshipType.FRIEND: {
if (!["ALL", "REMOVE"].includes(settings.store.friend)) break;
showNotification({
title: "Friend Removed",
body: `${user.username} is no longer on your friends list.`,
icon: user.getAvatarURL(),
onClick,
});
break;
}
case RelationshipType.PENDING_INCOMING: {
if (!["ALL", "REMOVE"].includes(settings.store.incomingRequest)) break;
showNotification({
title: "Friend Request Cancelled",
body: `${user.username} cancelled their friend request.`,
icon: user.getAvatarURL(),
onClick,
});
break;
}
case RelationshipType.PENDING_OUTGOING: {
if (!["ALL", "REMOVE"].includes(settings.store.outgoingRequest)) break;
showNotification({
title: "Friend Request Cancelled",
body: `You cancelled your friend request to ${user.username}`,
icon: user.getAvatarURL(),
onClick
});
break;
}
case RelationshipType.BLOCKED: {
if (!["ALL", "REMOVE"].includes(settings.store.block)) break;
showNotification({
title: "User Unblocked",
body: `You just unblocked ${user.username}`,
icon: user.getAvatarURL(),
onClick
});
break;
}
}
}

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: ".revealSpoiler=function",
replacement: {
match: /\.revealSpoiler=function\((.{1,2})\){/,
replace: ".revealSpoiler=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
find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,",
replacement: {
// url1 = url2 = props.attachment.url
// ...
// OpenLinks(url2 != null ? url2 : url1, someStuffs)
//
// the back references are needed because the code is like Z(a!=null?b:c,d), no way to match that
// otherwise
match: /(?<props>.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/,
replace: "$&,$<props>.target"
}
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",
replacement: [
{
match: /ariaLabel:\i\.Z\.Messages\.MESSAGE_ACTIONS_MENU_LABEL/,
replace: "$&,_vencordTarget:arguments[0].target"
},
{
// var f = props.itemHref, .... MakeNativeMenu(null != f ? f : blah)
match: /(\i)=\i\.itemHref,.+?\(null!=\1\?\1:.{1,10}(?=\))/,
replace: "$&,arguments[0]._vencordTarget"
}
]
}],
makeMenu(src: string, target: HTMLElement) {

View File

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

View File

@ -37,7 +37,7 @@ export default definePlugin({
find: "disableBorderColor:!0",
replacement: {
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;
padding: 0.5em;
position: relative;
font-size: 0.875rem;
line-height: 1.125rem;
text-indent: 0;
@ -47,7 +46,7 @@
padding: 4px 8px;
}
.shiki-btn~.shiki-btn {
.shiki-btn ~ .shiki-btn {
margin-left: 4px;
}
@ -57,7 +56,7 @@
.shiki-spinner-container {
align-items: center;
background-color: rgba(0, 0, 0, 0.6);
background-color: rgb(0 0 0 / 60%);
display: flex;
position: absolute;
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}>
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path fill="currentColor" d="M528 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h480c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM128 180v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm-336 96v-40c0-6.627-5.373-12-12-12H76c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12zm288 0v-40c0-6.627-5.373-12-12-12H172c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h232c6.627 0 12-5.373 12-12zm96 0v-40c0-6.627-5.373-12-12-12h-40c-6.627 0-12 5.373-12 12v40c0 6.627 5.373 12 12 12h40c6.627 0 12-5.373 12-12z" />
{isEnabled && <path d="M13 432L590 48" stroke="var(--status-red-500)" stroke-width="72" stroke-linecap="round" />}
{isEnabled && <path d="M13 432L590 48" stroke="var(--red-500)" stroke-width="72" stroke-linecap="round" />}
</svg>
</div>
</Button>

View File

@ -23,8 +23,8 @@ import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { debounce } from "@utils/debounce";
import { classes, LazyComponent } from "@utils/misc";
import { filters, find, findByCodeLazy } from "@webpack";
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState } from "@webpack/common";
import { filters, find } from "@webpack";
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
import { SpotifyStore, Track } from "./SpotifyStore";
@ -37,14 +37,6 @@ function msToHuman(ms: number) {
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) {
return () => (
<svg

View File

@ -55,7 +55,7 @@ export default definePlugin({
// return React.createElement(AccountPanel, { ..., showTaglessAccountPanel: blah })
match: /return ?(.{0,30}\(.{1,3},\{[^}]+?,showTaglessAccountPanel:.+?\}\))/,
// 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)

View File

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

View File

@ -29,7 +29,7 @@ export default definePlugin({
find: "PAYMENT_FLOW_MODAL_TEST_PAGE,",
replacement: {
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)

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,7 +21,8 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
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:");
@ -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({
name: "TypingTweaks",
description: "Show avatars and role colours in the typing indicator",
@ -64,36 +74,29 @@ export default definePlugin({
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
{
find: "getCooldownTextStyle",
replacement: {
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})"
match: /((\i)\.length\?.\..\.Messages\.THREE_USERS_TYPING.format\(\{a:(\i),b:(\i),c:.}\)):.+?SEVERAL_USERS_TYPING/,
replace: "$1:$self.buildSeveralUsers({a:$3,b:$4,c:$2.length-2})"
},
predicate: () => settings.store.alternativeFormatting
}
],
settings,
mutateChildren(props, users, children) {
buildSeveralUsers,
mutateChildren(props: any, users: User[], children: any) {
if (!Array.isArray(children)) return children;
let element = 0;
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={{
display: "grid",
gridAutoFlow: "column",
@ -103,9 +106,9 @@ export default definePlugin({
{settings.store.showAvatars && <div style={{ marginTop: "4px" }}>
<Avatar
size={Avatar.Sizes.SIZE_16}
src={user.getAvatarURL(guildId, 128)}/>
src={user.getAvatarURL(guildId, 128)} />
</div>}
{user.username}
{GuildMemberStore.getNick(guildId!, user.id) || !guildId && RelationshipStore.getNickname(user.id) || user.username}
</strong>;
}, { noop: true })
});

View File

@ -42,16 +42,16 @@ export default definePlugin({
// voice/stage channels
{
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
find: 'className:"channelMention",iconType:(',
find: ".EMOJI_IN_MESSAGE_HOVER",
replacement: {
match: /onClick:(.{1,3}),/,
replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)(),",
match: /onClick:(\i)(?=,.{0,30}className:"channelMention")/,
replace: "onClick:(_vcEv)=>(_vcEv.detail>=2||_vcEv.target.className.includes('MentionText'))&&($1)()",
}
}
],

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)/,
replace: "$1,Vencord.Plugins.plugins.ViewIcons.buildGuildContextMenuEntries(_guild),$2"
replace: "$1,$self.buildGuildContextMenuEntries(_guild),$2"
}
]
}

View File

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

View File

@ -27,7 +27,7 @@ export class Queue {
* @param maxSize The maximum amount of functions that can be queued at once.
* 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>>;

View File

@ -192,5 +192,13 @@ export const Devs = /* #__PURE__*/ Object.freeze({
captain: {
name: "Captain",
id: 347366054806159360n
},
whqwert: {
name: "whqwert",
id: 586239091520176128n
},
lewisakura: {
name: "lewisakura",
id: 96269247411400704n
}
});

View File

@ -22,9 +22,11 @@ export * from "./debounce";
export * as Discord from "./discord";
export { default as IpcEvents } from "./IpcEvents";
export { default as Logger } from "./Logger";
export * from "./margins";
export * from "./misc";
export * as Modals from "./modal";
export * from "./onceDefined";
export * from "./proxyLazy";
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

@ -200,3 +200,7 @@ export const checkIntersecting = (el: Element) => {
const documentHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
return !(elementBox.bottom < 0 || elementBox.top - documentHeight >= 0);
};
export function identity<T>(value: T): T {
return value;
}

View File

@ -22,6 +22,9 @@ const unconfigurable = ["arguments", "caller", "prototype"];
const handler: ProxyHandler<any> = {};
const GET_KEY = Symbol.for("vencord.lazy.get");
const CACHED_KEY = Symbol.for("vencord.lazy.cached");
for (const method of [
"apply",
"construct",
@ -38,11 +41,11 @@ for (const method of [
"setPrototypeOf"
]) {
handler[method] =
(target: any, ...args: any[]) => Reflect[method](target.get(), ...args);
(target: any, ...args: any[]) => Reflect[method](target[GET_KEY](), ...args);
}
handler.ownKeys = target => {
const v = target.get();
const v = target[GET_KEY]();
const keys = Reflect.ownKeys(v);
for (const key of unconfigurable) {
if (!keys.includes(key)) keys.push(key);
@ -54,7 +57,10 @@ handler.getOwnPropertyDescriptor = (target, p) => {
if (typeof p === "string" && unconfigurable.includes(p))
return Reflect.getOwnPropertyDescriptor(target, p);
return Reflect.getOwnPropertyDescriptor(target.get(), p);
const descriptor = Reflect.getOwnPropertyDescriptor(target[GET_KEY](), p);
if (descriptor) Object.defineProperty(target, p, descriptor);
return descriptor;
};
/**
@ -67,10 +73,10 @@ handler.getOwnPropertyDescriptor = (target, p) => {
* @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah);
*/
export function proxyLazy<T>(factory: () => T): T {
const proxyDummy: { (): void; cachedValue?: T; get(): T; } = function () { };
proxyDummy.cachedValue = void 0;
proxyDummy.get = () => proxyDummy.cachedValue ??= factory();
const proxyDummy: { (): void; [CACHED_KEY]?: T; [GET_KEY](): T; } = Object.assign(function () { }, {
[CACHED_KEY]: void 0,
[GET_KEY]: () => proxyDummy[CACHED_KEY] ??= factory(),
});
return new Proxy(proxyDummy, handler) as any;
}

View File

@ -112,7 +112,6 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> {
if (file) {
try {
console.log(file);
await importSettings(new TextDecoder().decode(file.data));
if (showToast) toastSuccess();
} catch (err) {

View File

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { moment } from "@webpack/common";
// Utils for readable text transformations eg: `toTitle(fromKebab())`
// Case style to words
@ -34,3 +36,59 @@ export const wordsToPascal = (words: string[]) =>
words.map(w => w[0].toUpperCase() + w.slice(1)).join("");
export const wordsToTitle = (words: string[]) =>
words.map(w => w[0].toUpperCase() + w.slice(1)).join(" ");
const units = ["years", "months", "weeks", "days", "hours", "minutes", "seconds"] as const;
type Units = typeof units[number];
function getUnitStr(unit: Units, isOne: boolean, short: boolean) {
if (short === false) return isOne ? unit.slice(0, -1) : unit;
return unit[0];
}
/**
* Forms time into a human readable string link "1 day, 2 hours, 3 minutes and 4 seconds"
* @param time The time on the specified unit
* @param unit The unit the time is on
* @param short Whether to use short units like "d" instead of "days"
*/
export function formatDuration(time: number, unit: Units, short: boolean = false) {
const dur = moment.duration(time, unit);
let unitsAmounts = units.map(unit => ({ amount: dur[unit](), unit }));
let amountsToBeRemoved = 0;
outer:
for (let i = 0; i < unitsAmounts.length; i++) {
if (unitsAmounts[i].amount === 0 || !(i + 1 < unitsAmounts.length)) continue;
for (let v = i + 1; v < unitsAmounts.length; v++) {
if (unitsAmounts[v].amount !== 0) continue outer;
}
amountsToBeRemoved = unitsAmounts.length - (i + 1);
}
unitsAmounts = amountsToBeRemoved === 0 ? unitsAmounts : unitsAmounts.slice(0, -amountsToBeRemoved);
const daysAmountIndex = unitsAmounts.findIndex(({ unit }) => unit === "days");
if (daysAmountIndex !== -1) {
const daysAmount = unitsAmounts[daysAmountIndex];
const daysMod = daysAmount.amount % 7;
if (daysMod === 0) unitsAmounts.splice(daysAmountIndex, 1);
else daysAmount.amount = daysMod;
}
let res: string = "";
while (unitsAmounts.length) {
const { amount, unit } = unitsAmounts.shift()!;
if (res.length) res += unitsAmounts.length ? ", " : " and ";
if (amount > 0 || res.length) {
res += `${amount} ${getUnitStr(unit, amount === 1, short)}`;
}
}
return res.length ? res : `0 ${getUnitStr(unit, false, short)}`;
}

View File

@ -49,5 +49,8 @@ export const Slider = waitForComponent<t.Slider>("Slider", filters.byCode("close
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record<string, string>;
/**
* @deprecated Use @utils/margins instead
*/
export const Margins: t.Margins = findByPropsLazy("marginTop20");
export const ButtonLooks: t.ButtonLooks = findByPropsLazy("BLANK", "FILLED", "INVERTED");

View File

@ -19,7 +19,7 @@
import { LazyComponent } from "@utils/misc";
// eslint-disable-next-line path-alias/no-relative
import { FilterFn, waitFor } from "../webpack";
import { FilterFn, filters, waitFor } from "../webpack";
export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]): T {
let myValue: T = function () {
@ -34,3 +34,7 @@ export function waitForComponent<T extends React.ComponentType<any> = React.Comp
return lazyComponent;
}
export function waitForStore(name: string, cb: (v: any) => void) {
waitFor(filters.byStoreName(name), cb);
}

View File

@ -25,7 +25,7 @@ export let useEffect: typeof React.useEffect;
export let useMemo: typeof React.useMemo;
export let useRef: typeof React.useRef;
export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPortal", "render");
export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/client") = findByPropsLazy("createPortal", "render");
waitFor("useState", m => {
React = m;

View File

@ -19,36 +19,71 @@
import type * as Stores from "discord-types/stores";
// eslint-disable-next-line path-alias/no-relative
import { filters, findByPropsLazy, mapMangledModuleLazy, waitFor } from "../webpack";
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "../webpack";
import { waitForStore } from "./internal";
import * as t from "./types/stores";
export const MessageStore = findByPropsLazy("getRawMessages") as Omit<Stores.MessageStore, "getMessages"> & {
export const Flux: t.Flux = findByPropsLazy("connectStores");
type GenericStore = t.FluxStore & Record<string, any>;
export let MessageStore: Omit<Stores.MessageStore, "getMessages"> & {
getMessages(chanId: string): any;
};
export const PermissionStore = findByPropsLazy("can", "getGuildPermissions");
export const PrivateChannelsStore = findByPropsLazy("openPrivateChannel");
export const GuildChannelStore = findByPropsLazy("getChannels");
export const ReadStateStore = findByPropsLazy("lastMessageId");
export const PresenceStore = findByPropsLazy("setCurrentUserOnConnectionOpen");
export let GuildStore: Stores.GuildStore;
export let UserStore: Stores.UserStore;
export let SelectedChannelStore: Stores.SelectedChannelStore;
export let SelectedGuildStore: any;
export let ChannelStore: Stores.ChannelStore;
export let GuildMemberStore: Stores.GuildMemberStore;
export let RelationshipStore: Stores.RelationshipStore & {
// this is not actually a FluxStore
export const PrivateChannelsStore = findByPropsLazy("openPrivateChannel");
export let PermissionStore: GenericStore;
export let GuildChannelStore: GenericStore;
export let ReadStateStore: GenericStore;
export let PresenceStore: GenericStore;
export let GuildStore: Stores.GuildStore & t.FluxStore;
export let UserStore: Stores.UserStore & t.FluxStore;
export let SelectedChannelStore: Stores.SelectedChannelStore & t.FluxStore;
export let SelectedGuildStore: t.FluxStore & Record<string, any>;
export let ChannelStore: Stores.ChannelStore & t.FluxStore;
export let GuildMemberStore: Stores.GuildMemberStore & t.FluxStore;
export let RelationshipStore: Stores.RelationshipStore & t.FluxStore & {
/** Get the date (as a string) that the relationship was created */
getSince(userId: string): string;
};
export let WindowStore: t.WindowStore;
export const MaskedLinkStore = mapMangledModuleLazy('"MaskedLinkStore"', {
openUntrustedLink: filters.byCode(".apply(this,arguments)")
});
waitFor(["getCurrentUser", "initialize"], m => UserStore = m);
waitFor("getSortedPrivateChannels", m => ChannelStore = m);
waitFor("getCurrentlySelectedChannelId", m => SelectedChannelStore = m);
waitFor("getLastSelectedGuildId", m => SelectedGuildStore = m);
waitFor("getGuildCount", m => GuildStore = m);
waitFor(["getMember", "initialize"], m => GuildMemberStore = m);
waitFor("getRelationshipType", m => RelationshipStore = m);
/**
* React hook that returns stateful data for one or more stores
* You might need a custom comparator (4th argument) if your store data is an object
*
* @param stores The stores to listen to
* @param mapper A function that returns the data you need
* @param idk some thing, idk just pass null
* @param isEqual A custom comparator for the data returned by mapper
*
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
*/
export const useStateFromStores: <T>(
stores: t.FluxStore[],
mapper: () => T,
idk?: any,
isEqual?: (old: T, newer: T) => boolean
) => T
= findByCodeLazy("useStateFromStores");
waitForStore("UserStore", s => UserStore = s);
waitForStore("ChannelStore", m => ChannelStore = m);
waitForStore("SelectedChannelStore", m => SelectedChannelStore = m);
waitForStore("SelectedGuildStore", m => SelectedGuildStore = m);
waitForStore("GuildStore", m => GuildStore = m);
waitForStore("GuildMemberStore", m => GuildMemberStore = m);
waitForStore("RelationshipStore", m => RelationshipStore = m);
waitForStore("PermissionStore", m => PermissionStore = m);
waitForStore("PresenceStore", m => PresenceStore = m);
waitForStore("ReadStateStore", m => ReadStateStore = m);
waitForStore("GuildChannelStore", m => GuildChannelStore = m);
waitForStore("MessageStore", m => MessageStore = m);
waitForStore("WindowStore", m => WindowStore = m);

View File

@ -215,9 +215,9 @@ export type Select = ComponentType<PropsWithChildren<{
closeOnSelect?: boolean;
hideIcon?: boolean;
select?(value: any): void;
isSelected?(value: any): boolean;
serialize?(value: any): string;
select(value: any): void;
isSelected(value: any): boolean;
serialize(value: any): string;
clear?(): void;
maxVisibleItems?: number;

24
src/webpack/common/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
/*
* 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 "./components";
export * from "./fluxEvents";
export * from "./menu";
export * from "./stores";
export * from "./utils";

40
src/webpack/common/types/stores.d.ts vendored Normal file
View File

@ -0,0 +1,40 @@
/*
* 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 { FluxDispatcher, FluxEvents } from "./utils";
export class FluxStore {
constructor(dispatcher: FluxDispatcher, eventHandlers?: Partial<Record<FluxEvents, (data: any) => void>>);
emitChange(): void;
getDispatchToken(): string;
getName(): string;
initialize(): void;
initializeIfNeeded(): void;
__getLocalVars(): Record<string, any>;
}
export interface Flux {
Store: typeof FluxStore;
}
export class WindowStore extends FluxStore {
isElementFullScreen(): boolean;
isFocused(): boolean;
windowSize(): Record<"width" | "height", number>;
}

View File

@ -31,20 +31,6 @@ export interface FluxDispatcher {
unsubscribe(event: FluxEvents, callback: (data: any) => void): void;
}
declare class FluxStore {
constructor(dispatcher: FluxDispatcher, eventHandlers?: Partial<Record<FluxEvents, (data: any) => void>>);
emitChange(): void;
getDispatchToken(): string;
getName(): string;
initialize(): void;
initializeIfNeeded(): void;
}
export interface Flux {
Store: typeof FluxStore;
}
export type Parser = Record<
| "parse"
| "parseTopic"

View File

@ -23,7 +23,6 @@ import { _resolveReady,filters, findByCodeLazy, findByPropsLazy, mapMangledModul
import type * as t from "./types/utils";
export let FluxDispatcher: t.FluxDispatcher;
export const Flux: t.Flux = findByPropsLazy("connectStores");
export const RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get");
export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear");

View File

@ -21,6 +21,7 @@ import Logger from "@utils/Logger";
import { canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types";
import { traceFunction } from "../debug/Tracer";
import { _initWebpack } from ".";
let webpackChunk: any[];
@ -132,6 +133,7 @@ function patchPush() {
for (let i = 0; i < patches.length; i++) {
const patch = patches[i];
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
if (patch.predicate && !patch.predicate()) continue;
if (code.includes(patch.find)) {
@ -146,7 +148,7 @@ function patchPush() {
canonicalizeReplacement(replacement, patch.plugin);
try {
const newCode = code.replace(replacement.match, replacement.replace as string);
const newCode = executePatch(replacement.match, replacement.replace as string);
if (newCode === code && !patch.noWarn) {
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`);
if (IS_DEV) {
@ -187,7 +189,7 @@ function patchPush() {
}
logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
logger.errorCustomFmt(...Logger.makeTitle("white", "After"), context);
logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
}

View File

@ -50,6 +50,8 @@ export const filters = {
}
return true;
},
byStoreName: (name: string): FilterFn => m =>
m.constructor?.displayName === name
};
export const subscriptions = new Map<FilterFn, CallbackFn>();
@ -326,6 +328,20 @@ export function findByCodeLazy(...code: string[]) {
return findLazy(filters.byCode(...code));
}
/**
* Find a store by its displayName
*/
export function findStore(name: string) {
return find(filters.byStoreName(name));
}
/**
* findByDisplayName but lazy
*/
export function findStoreLazy(name: string) {
return findLazy(filters.byStoreName(name));
}
/**
* Wait for a module that matches the provided filter to be registered,
* then call the callback with the module as the first argument

View File

@ -186,8 +186,11 @@ page.on("console", async e => {
} else if (isDebug) {
console.error(e.text());
} else if (level === "error") {
console.error("Got unexpected error", e.text());
report.otherErrors.push(e.text());
const text = e.text();
if (!text.startsWith("Failed to load resource: the server responded with a status of")) {
console.error("Got unexpected error", text);
report.otherErrors.push(text);
}
}
});
@ -209,6 +212,7 @@ function runTime(token: string) {
// Monkey patch Logger to not log with custom css
// @ts-ignore
Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
if (level === "warn" || level === "error")
console[level]("[Vencord]", this.name + ":", ...args);
@ -253,6 +257,8 @@ function runTime(token: string) {
if (!isWasm)
await wreq.e(id as any);
await new Promise(r => setTimeout(r, 100));
}
console.error("[PUP_DEBUG]", "Finished loading chunks!");

View File

@ -4,6 +4,7 @@
"esModuleInterop": true,
"lib": [
"DOM",
"DOM.Iterable",
"esnext",
"esnext.array",
"esnext.asynciterable",
@ -18,11 +19,12 @@
"baseUrl": "./src/",
"paths": {
"@webpack": ["./webpack"],
"@webpack/common": ["./webpack/common"],
"@utils/*": ["./utils/*"],
"@api/*": ["./api/*"],
"@components/*": ["./components/*"]
"@components/*": ["./components/*"],
"@utils/*": ["./utils/*"],
"@webpack/types": ["./webpack/common/types"],
"@webpack/common": ["./webpack/common"],
"@webpack": ["./webpack/webpack"]
}
},
"include": ["src/**/*"]