Compare commits
38 Commits
v1.0.4
...
feat/relat
Author | SHA1 | Date | |
---|---|---|---|
|
0e06b8d34c | ||
|
b972aa1663 | ||
|
3bf81ee0fa | ||
|
486230a335 | ||
|
77c691651e | ||
|
e14ec96e21 | ||
|
ff1f337699 | ||
|
3ca87848e5 | ||
|
9420735bc7 | ||
|
6807820f6c | ||
|
3cad0d60b4 | ||
|
fbbc198b1b | ||
|
224ae979f2 | ||
|
27fc20118b | ||
|
60ccd8cc25 | ||
|
5c1519156b | ||
|
58270ef925 | ||
|
68055977d2 | ||
|
2b0c25b45c | ||
|
c154965d70 | ||
|
614234ad20 | ||
|
2489bc6831 | ||
|
d95be1acba | ||
|
1d995e58f5 | ||
|
6114bc6b16 | ||
|
ae98401bd3 | ||
|
992a77e76c | ||
|
291f38115c | ||
|
8a52189378 | ||
|
70278f64a9 | ||
|
7b1d03699d | ||
|
8b40760187 | ||
|
de0990434e | ||
|
369d179bbf | ||
|
8f4e8d0a9b | ||
|
62f7e4d45c | ||
|
fce7d6b681 | ||
|
69715070b9 |
@ -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
6
.stylelintrc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"indentation": 4
|
||||
}
|
||||
}
|
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -42,7 +42,7 @@
|
||||
]
|
||||
},
|
||||
|
||||
"applications": {
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "vencord-firefox@vendicated.dev",
|
||||
"strict_min_version": "109.0"
|
||||
|
@ -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
727
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
|
||||
|
92
src/api/Notifications/NotificationComponent.tsx
Normal file
92
src/api/Notifications/NotificationComponent.tsx
Normal 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>
|
||||
);
|
||||
});
|
99
src/api/Notifications/Notifications.tsx
Normal file
99
src/api/Notifications/Notifications.tsx
Normal 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++));
|
||||
}
|
||||
}
|
19
src/api/Notifications/index.ts
Normal file
19
src/api/Notifications/index.ts
Normal 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";
|
49
src/api/Notifications/styles.css
Normal file
49
src/api/Notifications/styles.css
Normal 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%;
|
||||
}
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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 => (
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
3
src/modules.d.ts
vendored
@ -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;
|
||||
|
@ -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)),",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
}],
|
||||
});
|
||||
|
@ -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);'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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))",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -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'}",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -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) {
|
||||
|
37
src/plugins/colorSighted.ts
Normal file
37
src/plugins/colorSighted.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "ColorSighted",
|
||||
description: "Removes the colorblind-friendly icons from statuses, just like 2015-2017 Discord",
|
||||
authors: [Devs.lewisakura],
|
||||
patches: [
|
||||
{
|
||||
find: "Masks.STATUS_ONLINE",
|
||||
replacement: {
|
||||
// we can use global replacement here - these are specific to the status icons and are used nowhere else,
|
||||
// so it keeps the patch and plugin small and simple
|
||||
match: /Masks\.STATUS_(?:IDLE|DND|STREAMING|OFFLINE)/g,
|
||||
replace: "Masks.STATUS_ONLINE"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
@ -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])"
|
||||
},
|
||||
|
||||
},
|
||||
|
@ -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() {
|
||||
|
@ -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&&"
|
||||
}
|
||||
},
|
||||
],
|
||||
|
@ -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);"
|
||||
}
|
||||
}],
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -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"
|
||||
}
|
||||
}],
|
||||
|
||||
|
@ -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}
|
||||
|
3
src/plugins/messageLogger/deleteStyleOverlay.css
Normal file
3
src/plugins/messageLogger/deleteStyleOverlay.css
Normal file
@ -0,0 +1,3 @@
|
||||
.messagelogger-deleted {
|
||||
background-color: rgba(240 71 71 / 15%);
|
||||
}
|
3
src/plugins/messageLogger/deleteStyleText.css
Normal file
3
src/plugins/messageLogger/deleteStyleText.css
Normal file
@ -0,0 +1,3 @@
|
||||
.messagelogger-deleted div {
|
||||
color: #f04747;
|
||||
}
|
@ -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), "
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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);",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
250
src/plugins/relationshipNotifier.ts
Normal file
250
src/plugins/relationshipNotifier.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
58
src/plugins/revealAllSpoilers.ts
Normal file
58
src/plugins/revealAllSpoilers.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
@ -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) {
|
||||
|
@ -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",
|
||||
|
@ -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)"
|
||||
},
|
||||
}
|
||||
],
|
||||
|
67
src/plugins/richerCider.tsx
Normal file
67
src/plugins/richerCider.tsx
Normal 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 */
|
||||
}
|
||||
},
|
||||
});
|
123
src/plugins/roleColorEverywhere.tsx
Normal file
123
src/plugins/roleColorEverywhere.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
chatMentions: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Show role colors in chat mentions (including in the message box)",
|
||||
restartNeeded: true
|
||||
},
|
||||
memberList: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Show role colors in member list role headers",
|
||||
restartNeeded: true
|
||||
},
|
||||
voiceUsers: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Show role colors in the voice chat user list",
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "RoleColorEverywhere",
|
||||
authors: [Devs.KingFish, Devs.lewisakura],
|
||||
description: "Adds the top role color anywhere possible",
|
||||
patches: [
|
||||
// Chat Mentions
|
||||
{
|
||||
find: 'className:"mention"',
|
||||
replacement: [
|
||||
{
|
||||
match: /user:(\i),channelId:(\i).{0,300}?"@"\.concat\(.+?\)/,
|
||||
replace: "$&,color:$self.getUserColor($1.id,{channelId:$2})"
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.chatMentions,
|
||||
},
|
||||
// Slate
|
||||
{
|
||||
// taken from CommandsAPI
|
||||
find: ".source,children",
|
||||
replacement: [
|
||||
{
|
||||
match: /function \i\((\i)\).{5,20}id.{5,20}guildId.{5,10}channelId.{100,150}hidePersonalInformation.{5,50}jsx.{5,20},{/,
|
||||
replace: "$&color:$self.getUserColor($1.id,{guildId:$1.guildId}),"
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.chatMentions,
|
||||
},
|
||||
// Member List Role Names
|
||||
{
|
||||
find: ".memberGroupsPlaceholder",
|
||||
replacement: [
|
||||
{
|
||||
match: /(memo\(\(function\((\i)\).{300,500}CHANNEL_MEMBERS_A11Y_LABEL.{100,200}roleIcon.{5,20}null,).," \u2014 ",.\]/,
|
||||
replace: "$1$self.roleGroupColor($2)]"
|
||||
},
|
||||
],
|
||||
predicate: () => settings.store.memberList,
|
||||
},
|
||||
// Voice chat users
|
||||
{
|
||||
find: "renderPrioritySpeaker",
|
||||
replacement: [
|
||||
{
|
||||
match: /renderName=function\(\).{50,75}speaking.{50,100}jsx.{5,10}{/,
|
||||
replace: "$&...$self.getVoiceProps(this.props),"
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.voiceUsers,
|
||||
}
|
||||
],
|
||||
settings,
|
||||
|
||||
getColor(userId: string, { channelId, guildId }: { channelId?: string; guildId?: string; }) {
|
||||
if (!(guildId ??= ChannelStore.getChannel(channelId!)?.guild_id)) return null;
|
||||
return GuildMemberStore.getMember(guildId, userId)?.colorString ?? null;
|
||||
},
|
||||
getUserColor(userId: string, ids: { channelId?: string; guildId?: string; }) {
|
||||
const colorString = this.getColor(userId, ids);
|
||||
return colorString && parseInt(colorString.slice(1), 16);
|
||||
},
|
||||
roleGroupColor({ id, count, title, guildId }: { id: string; count: number; title: string; guildId: string; }) {
|
||||
const guild = GuildStore.getGuild(guildId);
|
||||
const role = guild?.roles[id];
|
||||
|
||||
return <span style={{
|
||||
color: role?.colorString,
|
||||
fontWeight: "unset",
|
||||
letterSpacing: ".05em"
|
||||
}}>{title} — {count}</span>;
|
||||
},
|
||||
getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) {
|
||||
return {
|
||||
style: {
|
||||
color: this.getColor(userId, { guildId })
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
)
|
||||
});
|
@ -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);
|
370
src/plugins/showHiddenChannels/index.tsx
Normal file
370
src/plugins/showHiddenChannels/index.tsx
Normal 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 })
|
||||
});
|
106
src/plugins/showHiddenChannels/style.css
Normal file
106
src/plugins/showHiddenChannels/style.css
Normal 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;
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
|
135
src/plugins/typingIndicator.tsx
Normal file
135
src/plugins/typingIndicator.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings, Settings } from "@api/settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { LazyComponent } from "@utils/misc";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { find, findLazy, findStoreLazy } from "@webpack";
|
||||
import { ChannelStore, GuildMemberStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { buildSeveralUsers } from "./typingTweaks";
|
||||
|
||||
const ThreeDots = LazyComponent(() => find(m => m.type?.render?.toString()?.includes("().dots")));
|
||||
|
||||
const TypingStore = findStoreLazy("TypingStore");
|
||||
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
|
||||
|
||||
const Formatters = findLazy(m => m.Messages?.SEVERAL_USERS_TYPING);
|
||||
|
||||
function getDisplayName(guildId: string, userId: string) {
|
||||
return GuildMemberStore.getNick(guildId, userId) ?? UserStore.getUser(userId).username;
|
||||
}
|
||||
|
||||
function TypingIndicator({ channelId }: { channelId: string; }) {
|
||||
const typingUsers: Record<string, number> = useStateFromStores(
|
||||
[TypingStore],
|
||||
() => ({ ...TypingStore.getTypingUsers(channelId) as Record<string, number> }),
|
||||
null,
|
||||
(old, current) => {
|
||||
const oldKeys = Object.keys(old);
|
||||
const currentKeys = Object.keys(current);
|
||||
|
||||
return oldKeys.length === currentKeys.length && JSON.stringify(oldKeys) === JSON.stringify(currentKeys);
|
||||
}
|
||||
);
|
||||
|
||||
const guildId = ChannelStore.getChannel(channelId).guild_id;
|
||||
|
||||
if (!settings.store.includeMutedChannels) {
|
||||
const isChannelMuted = UserGuildSettingsStore.isChannelMuted(guildId, channelId);
|
||||
if (isChannelMuted) return null;
|
||||
}
|
||||
|
||||
delete typingUsers[UserStore.getCurrentUser().id];
|
||||
|
||||
const typingUsersArray = Object.keys(typingUsers);
|
||||
let tooltipText: string;
|
||||
|
||||
switch (typingUsersArray.length) {
|
||||
case 0: break;
|
||||
case 1: {
|
||||
tooltipText = Formatters.Messages.ONE_USER_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]) });
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
tooltipText = Formatters.Messages.TWO_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) });
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
tooltipText = Formatters.Messages.THREE_USERS_TYPING.format({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
tooltipText = Settings.plugins.TypingTweaks.enabled
|
||||
? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: typingUsersArray.length - 2 })
|
||||
: Formatters.Messages.SEVERAL_USERS_TYPING;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (typingUsersArray.length > 0) {
|
||||
return (
|
||||
<Tooltip text={tooltipText!}>
|
||||
{({ onMouseLeave, onMouseEnter }) => (
|
||||
<div
|
||||
style={{ marginLeft: 6, zIndex: 0, cursor: "pointer" }}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<ThreeDots dotRadius={3} themed={true} />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
includeMutedChannels: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to show the typing indicator for muted channels.",
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "TypingIndicator",
|
||||
description: "Adds an indicator if someone is typing on a channel.",
|
||||
authors: [Devs.Nuckyz],
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".UNREAD_HIGHLIGHT",
|
||||
replacement: {
|
||||
match: /(?<=(?<channel>\i)=\i\.channel,.+?\(\)\.children.+?:null)/,
|
||||
replace: ",$self.TypingIndicator($<channel>.id)"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
TypingIndicator: (channelId: string) => (
|
||||
<ErrorBoundary noop>
|
||||
<TypingIndicator channelId={channelId} />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
});
|
@ -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 })
|
||||
});
|
||||
|
@ -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)()",
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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)" }}
|
||||
|
@ -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>>;
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
|
@ -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
35
src/utils/margins.ts
Normal 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 });
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)}`;
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
6
src/webpack/common/types/components.d.ts
vendored
6
src/webpack/common/types/components.d.ts
vendored
@ -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
24
src/webpack/common/types/index.d.ts
vendored
Normal 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
40
src/webpack/common/types/stores.d.ts
vendored
Normal 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>;
|
||||
}
|
14
src/webpack/common/types/utils.d.ts
vendored
14
src/webpack/common/types/utils.d.ts
vendored
@ -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"
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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!");
|
||||
|
||||
|
@ -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/**/*"]
|
||||
|
Reference in New Issue
Block a user