mirror of
https://github.com/Vendicated/Vencord.git
synced 2024-09-20 06:30:35 +00:00
Merge branch 'dev' into main
This commit is contained in:
commit
e9cb2eccec
82 changed files with 1770 additions and 411 deletions
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
|
@ -1,11 +1,9 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"eamodio.gitlens",
|
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"ExodiusStudios.comment-anchors",
|
|
||||||
"formulahendry.auto-rename-tag",
|
|
||||||
"GregorBiswanger.json2ts",
|
"GregorBiswanger.json2ts",
|
||||||
"stylelint.vscode-stylelint"
|
"stylelint.vscode-stylelint",
|
||||||
|
"Vendicated.vencord-companion"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.8.1",
|
"version": "1.8.4",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
@ -303,8 +303,10 @@ async function runtime(token: string) {
|
||||||
delete patch.predicate;
|
delete patch.predicate;
|
||||||
delete patch.group;
|
delete patch.group;
|
||||||
|
|
||||||
if (!Array.isArray(patch.replacement))
|
Vencord.Util.canonicalizeFind(patch);
|
||||||
|
if (!Array.isArray(patch.replacement)) {
|
||||||
patch.replacement = [patch.replacement];
|
patch.replacement = [patch.replacement];
|
||||||
|
}
|
||||||
|
|
||||||
patch.replacement.forEach(r => {
|
patch.replacement.forEach(r => {
|
||||||
delete r.predicate;
|
delete r.predicate;
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mergeDefaults } from "@utils/misc";
|
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { MessageActions, SnowflakeUtils } from "@webpack/common";
|
import { MessageActions, SnowflakeUtils } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { debounce } from "@shared/debounce";
|
||||||
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore";
|
||||||
import { localStorage } from "@utils/localStorage";
|
import { localStorage } from "@utils/localStorage";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { mergeDefaults } from "@utils/misc";
|
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||||
import { putCloudSettings } from "@utils/settingsSync";
|
import { putCloudSettings } from "@utils/settingsSync";
|
||||||
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
|
||||||
import { React } from "@webpack/common";
|
import { React } from "@webpack/common";
|
||||||
|
|
|
@ -21,7 +21,7 @@ import "./addonCard.css";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { Badge } from "@components/Badge";
|
import { Badge } from "@components/Badge";
|
||||||
import { Switch } from "@components/Switch";
|
import { Switch } from "@components/Switch";
|
||||||
import { Text } from "@webpack/common";
|
import { Text, useRef } from "@webpack/common";
|
||||||
import type { MouseEventHandler, ReactNode } from "react";
|
import type { MouseEventHandler, ReactNode } from "react";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-addon-");
|
const cl = classNameFactory("vc-addon-");
|
||||||
|
@ -42,6 +42,8 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
|
||||||
|
const titleRef = useRef<HTMLDivElement>(null);
|
||||||
|
const titleContainerRef = useRef<HTMLDivElement>(null);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cl("card", { "card-disabled": disabled })}
|
className={cl("card", { "card-disabled": disabled })}
|
||||||
|
@ -51,7 +53,21 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
|
||||||
<div className={cl("header")}>
|
<div className={cl("header")}>
|
||||||
<div className={cl("name-author")}>
|
<div className={cl("name-author")}>
|
||||||
<Text variant="text-md/bold" className={cl("name")}>
|
<Text variant="text-md/bold" className={cl("name")}>
|
||||||
{name}{isNew && <Badge text="NEW" color="#ED4245" />}
|
<div ref={titleContainerRef} className={cl("title-container")}>
|
||||||
|
<div
|
||||||
|
ref={titleRef}
|
||||||
|
className={cl("title")}
|
||||||
|
onMouseOver={() => {
|
||||||
|
const title = titleRef.current!;
|
||||||
|
const titleContainer = titleContainerRef.current!;
|
||||||
|
|
||||||
|
title.style.setProperty("--offset", `${titleContainer.clientWidth - title.scrollWidth}px`);
|
||||||
|
title.style.setProperty("--duration", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
|
||||||
</Text>
|
</Text>
|
||||||
{!!author && (
|
{!!author && (
|
||||||
<Text variant="text-md/normal" className={cl("author")}>
|
<Text variant="text-md/normal" className={cl("author")}>
|
||||||
|
|
|
@ -180,7 +180,8 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Forms.FormTitle>replacement</Forms.FormTitle>
|
{/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
|
||||||
|
<Forms.FormTitle className="">replacement</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={replacement?.toString()}
|
value={replacement?.toString()}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -188,7 +189,7 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
/>
|
/>
|
||||||
{!isFunc && (
|
{!isFunc && (
|
||||||
<div className="vc-text-selectable">
|
<div className="vc-text-selectable">
|
||||||
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
|
||||||
{Object.entries({
|
{Object.entries({
|
||||||
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
|
||||||
"$$": "Insert a $",
|
"$$": "Insert a $",
|
||||||
|
@ -220,11 +221,12 @@ function ReplacementInput({ replacement, setReplacement, replacementError }) {
|
||||||
|
|
||||||
interface FullPatchInputProps {
|
interface FullPatchInputProps {
|
||||||
setFind(v: string): void;
|
setFind(v: string): void;
|
||||||
|
setParsedFind(v: string | RegExp): void;
|
||||||
setMatch(v: string): void;
|
setMatch(v: string): void;
|
||||||
setReplacement(v: string | ReplaceFn): void;
|
setReplacement(v: string | ReplaceFn): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputProps) {
|
function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
|
||||||
const [fullPatch, setFullPatch] = React.useState<string>("");
|
const [fullPatch, setFullPatch] = React.useState<string>("");
|
||||||
const [fullPatchError, setFullPatchError] = React.useState<string>("");
|
const [fullPatchError, setFullPatchError] = React.useState<string>("");
|
||||||
|
|
||||||
|
@ -233,6 +235,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
||||||
setFullPatchError("");
|
setFullPatchError("");
|
||||||
|
|
||||||
setFind("");
|
setFind("");
|
||||||
|
setParsedFind("");
|
||||||
setMatch("");
|
setMatch("");
|
||||||
setReplacement("");
|
setReplacement("");
|
||||||
return;
|
return;
|
||||||
|
@ -256,7 +259,8 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
||||||
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
|
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
|
||||||
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
|
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
|
||||||
|
|
||||||
setFind(parsed.find);
|
setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
|
||||||
|
setParsedFind(parsed.find);
|
||||||
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
|
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
|
||||||
setReplacement(parsed.replacement.replace);
|
setReplacement(parsed.replacement.replace);
|
||||||
setFullPatchError("");
|
setFullPatchError("");
|
||||||
|
@ -266,7 +270,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Forms.FormText>Paste your full JSON patch here to fill out the fields</Forms.FormText>
|
<Forms.FormText className={Margins.bottom8}>Paste your full JSON patch here to fill out the fields</Forms.FormText>
|
||||||
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
|
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
|
||||||
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
|
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
|
||||||
</>;
|
</>;
|
||||||
|
@ -274,6 +278,7 @@ function FullPatchInput({ setFind, setMatch, setReplacement }: FullPatchInputPro
|
||||||
|
|
||||||
function PatchHelper() {
|
function PatchHelper() {
|
||||||
const [find, setFind] = React.useState<string>("");
|
const [find, setFind] = React.useState<string>("");
|
||||||
|
const [parsedFind, setParsedFind] = React.useState<string | RegExp>("");
|
||||||
const [match, setMatch] = React.useState<string>("");
|
const [match, setMatch] = React.useState<string>("");
|
||||||
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
|
||||||
|
|
||||||
|
@ -285,20 +290,34 @@ function PatchHelper() {
|
||||||
const code = React.useMemo(() => {
|
const code = React.useMemo(() => {
|
||||||
return `
|
return `
|
||||||
{
|
{
|
||||||
find: ${JSON.stringify(find)},
|
find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
|
||||||
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`.trim();
|
`.trim();
|
||||||
}, [find, match, replacement]);
|
}, [parsedFind, match, replacement]);
|
||||||
|
|
||||||
function onFindChange(v: string) {
|
function onFindChange(v: string) {
|
||||||
setFindError(void 0);
|
setFindError(void 0);
|
||||||
setFind(v);
|
setFind(v);
|
||||||
if (v.length) {
|
}
|
||||||
findCandidates({ find: v, setModule, setError: setFindError });
|
|
||||||
|
function onFindBlur() {
|
||||||
|
try {
|
||||||
|
let parsedFind = find as string | RegExp;
|
||||||
|
if (/^\/.+?\/$/.test(find)) parsedFind = new RegExp(find.slice(1, -1));
|
||||||
|
|
||||||
|
setFindError(void 0);
|
||||||
|
setFind(find);
|
||||||
|
setParsedFind(parsedFind);
|
||||||
|
|
||||||
|
if (find.length) {
|
||||||
|
findCandidates({ find: parsedFind, setModule, setError: setFindError });
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setFindError((e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,19 +336,21 @@ function PatchHelper() {
|
||||||
<Forms.FormTitle>full patch</Forms.FormTitle>
|
<Forms.FormTitle>full patch</Forms.FormTitle>
|
||||||
<FullPatchInput
|
<FullPatchInput
|
||||||
setFind={onFindChange}
|
setFind={onFindChange}
|
||||||
|
setParsedFind={setParsedFind}
|
||||||
setMatch={onMatchChange}
|
setMatch={onMatchChange}
|
||||||
setReplacement={setReplacement}
|
setReplacement={setReplacement}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle>find</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
value={find}
|
value={find}
|
||||||
onChange={onFindChange}
|
onChange={onFindChange}
|
||||||
|
onBlur={onFindBlur}
|
||||||
error={findError}
|
error={findError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Forms.FormTitle>match</Forms.FormTitle>
|
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
|
||||||
<CheckedTextInput
|
<CheckedTextInput
|
||||||
value={match}
|
value={match}
|
||||||
onChange={onMatchChange}
|
onChange={onMatchChange}
|
||||||
|
@ -342,6 +363,7 @@ function PatchHelper() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className={Margins.top8} />
|
||||||
<ReplacementInput
|
<ReplacementInput
|
||||||
replacement={replacement}
|
replacement={replacement}
|
||||||
setReplacement={setReplacement}
|
setReplacement={setReplacement}
|
||||||
|
|
|
@ -62,3 +62,36 @@
|
||||||
.vc-addon-author::before {
|
.vc-addon-author::before {
|
||||||
content: "by ";
|
content: "by ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vc-addon-title-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 1.25em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-addon-title {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes vc-addon-title {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateX(var(--offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-addon-title:hover {
|
||||||
|
overflow: visible;
|
||||||
|
animation: vc-addon-title var(--duration) linear infinite;
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import type { Settings } from "@api/Settings";
|
import type { Settings } from "@api/Settings";
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
import { SettingsStore } from "@shared/SettingsStore";
|
import { SettingsStore } from "@shared/SettingsStore";
|
||||||
|
import { mergeDefaults } from "@utils/mergeDefaults";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
|
||||||
|
@ -42,7 +43,22 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string
|
||||||
RendererSettings.setData(data, pathToNotify);
|
RendererSettings.setData(data, pathToNotify);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NativeSettings = new SettingsStore(readSettings("native", NATIVE_SETTINGS_FILE));
|
export interface NativeSettings {
|
||||||
|
plugins: {
|
||||||
|
[plugin: string]: {
|
||||||
|
[setting: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultNativeSettings: NativeSettings = {
|
||||||
|
plugins: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
|
||||||
|
mergeDefaults(nativeSettings, DefaultNativeSettings);
|
||||||
|
|
||||||
|
export const NativeSettings = new SettingsStore(nativeSettings);
|
||||||
|
|
||||||
NativeSettings.addGlobalChangeListener(() => {
|
NativeSettings.addGlobalChangeListener(() => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -35,6 +35,7 @@ export const ALLOWED_PROTOCOLS = [
|
||||||
"steam:",
|
"steam:",
|
||||||
"spotify:",
|
"spotify:",
|
||||||
"com.epicgames.launcher:",
|
"com.epicgames.launcher:",
|
||||||
|
"tidal:"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
|
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");
|
||||||
|
|
|
@ -26,55 +26,63 @@ import UpdaterTab from "@components/VencordSettings/UpdaterTab";
|
||||||
import VencordTab from "@components/VencordSettings/VencordTab";
|
import VencordTab from "@components/VencordSettings/VencordTab";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { React } from "@webpack/common";
|
import { i18n, React } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
|
|
||||||
|
type SectionType = "HEADER" | "DIVIDER" | "CUSTOM";
|
||||||
|
type SectionTypes = Record<SectionType, SectionType>;
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
description: "Adds Settings UI and debug info",
|
description: "Adds Settings UI and debug info",
|
||||||
authors: [Devs.Ven, Devs.Megu],
|
authors: [Devs.Ven, Devs.Megu],
|
||||||
required: true,
|
required: true,
|
||||||
|
|
||||||
patches: [{
|
patches: [
|
||||||
find: ".versionHash",
|
{
|
||||||
replacement: [
|
find: ".versionHash",
|
||||||
{
|
replacement: [
|
||||||
match: /\[\(0,.{1,3}\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
|
{
|
||||||
replace: (m, component, props) => {
|
match: /\[\(0,\i\.jsxs?\)\((.{1,10}),(\{[^{}}]+\{.{0,20}.versionHash,.+?\})\)," "/,
|
||||||
props = props.replace(/children:\[.+\]/, "");
|
replace: (m, component, props) => {
|
||||||
return `${m},$self.makeInfoElements(${component}, ${props})`;
|
props = props.replace(/children:\[.+\]/, "");
|
||||||
|
return `${m},$self.makeInfoElements(${component}, ${props})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /copyValue:\i\.join\(" "\)/,
|
||||||
|
replace: "$& + $self.getInfoString()"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Discord Canary
|
||||||
|
{
|
||||||
|
find: "Messages.ACTIVITY_SETTINGS",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=section:(.{0,50})\.DIVIDER\}\))([,;])(?=.{0,200}(\i)\.push.{0,100}label:(\i)\.header)/,
|
||||||
|
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "useDefaultUserSettingsSections:function",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=useDefaultUserSettingsSections:function\(\){return )(\i)\}/,
|
||||||
|
replace: "$self.wrapSettingsHook($1)}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
|
||||||
|
replace: "$2.default.open($1);return;"
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}, {
|
|
||||||
find: "Messages.ACTIVITY_SETTINGS",
|
|
||||||
replacement: {
|
|
||||||
get match() {
|
|
||||||
switch (Settings.plugins.Settings.settingsLocation) {
|
|
||||||
case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/;
|
|
||||||
case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/;
|
|
||||||
case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/;
|
|
||||||
case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/;
|
|
||||||
case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/;
|
|
||||||
case "aboveActivity":
|
|
||||||
default:
|
|
||||||
return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
replace: "...$self.makeSettingsCategories($1),$&"
|
|
||||||
}
|
}
|
||||||
}, {
|
],
|
||||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
|
||||||
replacement: {
|
|
||||||
match: /(?<=function\((\i),\i\)\{)(?=let \i=Object.values\(\i.UserSettingsSections\).*?(\i)\.default\.open\()/,
|
|
||||||
replace: "$2.default.open($1);return;"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
|
|
||||||
customSections: [] as ((SectionTypes: Record<string, unknown>) => any)[],
|
customSections: [] as ((SectionTypes: SectionTypes) => any)[],
|
||||||
|
|
||||||
makeSettingsCategories(SectionTypes: Record<string, unknown>) {
|
makeSettingsCategories(SectionTypes: SectionTypes) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
section: SectionTypes.HEADER,
|
section: SectionTypes.HEADER,
|
||||||
|
@ -130,19 +138,63 @@ export default definePlugin({
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isRightSpot({ header, settings }: { header?: string; settings?: string[]; }) {
|
||||||
|
const firstChild = settings?.[0];
|
||||||
|
// lowest two elements... sanity backup
|
||||||
|
if (firstChild === "LOGOUT" || firstChild === "SOCIAL_LINKS") return true;
|
||||||
|
|
||||||
|
const { settingsLocation } = Settings.plugins.Settings;
|
||||||
|
|
||||||
|
if (settingsLocation === "bottom") return firstChild === "LOGOUT";
|
||||||
|
if (settingsLocation === "belowActivity") return firstChild === "CHANGELOG";
|
||||||
|
|
||||||
|
if (!header) return;
|
||||||
|
|
||||||
|
const names = {
|
||||||
|
top: i18n.Messages.USER_SETTINGS,
|
||||||
|
aboveNitro: i18n.Messages.BILLING_SETTINGS,
|
||||||
|
belowNitro: i18n.Messages.APP_SETTINGS,
|
||||||
|
aboveActivity: i18n.Messages.ACTIVITY_SETTINGS
|
||||||
|
};
|
||||||
|
return header === names[settingsLocation];
|
||||||
|
},
|
||||||
|
|
||||||
|
patchedSettings: new WeakSet(),
|
||||||
|
|
||||||
|
addSettings(elements: any[], element: { header?: string; settings: string[]; }, sectionTypes: SectionTypes) {
|
||||||
|
if (this.patchedSettings.has(elements) || !this.isRightSpot(element)) return;
|
||||||
|
|
||||||
|
this.patchedSettings.add(elements);
|
||||||
|
|
||||||
|
elements.push(...this.makeSettingsCategories(sectionTypes));
|
||||||
|
},
|
||||||
|
|
||||||
|
wrapSettingsHook(originalHook: (...args: any[]) => Record<string, unknown>[]) {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
const elements = originalHook(...args);
|
||||||
|
if (!this.patchedSettings.has(elements))
|
||||||
|
elements.unshift(...this.makeSettingsCategories({
|
||||||
|
HEADER: "HEADER",
|
||||||
|
DIVIDER: "DIVIDER",
|
||||||
|
CUSTOM: "CUSTOM"
|
||||||
|
}));
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
settingsLocation: {
|
settingsLocation: {
|
||||||
type: OptionType.SELECT,
|
type: OptionType.SELECT,
|
||||||
description: "Where to put the Vencord settings section",
|
description: "Where to put the Vencord settings section",
|
||||||
options: [
|
options: [
|
||||||
{ label: "At the very top", value: "top" },
|
{ label: "At the very top", value: "top" },
|
||||||
{ label: "Above the Nitro section", value: "aboveNitro" },
|
{ label: "Above the Nitro section", value: "aboveNitro", default: true },
|
||||||
{ label: "Below the Nitro section", value: "belowNitro" },
|
{ label: "Below the Nitro section", value: "belowNitro" },
|
||||||
{ label: "Above Activity Settings", value: "aboveActivity", default: true },
|
{ label: "Above Activity Settings", value: "aboveActivity" },
|
||||||
{ label: "Below Activity Settings", value: "belowActivity" },
|
{ label: "Below Activity Settings", value: "belowActivity" },
|
||||||
{ label: "At the very bottom", value: "bottom" },
|
{ label: "At the very bottom", value: "bottom" },
|
||||||
],
|
]
|
||||||
restartNeeded: true
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -169,15 +221,24 @@ export default definePlugin({
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
|
||||||
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) {
|
getInfoRows() {
|
||||||
const { electronVersion, chromiumVersion, additionalInfo } = this;
|
const { electronVersion, chromiumVersion, additionalInfo } = this;
|
||||||
|
|
||||||
return (
|
const rows = [`Vencord ${gitHash}${additionalInfo}`];
|
||||||
<>
|
|
||||||
<Component {...props}>Vencord {gitHash}{additionalInfo}</Component>
|
if (electronVersion) rows.push(`Electron ${electronVersion}`);
|
||||||
{electronVersion && <Component {...props}>Electron {electronVersion}</Component>}
|
if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`);
|
||||||
{chromiumVersion && <Component {...props}>Chromium {chromiumVersion}</Component>}
|
|
||||||
</>
|
return rows;
|
||||||
|
},
|
||||||
|
|
||||||
|
getInfoString() {
|
||||||
|
return "\n" + this.getInfoRows().join("\n");
|
||||||
|
},
|
||||||
|
|
||||||
|
makeInfoElements(Component: React.ComponentType<React.PropsWithChildren>, props: React.PropsWithChildren) {
|
||||||
|
return this.getInfoRows().map((text, i) =>
|
||||||
|
<Component key={i} {...props}>{text}</Component>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,20 +16,24 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DataStore } from "@api/index";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Link } from "@components/Link";
|
||||||
|
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
||||||
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
import { isPluginDev } from "@utils/misc";
|
import { isPluginDev } from "@utils/misc";
|
||||||
|
import { relaunch } from "@utils/native";
|
||||||
import { makeCodeblock } from "@utils/text";
|
import { makeCodeblock } from "@utils/text";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { isOutdated } from "@utils/updater";
|
import { isOutdated, update } from "@utils/updater";
|
||||||
import { Alerts, Forms, UserStore } from "@webpack/common";
|
import { Alerts, Card, ChannelStore, Forms, GuildMemberStore, NavigationRouter, Parser, RelationshipStore, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import gitHash from "~git-hash";
|
import gitHash from "~git-hash";
|
||||||
import plugins from "~plugins";
|
import plugins from "~plugins";
|
||||||
|
|
||||||
import settings from "./settings";
|
import settings from "./settings";
|
||||||
|
|
||||||
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
|
const VENCORD_GUILD_ID = "1015060230222131221";
|
||||||
|
|
||||||
const AllowedChannelIds = [
|
const AllowedChannelIds = [
|
||||||
SUPPORT_CHANNEL_ID,
|
SUPPORT_CHANNEL_ID,
|
||||||
|
@ -37,6 +41,12 @@ const AllowedChannelIds = [
|
||||||
"1033680203433660458", // Vencord > #v
|
"1033680203433660458", // Vencord > #v
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const TrustedRolesIds = [
|
||||||
|
"1026534353167208489", // contributor
|
||||||
|
"1026504932959977532", // regular
|
||||||
|
"1042507929485586532", // donor
|
||||||
|
];
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "SupportHelper",
|
name: "SupportHelper",
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -44,6 +54,14 @@ export default definePlugin({
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
dependencies: ["CommandsAPI"],
|
dependencies: ["CommandsAPI"],
|
||||||
|
|
||||||
|
patches: [{
|
||||||
|
find: ".BEGINNING_DM.format",
|
||||||
|
replacement: {
|
||||||
|
match: /BEGINNING_DM\.format\(\{.+?\}\),(?=.{0,100}userId:(\i\.getRecipientId\(\)))/,
|
||||||
|
replace: "$& $self.ContributorDmWarningCard({ userId: $1 }),"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
|
||||||
commands: [{
|
commands: [{
|
||||||
name: "vencord-debug",
|
name: "vencord-debug",
|
||||||
description: "Send Vencord Debug info",
|
description: "Send Vencord Debug info",
|
||||||
|
@ -64,15 +82,13 @@ export default definePlugin({
|
||||||
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
|
const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
|
||||||
|
|
||||||
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
|
const enabledPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && !isApiPlugin(p));
|
||||||
const enabledApiPlugins = Object.keys(plugins).filter(p => Vencord.Plugins.isPluginEnabled(p) && isApiPlugin(p));
|
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
Vencord: `v${VERSION} • ${gitHash}${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
Vencord:
|
||||||
"Discord Branch": RELEASE_CHANNEL,
|
`v${VERSION} • [${gitHash}](<https://github.com/Vendicated/Vencord/commit/${gitHash}>)` +
|
||||||
Client: client,
|
`${settings.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
|
||||||
Platform: window.navigator.platform,
|
Client: `${RELEASE_CHANNEL} ~ ${client}`,
|
||||||
Outdated: isOutdated,
|
Platform: window.navigator.platform
|
||||||
OpenAsar: "openasar" in window,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IS_DISCORD_DESKTOP) {
|
if (IS_DISCORD_DESKTOP) {
|
||||||
|
@ -80,11 +96,10 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
|
|
||||||
const debugInfo = `
|
const debugInfo = `
|
||||||
**Vencord Debug Info**
|
>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}
|
||||||
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
|
|
||||||
|
|
||||||
Enabled Plugins (${enabledPlugins.length + enabledApiPlugins.length}):
|
Enabled Plugins (${enabledPlugins.length}):
|
||||||
${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "))}
|
${makeCodeblock(enabledPlugins.join(", "))}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -97,24 +112,75 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
|
||||||
async CHANNEL_SELECT({ channelId }) {
|
async CHANNEL_SELECT({ channelId }) {
|
||||||
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
if (channelId !== SUPPORT_CHANNEL_ID) return;
|
||||||
|
|
||||||
if (isPluginDev(UserStore.getCurrentUser().id)) return;
|
const selfId = UserStore.getCurrentUser()?.id;
|
||||||
|
if (!selfId || isPluginDev(selfId)) return;
|
||||||
|
|
||||||
if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) {
|
if (isOutdated) {
|
||||||
const rememberDismiss = () => DataStore.set(REMEMBER_DISMISS_KEY, gitHash);
|
return Alerts.show({
|
||||||
|
|
||||||
Alerts.show({
|
|
||||||
title: "Hold on!",
|
title: "Hold on!",
|
||||||
body: <div>
|
body: <div>
|
||||||
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
|
<Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
|
||||||
<Forms.FormText>
|
<Forms.FormText className={Margins.top8}>
|
||||||
Please first update using the Updater Page in Settings, or use the VencordInstaller (Update Vencord Button)
|
Please first update before asking for support!
|
||||||
to do so, in case you can't access the Updater page.
|
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
</div>,
|
</div>,
|
||||||
onCancel: rememberDismiss,
|
onCancel: () => openUpdaterModal!(),
|
||||||
onConfirm: rememberDismiss
|
cancelText: "View Updates",
|
||||||
|
confirmText: "Update & Restart Now",
|
||||||
|
async onConfirm() {
|
||||||
|
await update();
|
||||||
|
relaunch();
|
||||||
|
},
|
||||||
|
secondaryConfirmText: "I know what I'm doing or I can't update"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore outdated type
|
||||||
|
const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
|
||||||
|
if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
|
||||||
|
|
||||||
|
if (!IS_WEB && IS_UPDATER_DISABLED) {
|
||||||
|
return Alerts.show({
|
||||||
|
title: "Hold on!",
|
||||||
|
body: <div>
|
||||||
|
<Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>
|
||||||
|
<Forms.FormText className={Margins.top8}>
|
||||||
|
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
||||||
|
contact your package maintainer for support instead.
|
||||||
|
</Forms.FormText>
|
||||||
|
</div>,
|
||||||
|
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = await VencordNative.updater.getRepo();
|
||||||
|
if (repo.ok && !repo.value.includes("Vendicated/Vencord")) {
|
||||||
|
return Alerts.show({
|
||||||
|
title: "Hold on!",
|
||||||
|
body: <div>
|
||||||
|
<Forms.FormText>You are using a fork of Vencord, which we do not provide support for!</Forms.FormText>
|
||||||
|
<Forms.FormText className={Margins.top8}>
|
||||||
|
Please either switch to an <Link href="https://vencord.dev/download">officially supported version of Vencord</Link>, or
|
||||||
|
contact your package maintainer for support instead.
|
||||||
|
</Forms.FormText>
|
||||||
|
</div>,
|
||||||
|
onCloseCallback: () => setTimeout(() => NavigationRouter.back(), 50)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
ContributorDmWarningCard: ErrorBoundary.wrap(({ userId }) => {
|
||||||
|
if (!isPluginDev(userId)) return null;
|
||||||
|
if (RelationshipStore.isFriend(userId)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={`vc-plugins-restart-card ${Margins.top8}`}>
|
||||||
|
Please do not private message Vencord plugin developers for support!
|
||||||
|
<br />
|
||||||
|
Instead, use the Vencord support channel: {Parser.parse("https://discord.com/channels/1015060230222131221/1026515880080842772")}
|
||||||
|
{!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { FluxDispatcher, i18n } from "@webpack/common";
|
import { FluxDispatcher, i18n, useMemo } from "@webpack/common";
|
||||||
|
|
||||||
import FolderSideBar from "./FolderSideBar";
|
import FolderSideBar from "./FolderSideBar";
|
||||||
|
|
||||||
|
@ -117,8 +117,8 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
||||||
{
|
{
|
||||||
match: /(useStateFromStoresArray\).{0,25}let \i)=(\i\.\i.getGuildsTree\(\))/,
|
match: /\[(\i)\]=(\(0,\i\.useStateFromStoresArray\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
|
||||||
replace: (_, rest, guildsTree) => `${rest}=$self.getGuildTree(!!arguments[0].isBetterFolders,${guildsTree},arguments[0].betterFoldersExpandedIds)`
|
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0].isBetterFolders,betterFoldersOriginalTree,arguments[0].betterFoldersExpandedIds)`
|
||||||
},
|
},
|
||||||
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
||||||
{
|
{
|
||||||
|
@ -127,7 +127,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
|
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
|
||||||
{
|
{
|
||||||
match: /unreadMentionsIndicatorBottom,barClassName.+?}\)\]/,
|
match: /unreadMentionsIndicatorBottom,.+?}\)\]/,
|
||||||
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
|
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0].isBetterFolders))"
|
||||||
},
|
},
|
||||||
// Export the isBetterFolders variable to the folders component
|
// Export the isBetterFolders variable to the folders component
|
||||||
|
@ -252,19 +252,21 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getGuildTree(isBetterFolders: boolean, oldTree: any, expandedFolderIds?: Set<any>) {
|
getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set<any>) {
|
||||||
if (!isBetterFolders || expandedFolderIds == null) return oldTree;
|
return useMemo(() => {
|
||||||
|
if (!isBetterFolders || expandedFolderIds == null) return originalTree;
|
||||||
|
|
||||||
const newTree = new GuildsTree();
|
const newTree = new GuildsTree();
|
||||||
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
|
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
|
||||||
newTree.root.children = oldTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
|
newTree.root.children = originalTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
|
||||||
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
|
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
|
||||||
newTree.nodes = Object.fromEntries(
|
newTree.nodes = Object.fromEntries(
|
||||||
Object.entries(oldTree.nodes)
|
Object.entries(originalTree.nodes)
|
||||||
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
||||||
);
|
);
|
||||||
|
|
||||||
return newTree;
|
return newTree;
|
||||||
|
}, [isBetterFolders, originalTree, expandedFolderIds]);
|
||||||
},
|
},
|
||||||
|
|
||||||
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
||||||
|
@ -279,7 +281,7 @@ export default definePlugin({
|
||||||
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
||||||
return child => {
|
return child => {
|
||||||
if (isBetterFolders) {
|
if (isBetterFolders) {
|
||||||
return "onScroll" in child.props;
|
return child?.props?.onScroll != null;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { canonicalizeMatch } from "@utils/patches";
|
import { canonicalizeMatch } from "@utils/patches";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
@ -60,7 +61,7 @@ export default definePlugin({
|
||||||
find: ".popularApplicationCommandIds,",
|
find: ".popularApplicationCommandIds,",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /lastSection:(!?\i)}\),/,
|
match: /lastSection:(!?\i)}\),/,
|
||||||
replace: "$&$self.patchPadding($1),"
|
replace: "$&$self.patchPadding({lastSection:$1}),"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -80,10 +81,10 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
patchPadding(lastSection: any) {
|
patchPadding: ErrorBoundary.wrap(({ lastSection }) => {
|
||||||
if (!lastSection) return;
|
if (!lastSection) return null;
|
||||||
return (
|
return (
|
||||||
<div className={UserPopoutSectionCssClasses.lastSection}></div>
|
<div className={UserPopoutSectionCssClasses.lastSection} ></div>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findExportedComponentLazy, findStoreLazy } from "@webpack";
|
||||||
import { React, RestAPI, Tooltip } from "@webpack/common";
|
import { Constants, React, RestAPI, Tooltip } from "@webpack/common";
|
||||||
|
|
||||||
import { RenameButton } from "./components/RenameButton";
|
import { RenameButton } from "./components/RenameButton";
|
||||||
import { Session, SessionInfo } from "./types";
|
import { Session, SessionInfo } from "./types";
|
||||||
|
@ -168,7 +168,7 @@ export default definePlugin({
|
||||||
|
|
||||||
async checkNewSessions() {
|
async checkNewSessions() {
|
||||||
const data = await RestAPI.get({
|
const data = await RestAPI.get({
|
||||||
url: "/auth/sessions"
|
url: Constants.Endpoints.AUTH_SESSIONS
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const session of data.body.user_sessions) {
|
for (const session of data.body.user_sessions) {
|
||||||
|
|
|
@ -6,17 +6,18 @@
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { waitFor } from "@webpack";
|
||||||
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
|
import { ComponentDispatch, FocusLock, i18n, Menu, useEffect, useRef } from "@webpack/common";
|
||||||
import type { HTMLAttributes, ReactElement } from "react";
|
import type { HTMLAttributes, ReactElement } from "react";
|
||||||
|
|
||||||
type SettingsEntry = { section: string, label: string; };
|
type SettingsEntry = { section: string, label: string; };
|
||||||
|
|
||||||
const cl = classNameFactory("");
|
const cl = classNameFactory("");
|
||||||
const Classes = findByPropsLazy("animating", "baseLayer", "bg", "layer", "layers");
|
let Classes: Record<string, string>;
|
||||||
|
waitFor(["animating", "baseLayer", "bg", "layer", "layers"], m => Classes = m);
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
disableFade: {
|
disableFade: {
|
||||||
|
@ -118,18 +119,25 @@ export default definePlugin({
|
||||||
{ // Settings cog context menu
|
{ // Settings cog context menu
|
||||||
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
find: "Messages.USER_SETTINGS_ACTIONS_MENU_LABEL",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\(0,\i.default\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
match: /\(0,\i.useDefaultUserSettingsSections\)\(\)(?=\.filter\(\i=>\{let\{section:\i\}=)/,
|
||||||
replace: "$self.wrapMenu($&)"
|
replace: "$self.wrapMenu($&)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// This is the very outer layer of the entire ui, so we can't wrap this in an ErrorBoundary
|
||||||
|
// without possibly also catching unrelated errors of children.
|
||||||
|
//
|
||||||
|
// Thus, we sanity check webpack modules & do this really hacky try catch to hopefully prevent hard crashes if something goes wrong.
|
||||||
|
// try catch will only catch errors in the Layer function (hence why it's called as a plain function rather than a component), but
|
||||||
|
// not in children
|
||||||
Layer(props: LayerProps) {
|
Layer(props: LayerProps) {
|
||||||
return (
|
if (!FocusLock || !ComponentDispatch || !Classes) {
|
||||||
<ErrorBoundary fallback={() => props.children as any}>
|
new Logger("BetterSettings").error("Failed to find some components");
|
||||||
<Layer {...props} />
|
return props.children;
|
||||||
</ErrorBoundary>
|
}
|
||||||
);
|
|
||||||
|
return <Layer {...props} />;
|
||||||
},
|
},
|
||||||
|
|
||||||
wrapMenu(list: SettingsEntry[]) {
|
wrapMenu(list: SettingsEntry[]) {
|
||||||
|
|
|
@ -24,22 +24,20 @@ import { closeAllModals } from "@utils/modal";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { maybePromptToUpdate } from "@utils/updater";
|
import { maybePromptToUpdate } from "@utils/updater";
|
||||||
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
|
import { filters, findBulk, proxyLazyWebpack } from "@webpack";
|
||||||
import { FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
|
import { DraftType, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
|
||||||
|
|
||||||
const CrashHandlerLogger = new Logger("CrashHandler");
|
const CrashHandlerLogger = new Logger("CrashHandler");
|
||||||
const { ModalStack, DraftManager, DraftType, closeExpressionPicker } = proxyLazyWebpack(() => {
|
|
||||||
const modules = findBulk(
|
const { ModalStack, DraftManager, closeExpressionPicker } = proxyLazyWebpack(() => {
|
||||||
|
const [ModalStack, DraftManager, ExpressionManager] = findBulk(
|
||||||
filters.byProps("pushLazy", "popAll"),
|
filters.byProps("pushLazy", "popAll"),
|
||||||
filters.byProps("clearDraft", "saveDraft"),
|
filters.byProps("clearDraft", "saveDraft"),
|
||||||
filters.byProps("DraftType"),
|
filters.byProps("closeExpressionPicker", "openExpressionPicker"),);
|
||||||
filters.byProps("closeExpressionPicker", "openExpressionPicker"),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ModalStack: modules[0],
|
ModalStack,
|
||||||
DraftManager: modules[1],
|
DraftManager,
|
||||||
DraftType: modules[2]?.DraftType,
|
closeExpressionPicker: ExpressionManager?.closeExpressionPicker,
|
||||||
closeExpressionPicker: modules[3]?.closeExpressionPicker,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -104,7 +102,7 @@ export default definePlugin({
|
||||||
|
|
||||||
shouldAttemptRecover = false;
|
shouldAttemptRecover = false;
|
||||||
// This is enough to avoid a crash loop
|
// This is enough to avoid a crash loop
|
||||||
setTimeout(() => shouldAttemptRecover = true, 500);
|
setTimeout(() => shouldAttemptRecover = true, 1000);
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -137,8 +135,11 @@ export default definePlugin({
|
||||||
try {
|
try {
|
||||||
const channelId = SelectedChannelStore.getChannelId();
|
const channelId = SelectedChannelStore.getChannelId();
|
||||||
|
|
||||||
DraftManager.clearDraft(channelId, DraftType.ChannelMessage);
|
for (const key in DraftType) {
|
||||||
DraftManager.clearDraft(channelId, DraftType.FirstThreadMessage);
|
if (!Number.isNaN(Number(key))) continue;
|
||||||
|
|
||||||
|
DraftManager.clearDraft(channelId, DraftType[key]);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
CrashHandlerLogger.debug("Failed to clear drafts.", err);
|
CrashHandlerLogger.debug("Failed to clear drafts.", err);
|
||||||
}
|
}
|
||||||
|
|
68
src/plugins/ctrlEnterSend/index.ts
Normal file
68
src/plugins/ctrlEnterSend/index.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "CtrlEnterSend",
|
||||||
|
authors: [Devs.UlyssesZhan],
|
||||||
|
description: "Use Ctrl+Enter to send messages (customizable)",
|
||||||
|
settings: definePluginSettings({
|
||||||
|
submitRule: {
|
||||||
|
description: "The way to send a message",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Ctrl+Enter (Enter or Shift+Enter for new line)",
|
||||||
|
value: "ctrl+enter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Shift+Enter (Enter for new line)",
|
||||||
|
value: "shift+enter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Enter (Shift+Enter for new line; Discord default)",
|
||||||
|
value: "enter"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
default: "ctrl+enter"
|
||||||
|
},
|
||||||
|
sendMessageInTheMiddleOfACodeBlock: {
|
||||||
|
description: "Whether to send a message in the middle of a code block",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "KeyboardKeys.ENTER&&(!",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=(\i)\.which===\i\.KeyboardKeys.ENTER&&).{0,100}(\(0,\i\.hasOpenPlainTextCodeBlock\)\(\i\)).{0,100}(?=&&\(\i\.preventDefault)/,
|
||||||
|
replace: "$self.shouldSubmit($1, $2)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shouldSubmit(event: KeyboardEvent, codeblock: boolean): boolean {
|
||||||
|
let result = false;
|
||||||
|
switch (this.settings.store.submitRule) {
|
||||||
|
case "shift+enter":
|
||||||
|
result = event.shiftKey;
|
||||||
|
break;
|
||||||
|
case "ctrl+enter":
|
||||||
|
result = event.ctrlKey;
|
||||||
|
break;
|
||||||
|
case "enter":
|
||||||
|
result = !event.shiftKey && !event.ctrlKey;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!this.settings.store.sendMessageInTheMiddleOfACodeBlock) {
|
||||||
|
result &&= !codeblock;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
|
@ -17,13 +17,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings, Settings } from "@api/Settings";
|
import { definePluginSettings, Settings } from "@api/Settings";
|
||||||
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { isTruthy } from "@utils/guards";
|
import { isTruthy } from "@utils/guards";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import { classes } from "@utils/misc";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
|
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, StatusSettingsStores, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
|
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
|
||||||
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
|
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
|
||||||
|
@ -386,17 +389,36 @@ async function setRpc(disable?: boolean) {
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "CustomRPC",
|
name: "CustomRPC",
|
||||||
description: "Allows you to set a custom rich presence.",
|
description: "Allows you to set a custom rich presence.",
|
||||||
authors: [Devs.captain, Devs.AutumnVN],
|
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
|
||||||
start: setRpc,
|
start: setRpc,
|
||||||
stop: () => setRpc(true),
|
stop: () => setRpc(true),
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
settingsAboutComponent: () => {
|
settingsAboutComponent: () => {
|
||||||
const activity = useAwaiter(createActivity);
|
const activity = useAwaiter(createActivity);
|
||||||
|
const gameActivityEnabled = StatusSettingsStores.ShowCurrentGame.useSetting();
|
||||||
const { profileThemeStyle } = useProfileThemeStyle({});
|
const { profileThemeStyle } = useProfileThemeStyle({});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{!gameActivityEnabled && (
|
||||||
|
<ErrorCard
|
||||||
|
className={classes(Margins.top16, Margins.bottom16)}
|
||||||
|
style={{ padding: "1em" }}
|
||||||
|
>
|
||||||
|
<Forms.FormTitle>Notice</Forms.FormTitle>
|
||||||
|
<Forms.FormText>Game activity isn't enabled, people won't be able to see your custom rich presence!</Forms.FormText>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color={Button.Colors.TRANSPARENT}
|
||||||
|
className={Margins.top8}
|
||||||
|
onClick={() => StatusSettingsStores.ShowCurrentGame.updateSetting(true)}
|
||||||
|
>
|
||||||
|
Enable
|
||||||
|
</Button>
|
||||||
|
</ErrorCard>
|
||||||
|
)}
|
||||||
|
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
|
Go to <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
|
||||||
get the application ID.
|
get the application ID.
|
||||||
|
@ -407,7 +429,9 @@ export default definePlugin({
|
||||||
<Forms.FormText>
|
<Forms.FormText>
|
||||||
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address".
|
If you want to use image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and select "Copy image address".
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
<Forms.FormDivider />
|
|
||||||
|
<Forms.FormDivider className={Margins.top8} />
|
||||||
|
|
||||||
<div style={{ width: "284px", ...profileThemeStyle }}>
|
<div style={{ width: "284px", ...profileThemeStyle }}>
|
||||||
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
|
{activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
|
||||||
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
|
guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
|
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { Tooltip } from "@webpack/common";
|
import { Tooltip } from "@webpack/common";
|
||||||
import type { Component } from "react";
|
import type { Component } from "react";
|
||||||
|
|
||||||
|
@ -34,11 +35,19 @@ interface Props {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enum ReplaceElements {
|
||||||
|
ReplaceAllElements,
|
||||||
|
ReplaceTitlesOnly,
|
||||||
|
ReplaceThumbnailsOnly
|
||||||
|
}
|
||||||
|
|
||||||
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
|
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
|
||||||
|
|
||||||
async function embedDidMount(this: Component<Props>) {
|
async function embedDidMount(this: Component<Props>) {
|
||||||
try {
|
try {
|
||||||
const { embed } = this.props;
|
const { embed } = this.props;
|
||||||
|
const { replaceElements } = settings.store;
|
||||||
|
|
||||||
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
|
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
|
||||||
|
|
||||||
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
|
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
|
||||||
|
@ -58,12 +67,12 @@ async function embedDidMount(this: Component<Props>) {
|
||||||
enabled: true
|
enabled: true
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasTitle) {
|
if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) {
|
||||||
embed.dearrow.oldTitle = embed.rawTitle;
|
embed.dearrow.oldTitle = embed.rawTitle;
|
||||||
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
|
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasThumb) {
|
if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) {
|
||||||
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
|
||||||
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
|
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
|
||||||
}
|
}
|
||||||
|
@ -128,10 +137,30 @@ function DearrowButton({ component }: { component: Component<Props>; }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
hideButton: {
|
||||||
|
description: "Hides the Dearrow button from YouTube embeds",
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
default: false,
|
||||||
|
restartNeeded: true
|
||||||
|
},
|
||||||
|
replaceElements: {
|
||||||
|
description: "Choose which elements of the embed will be replaced",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
restartNeeded: true,
|
||||||
|
options: [
|
||||||
|
{ label: "Everything (Titles & Thumbnails)", value: ReplaceElements.ReplaceAllElements, default: true },
|
||||||
|
{ label: "Titles", value: ReplaceElements.ReplaceTitlesOnly },
|
||||||
|
{ label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "Dearrow",
|
name: "Dearrow",
|
||||||
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
|
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
|
settings,
|
||||||
|
|
||||||
embedDidMount,
|
embedDidMount,
|
||||||
renderButton(component: Component<Props>) {
|
renderButton(component: Component<Props>) {
|
||||||
|
@ -154,7 +183,8 @@ export default definePlugin({
|
||||||
// add dearrow button
|
// add dearrow button
|
||||||
{
|
{
|
||||||
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
|
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
|
||||||
replace: "children:[$self.renderButton(this),"
|
replace: "children:[$self.renderButton(this),",
|
||||||
|
predicate: () => !settings.store.hideButton
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}],
|
}],
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { Margins } from "@utils/margins";
|
||||||
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
|
import { ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
|
import { Constants, EmojiStore, FluxDispatcher, Forms, GuildStore, Menu, PermissionsBits, PermissionStore, React, RestAPI, Toasts, Tooltip, UserStore } from "@webpack/common";
|
||||||
import { Promisable } from "type-fest";
|
import { Promisable } from "type-fest";
|
||||||
|
|
||||||
const StickersStore = findStoreLazy("StickersStore");
|
const StickersStore = findStoreLazy("StickersStore");
|
||||||
|
@ -64,7 +64,7 @@ async function fetchSticker(id: string) {
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const { body } = await RestAPI.get({
|
const { body } = await RestAPI.get({
|
||||||
url: `/stickers/${id}`
|
url: Constants.Endpoints.STICKER(id)
|
||||||
});
|
});
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
|
@ -83,7 +83,7 @@ async function cloneSticker(guildId: string, sticker: Sticker) {
|
||||||
data.append("file", await fetchBlob(getUrl(sticker)));
|
data.append("file", await fetchBlob(getUrl(sticker)));
|
||||||
|
|
||||||
const { body } = await RestAPI.post({
|
const { body } = await RestAPI.post({
|
||||||
url: `/guilds/${guildId}/stickers`,
|
url: Constants.Endpoints.GUILD_STICKER_PACKS(guildId),
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -322,8 +322,9 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
|
||||||
switch (favoriteableType) {
|
switch (favoriteableType) {
|
||||||
case "emoji":
|
case "emoji":
|
||||||
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
|
const match = props.message.content.match(RegExp(`<a?:(\\w+)(?:~\\d+)?:${favoriteableId}>|https://cdn\\.discordapp\\.com/emojis/${favoriteableId}\\.`));
|
||||||
if (!match) return;
|
const reaction = props.message.reactions.find(reaction => reaction.emoji.id === favoriteableId);
|
||||||
const name = match[1] ?? "FakeNitroEmoji";
|
if (!match && !reaction) return;
|
||||||
|
const name = (match && match[1]) ?? reaction?.emoji.name ?? "FakeNitroEmoji";
|
||||||
|
|
||||||
return buildMenuItem("Emoji", () => ({
|
return buildMenuItem("Emoji", () => ({
|
||||||
id: favoriteableId,
|
id: favoriteableId,
|
||||||
|
|
|
@ -24,13 +24,12 @@ import { getCurrentGuild } from "@utils/discord";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
|
import { findByPropsLazy, findStoreLazy, proxyLazyWebpack } from "@webpack";
|
||||||
import { Alerts, ChannelStore, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
|
import { Alerts, ChannelStore, DraftType, EmojiStore, FluxDispatcher, Forms, IconUtils, lodash, Parser, PermissionsBits, PermissionStore, UploadHandler, UserSettingsActionCreators, UserStore } from "@webpack/common";
|
||||||
import type { CustomEmoji } from "@webpack/types";
|
import type { CustomEmoji } from "@webpack/types";
|
||||||
import type { Message } from "discord-types/general";
|
import type { Message } from "discord-types/general";
|
||||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
|
||||||
const StickerStore = findStoreLazy("StickersStore") as {
|
const StickerStore = findStoreLazy("StickersStore") as {
|
||||||
getPremiumPacks(): StickerPack[];
|
getPremiumPacks(): StickerPack[];
|
||||||
getAllGuildStickers(): Map<string, Sticker[]>;
|
getAllGuildStickers(): Map<string, Sticker[]>;
|
||||||
|
@ -39,6 +38,7 @@ const StickerStore = findStoreLazy("StickersStore") as {
|
||||||
|
|
||||||
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
|
||||||
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
|
const ProtoUtils = findByPropsLazy("BINARY_READ_OPTIONS");
|
||||||
|
const RoleSubscriptionEmojiUtils = findByPropsLazy("isUnusableRoleSubscriptionEmoji");
|
||||||
|
|
||||||
function searchProtoClassField(localName: string, protoClass: any) {
|
function searchProtoClassField(localName: string, protoClass: any) {
|
||||||
const field = protoClass?.fields?.find((field: any) => field.localName === localName);
|
const field = protoClass?.fields?.find((field: any) => field.localName === localName);
|
||||||
|
@ -111,7 +111,7 @@ const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
enableEmojiBypass: {
|
enableEmojiBypass: {
|
||||||
description: "Allow sending fake emojis",
|
description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
default: true,
|
default: true,
|
||||||
restartNeeded: true
|
restartNeeded: true
|
||||||
|
@ -129,7 +129,7 @@ const settings = definePluginSettings({
|
||||||
restartNeeded: true
|
restartNeeded: true
|
||||||
},
|
},
|
||||||
enableStickerBypass: {
|
enableStickerBypass: {
|
||||||
description: "Allow sending fake stickers",
|
description: "Allows sending fake stickers (also bypasses missing permission to use stickers)",
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
default: true,
|
default: true,
|
||||||
restartNeeded: true
|
restartNeeded: true
|
||||||
|
@ -190,7 +190,7 @@ const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, Permi
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "FakeNitro",
|
name: "FakeNitro",
|
||||||
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN],
|
||||||
description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
|
description: "Allows you to stream in nitro quality, send fake emojis/stickers, use client themes and custom Discord notifications.",
|
||||||
dependencies: ["MessageEventsAPI"],
|
dependencies: ["MessageEventsAPI"],
|
||||||
|
|
||||||
settings,
|
settings,
|
||||||
|
@ -408,6 +408,15 @@ export default definePlugin({
|
||||||
match: /canUseCustomNotificationSounds:function\(\i\){/,
|
match: /canUseCustomNotificationSounds:function\(\i\){/,
|
||||||
replace: "$&return true;"
|
replace: "$&return true;"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// Allows the usage of subscription-locked emojis
|
||||||
|
{
|
||||||
|
find: "isUnusableRoleSubscriptionEmoji:function",
|
||||||
|
replacement: {
|
||||||
|
match: /isUnusableRoleSubscriptionEmoji:function/,
|
||||||
|
// replace the original export with a func that always returns false and alias the original
|
||||||
|
replace: "isUnusableRoleSubscriptionEmoji:()=>()=>false,isUnusableRoleSubscriptionEmojiOriginal:function"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -797,13 +806,16 @@ export default definePlugin({
|
||||||
gif.finish();
|
gif.finish();
|
||||||
|
|
||||||
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
|
||||||
UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
|
UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DraftType.ChannelMessage);
|
||||||
},
|
},
|
||||||
|
|
||||||
canUseEmote(e: CustomEmoji, channelId: string) {
|
canUseEmote(e: CustomEmoji, channelId: string) {
|
||||||
if (e.require_colons === false) return true;
|
if (e.require_colons === false) return true;
|
||||||
if (e.available === false) return false;
|
if (e.available === false) return false;
|
||||||
|
|
||||||
|
const isUnusableRoleSubEmoji = RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmojiOriginal ?? RoleSubscriptionEmojiUtils.isUnusableRoleSubscriptionEmoji;
|
||||||
|
if (isUnusableRoleSubEmoji(e, this.guildId)) return false;
|
||||||
|
|
||||||
if (this.canUseEmotes)
|
if (this.canUseEmotes)
|
||||||
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
|
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);
|
||||||
else
|
else
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption,
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { RestAPI, UserStore } from "@webpack/common";
|
import { Constants, RestAPI, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
const FriendInvites = findByPropsLazy("createFriendInvite");
|
const FriendInvites = findByPropsLazy("createFriendInvite");
|
||||||
const { uuid4 } = findByPropsLazy("uuid4");
|
const { uuid4 } = findByPropsLazy("uuid4");
|
||||||
|
@ -58,7 +58,7 @@ export default definePlugin({
|
||||||
if (uses === 1) {
|
if (uses === 1) {
|
||||||
const random = uuid4();
|
const random = uuid4();
|
||||||
const { body: { invite_suggestions } } = await RestAPI.post({
|
const { body: { invite_suggestions } } = await RestAPI.post({
|
||||||
url: "/friend-finder/find-friends",
|
url: Constants.Endpoints.FRIEND_FINDER,
|
||||||
body: {
|
body: {
|
||||||
modified_contacts: {
|
modified_contacts: {
|
||||||
[random]: [1, "", ""]
|
[random]: [1, "", ""]
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
|
import { FluxDispatcher, React, useRef, useState } from "@webpack/common";
|
||||||
|
|
||||||
import { ELEMENT_ID } from "../constants";
|
import { ELEMENT_ID } from "../constants";
|
||||||
|
@ -36,7 +37,7 @@ export interface MagnifierProps {
|
||||||
|
|
||||||
const cl = classNameFactory("vc-imgzoom-");
|
const cl = classNameFactory("vc-imgzoom-");
|
||||||
|
|
||||||
export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSize, zoom: initalZoom }) => {
|
export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: initialSize, zoom: initalZoom }) => {
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
|
const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
|
||||||
|
@ -199,4 +200,4 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}, { noop: true });
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { registerCommand, unregisterCommand } from "@api/Commands";
|
||||||
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
|
import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
|
import { canonicalizeFind } from "@utils/patches";
|
||||||
import { Patch, Plugin, StartAt } from "@utils/types";
|
import { Patch, Plugin, StartAt } from "@utils/types";
|
||||||
import { FluxDispatcher } from "@webpack/common";
|
import { FluxDispatcher } from "@webpack/common";
|
||||||
import { FluxEvents } from "@webpack/types";
|
import { FluxEvents } from "@webpack/types";
|
||||||
|
@ -83,8 +84,12 @@ for (const p of pluginsValues) {
|
||||||
if (p.patches && isPluginEnabled(p.name)) {
|
if (p.patches && isPluginEnabled(p.name)) {
|
||||||
for (const patch of p.patches) {
|
for (const patch of p.patches) {
|
||||||
patch.plugin = p.name;
|
patch.plugin = p.name;
|
||||||
if (!Array.isArray(patch.replacement))
|
|
||||||
|
canonicalizeFind(patch);
|
||||||
|
if (!Array.isArray(patch.replacement)) {
|
||||||
patch.replacement = [patch.replacement];
|
patch.replacement = [patch.replacement];
|
||||||
|
}
|
||||||
|
|
||||||
patches.push(patch);
|
patches.push(patch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { getStegCloak } from "@utils/dependencies";
|
import { getStegCloak } from "@utils/dependencies";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
|
import { ChannelStore, Constants, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
import { buildDecModal } from "./components/DecryptionModal";
|
import { buildDecModal } from "./components/DecryptionModal";
|
||||||
|
@ -153,7 +153,7 @@ export default definePlugin({
|
||||||
// Gets the Embed of a Link
|
// Gets the Embed of a Link
|
||||||
async getEmbed(url: URL): Promise<Object | {}> {
|
async getEmbed(url: URL): Promise<Object | {}> {
|
||||||
const { body } = await RestAPI.post({
|
const { body } = await RestAPI.post({
|
||||||
url: "/unfurler/embed-urls",
|
url: Constants.Endpoints.UNFURL_EMBED_URLS,
|
||||||
body: {
|
body: {
|
||||||
urls: [url]
|
urls: [url]
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,8 @@ const enum NameFormat {
|
||||||
ArtistFirst = "artist-first",
|
ArtistFirst = "artist-first",
|
||||||
SongFirst = "song-first",
|
SongFirst = "song-first",
|
||||||
ArtistOnly = "artist",
|
ArtistOnly = "artist",
|
||||||
SongOnly = "song"
|
SongOnly = "song",
|
||||||
|
AlbumName = "album"
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationId = "1108588077900898414";
|
const applicationId = "1108588077900898414";
|
||||||
|
@ -147,6 +148,10 @@ const settings = definePluginSettings({
|
||||||
{
|
{
|
||||||
label: "Use song name only",
|
label: "Use song name only",
|
||||||
value: NameFormat.SongOnly
|
value: NameFormat.SongOnly
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Use album name (falls back to custom status text if song has no album)",
|
||||||
|
value: NameFormat.AlbumName
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -313,6 +318,8 @@ export default definePlugin({
|
||||||
return trackData.artist;
|
return trackData.artist;
|
||||||
case NameFormat.SongOnly:
|
case NameFormat.SongOnly:
|
||||||
return trackData.name;
|
return trackData.name;
|
||||||
|
case NameFormat.AlbumName:
|
||||||
|
return trackData.album || settings.store.statusName;
|
||||||
default:
|
default:
|
||||||
return settings.store.statusName;
|
return settings.store.statusName;
|
||||||
}
|
}
|
||||||
|
|
31
src/plugins/messageLatency/README.md
Normal file
31
src/plugins/messageLatency/README.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# MessageLatency
|
||||||
|
|
||||||
|
Displays an indicator for messages that took ≥n seconds to send.
|
||||||
|
|
||||||
|
> **NOTE**
|
||||||
|
>
|
||||||
|
> - This plugin only applies to messages received after opening the channel
|
||||||
|
> - False positives can exist if the user's system clock has drifted.
|
||||||
|
> - Grouped messages only display latency of the first message
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
### Chat View
|
||||||
|
|
||||||
|
![chat-view](https://github.com/Vendicated/Vencord/assets/82430093/69430881-60b3-422f-aa3d-c62953837566)
|
||||||
|
|
||||||
|
### Clock -ve Drift
|
||||||
|
|
||||||
|
![pissbot-on-top](https://github.com/Vendicated/Vencord/assets/82430093/d9248b66-e761-4872-8829-e8bf4fea6ec8)
|
||||||
|
|
||||||
|
### Clock +ve Drift
|
||||||
|
|
||||||
|
![dumb-ai](https://github.com/Vendicated/Vencord/assets/82430093/0e9783cf-51d5-4559-ae10-42399e7d4099)
|
||||||
|
|
||||||
|
### Connection Delay
|
||||||
|
|
||||||
|
![who-this](https://github.com/Vendicated/Vencord/assets/82430093/fd68873d-8630-42cc-a166-e9063d2718b2)
|
||||||
|
|
||||||
|
### Icons
|
||||||
|
|
||||||
|
![icons](https://github.com/Vendicated/Vencord/assets/82430093/17630bd9-44ee-4967-bcdf-3315eb6eca85)
|
193
src/plugins/messageLatency/index.tsx
Normal file
193
src/plugins/messageLatency/index.tsx
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { isNonNullish } from "@utils/guards";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { findExportedComponentLazy } from "@webpack";
|
||||||
|
import { SnowflakeUtils, Tooltip } from "@webpack/common";
|
||||||
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
type FillValue = ("status-danger" | "status-warning" | "status-positive" | "text-muted");
|
||||||
|
type Fill = [FillValue, FillValue, FillValue];
|
||||||
|
type DiffKey = keyof Diff;
|
||||||
|
|
||||||
|
interface Diff {
|
||||||
|
days: number,
|
||||||
|
hours: number,
|
||||||
|
minutes: number,
|
||||||
|
seconds: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DISCORD_KT_DELAY = 1471228.928;
|
||||||
|
const HiddenVisually = findExportedComponentLazy("HiddenVisually");
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "MessageLatency",
|
||||||
|
description: "Displays an indicator for messages that took ≥n seconds to send",
|
||||||
|
authors: [Devs.arHSM],
|
||||||
|
|
||||||
|
settings: definePluginSettings({
|
||||||
|
latency: {
|
||||||
|
type: OptionType.NUMBER,
|
||||||
|
description: "Threshold in seconds for latency indicator",
|
||||||
|
default: 2
|
||||||
|
},
|
||||||
|
detectDiscordKotlin: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Detect old Discord Android clients",
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "showCommunicationDisabledStyles",
|
||||||
|
replacement: {
|
||||||
|
match: /(message:(\i),avatar:\i,username:\(0,\i.jsxs\)\(\i.Fragment,\{children:\[)(\i&&)/,
|
||||||
|
replace: "$1$self.Tooltip()({ message: $2 }),$3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
stringDelta(delta: number) {
|
||||||
|
const diff: Diff = {
|
||||||
|
days: Math.round(delta / (60 * 60 * 24)),
|
||||||
|
hours: Math.round((delta / (60 * 60)) % 24),
|
||||||
|
minutes: Math.round((delta / (60)) % 60),
|
||||||
|
seconds: Math.round(delta % 60),
|
||||||
|
};
|
||||||
|
|
||||||
|
const str = (k: DiffKey) => diff[k] > 0 ? `${diff[k]} ${diff[k] > 1 ? k : k.substring(0, k.length - 1)}` : null;
|
||||||
|
const keys = Object.keys(diff) as DiffKey[];
|
||||||
|
|
||||||
|
const ts = keys.reduce((prev, k) => {
|
||||||
|
const s = str(k);
|
||||||
|
|
||||||
|
return prev + (
|
||||||
|
isNonNullish(s)
|
||||||
|
? (prev !== ""
|
||||||
|
? k === "seconds"
|
||||||
|
? " and "
|
||||||
|
: " "
|
||||||
|
: "") + s
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
}, "");
|
||||||
|
|
||||||
|
return ts || "0 seconds";
|
||||||
|
},
|
||||||
|
|
||||||
|
latencyTooltipData(message: Message) {
|
||||||
|
const { latency, detectDiscordKotlin } = this.settings.store;
|
||||||
|
const { id, nonce } = message;
|
||||||
|
|
||||||
|
// Message wasn't received through gateway
|
||||||
|
if (!isNonNullish(nonce)) return null;
|
||||||
|
|
||||||
|
let isDiscordKotlin = false;
|
||||||
|
let delta = Math.round((SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce)) / 1000);
|
||||||
|
|
||||||
|
// Old Discord Android clients have a delay of around 17 days
|
||||||
|
// This is a workaround for that
|
||||||
|
if (-delta >= DISCORD_KT_DELAY - 86400) { // One day of padding for good measure
|
||||||
|
isDiscordKotlin = detectDiscordKotlin;
|
||||||
|
delta += DISCORD_KT_DELAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thanks dziurwa (I hate you)
|
||||||
|
// This is when the user's clock is ahead
|
||||||
|
// Can't do anything if the clock is behind
|
||||||
|
const abs = Math.abs(delta);
|
||||||
|
const ahead = abs !== delta;
|
||||||
|
|
||||||
|
const stringDelta = abs >= latency ? this.stringDelta(abs) : null;
|
||||||
|
|
||||||
|
// Also thanks dziurwa
|
||||||
|
// 2 minutes
|
||||||
|
const TROLL_LIMIT = 2 * 60;
|
||||||
|
|
||||||
|
const fill: Fill = isDiscordKotlin
|
||||||
|
? ["status-positive", "status-positive", "text-muted"]
|
||||||
|
: delta >= TROLL_LIMIT || ahead
|
||||||
|
? ["text-muted", "text-muted", "text-muted"]
|
||||||
|
: delta >= (latency * 2)
|
||||||
|
? ["status-danger", "text-muted", "text-muted"]
|
||||||
|
: ["status-warning", "status-warning", "text-muted"];
|
||||||
|
|
||||||
|
return (abs >= latency || isDiscordKotlin) ? { delta: stringDelta, ahead, fill, isDiscordKotlin } : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
Tooltip() {
|
||||||
|
return ErrorBoundary.wrap(({ message }: { message: Message; }) => {
|
||||||
|
const d = this.latencyTooltipData(message);
|
||||||
|
|
||||||
|
if (!isNonNullish(d)) return null;
|
||||||
|
|
||||||
|
let text: string;
|
||||||
|
if (!d.delta) {
|
||||||
|
text = "User is suspected to be on an old Discord Android client";
|
||||||
|
} else {
|
||||||
|
text = (d.ahead ? `This user's clock is ${d.delta} ahead.` : `This message was sent with a delay of ${d.delta}.`) + (d.isDiscordKotlin ? " User is suspected to be on an old Discord Android client." : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Tooltip
|
||||||
|
text={text}
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
props => <>
|
||||||
|
{<this.Icon delta={d.delta} fill={d.fill} props={props} />}
|
||||||
|
{/* Time Out indicator uses this, I think this is for a11y */}
|
||||||
|
<HiddenVisually>Delayed Message</HiddenVisually>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</Tooltip>;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
Icon({ delta, fill, props }: {
|
||||||
|
delta: string | null;
|
||||||
|
fill: Fill,
|
||||||
|
props: {
|
||||||
|
onClick(): void;
|
||||||
|
onMouseEnter(): void;
|
||||||
|
onMouseLeave(): void;
|
||||||
|
onContextMenu(): void;
|
||||||
|
onFocus(): void;
|
||||||
|
onBlur(): void;
|
||||||
|
"aria-label"?: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return <svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
role="img"
|
||||||
|
fill="none"
|
||||||
|
style={{ marginRight: "8px", verticalAlign: -1 }}
|
||||||
|
aria-label={delta ?? "Old Discord Android client"}
|
||||||
|
aria-hidden="false"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill={`var(--${fill[0]})`}
|
||||||
|
d="M4.8001 12C4.8001 11.5576 4.51344 11.2 4.16023 11.2H2.23997C1.88676 11.2 1.6001 11.5576 1.6001 12V13.6C1.6001 14.0424 1.88676 14.4 2.23997 14.4H4.15959C4.5128 14.4 4.79946 14.0424 4.79946 13.6L4.8001 12Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill={`var(--${fill[1]})`}
|
||||||
|
d="M9.6001 7.12724C9.6001 6.72504 9.31337 6.39998 8.9601 6.39998H7.0401C6.68684 6.39998 6.40011 6.72504 6.40011 7.12724V13.6727C6.40011 14.0749 6.68684 14.4 7.0401 14.4H8.9601C9.31337 14.4 9.6001 14.0749 9.6001 13.6727V7.12724Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill={`var(--${fill[2]})`}
|
||||||
|
d="M14.4001 2.31109C14.4001 1.91784 14.1134 1.59998 13.7601 1.59998H11.8401C11.4868 1.59998 11.2001 1.91784 11.2001 2.31109V13.6888C11.2001 14.0821 11.4868 14.4 11.8401 14.4H13.7601C14.1134 14.4 14.4001 14.0821 14.4001 13.6888V2.31109Z"
|
||||||
|
/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
});
|
|
@ -27,6 +27,7 @@ import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ChannelStore,
|
ChannelStore,
|
||||||
|
Constants,
|
||||||
FluxDispatcher,
|
FluxDispatcher,
|
||||||
GuildStore,
|
GuildStore,
|
||||||
IconUtils,
|
IconUtils,
|
||||||
|
@ -132,7 +133,7 @@ async function fetchMessage(channelID: string, messageID: string) {
|
||||||
messageCache.set(messageID, { fetched: false });
|
messageCache.set(messageID, { fetched: false });
|
||||||
|
|
||||||
const res = await RestAPI.get({
|
const res = await RestAPI.get({
|
||||||
url: `/channels/${channelID}/messages`,
|
url: Constants.Endpoints.MESSAGES(channelID),
|
||||||
query: {
|
query: {
|
||||||
limit: 1,
|
limit: 1,
|
||||||
around: messageID
|
around: messageID
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
.messagelogger-deleted {
|
.messagelogger-deleted {
|
||||||
background-color: rgba(240 71 71 / 15%) !important;
|
background-color: hsla(var(--red-430-hsl, 0 85% 61%) / 15%) !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
/* Message content highlighting */
|
/* Message content highlighting */
|
||||||
.messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) {
|
.messagelogger-deleted [class*="contents"] > :is(div, h1, h2, h3, p) {
|
||||||
color: #f04747 !important;
|
color: var(--status-danger, #f04747) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bot "thinking" text highlighting */
|
/* Bot "thinking" text highlighting */
|
||||||
.messagelogger-deleted [class*="colorStandard"] {
|
.messagelogger-deleted [class*="colorStandard"] {
|
||||||
color: #f04747 !important;
|
color: var(--status-danger, #f04747) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Embed highlighting */
|
/* Embed highlighting */
|
||||||
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
|
.messagelogger-deleted article :is(div, span, h1, h2, h3, p) {
|
||||||
color: #f04747 !important;
|
color: var(--status-danger, #f04747) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messagelogger-deleted a {
|
.messagelogger-deleted a {
|
||||||
color: #be3535 !important;
|
color: var(--red-460, #be3535) !important;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
|
@ -255,7 +255,7 @@ export default definePlugin({
|
||||||
replace: "$1" +
|
replace: "$1" +
|
||||||
".update($3,m =>" +
|
".update($3,m =>" +
|
||||||
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message, true)) ? m :" +
|
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message, true)) ? m :" +
|
||||||
" $2.message.content !== m.content ?" +
|
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
|
||||||
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
|
||||||
" m" +
|
" m" +
|
||||||
")" +
|
")" +
|
||||||
|
|
|
@ -50,6 +50,7 @@ interface TagSettings {
|
||||||
MODERATOR_STAFF: TagSetting,
|
MODERATOR_STAFF: TagSetting,
|
||||||
MODERATOR: TagSetting,
|
MODERATOR: TagSetting,
|
||||||
VOICE_MODERATOR: TagSetting,
|
VOICE_MODERATOR: TagSetting,
|
||||||
|
TRIAL_MODERATOR: TagSetting,
|
||||||
[k: string]: TagSetting;
|
[k: string]: TagSetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +94,11 @@ const tags: Tag[] = [
|
||||||
displayName: "VC Mod",
|
displayName: "VC Mod",
|
||||||
description: "Can manage voice chats",
|
description: "Can manage voice chats",
|
||||||
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
|
permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"]
|
||||||
|
}, {
|
||||||
|
name: "CHAT_MODERATOR",
|
||||||
|
displayName: "Chat Mod",
|
||||||
|
description: "Can timeout people",
|
||||||
|
permissions: ["MODERATE_MEMBERS"]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const defaultSettings = Object.fromEntries(
|
const defaultSettings = Object.fromEntries(
|
||||||
|
@ -263,34 +269,14 @@ export default definePlugin({
|
||||||
],
|
],
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (settings.store.tagSettings) return;
|
settings.store.tagSettings ??= defaultSettings;
|
||||||
// @ts-ignore
|
|
||||||
if (!settings.store.visibility_WEBHOOK) settings.store.tagSettings = defaultSettings;
|
// newly added field might be missing from old users
|
||||||
else {
|
settings.store.tagSettings.CHAT_MODERATOR ??= {
|
||||||
const newSettings = { ...defaultSettings };
|
text: "Chat Mod",
|
||||||
Object.entries(Vencord.PlainSettings.plugins.MoreUserTags).forEach(([name, value]) => {
|
showInChat: true,
|
||||||
const [setting, tag] = name.split("_");
|
showInNotChat: true
|
||||||
if (setting === "visibility") {
|
};
|
||||||
switch (value) {
|
|
||||||
case "always":
|
|
||||||
// its the default
|
|
||||||
break;
|
|
||||||
case "chat":
|
|
||||||
newSettings[tag].showInNotChat = false;
|
|
||||||
break;
|
|
||||||
case "not-chat":
|
|
||||||
newSettings[tag].showInChat = false;
|
|
||||||
break;
|
|
||||||
case "never":
|
|
||||||
newSettings[tag].showInChat = false;
|
|
||||||
newSettings[tag].showInNotChat = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settings.store.tagSettings = newSettings;
|
|
||||||
delete Vencord.Settings.plugins.MoreUserTags[name];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getPermissions(user: User, channel: Channel): string[] {
|
getPermissions(user: User, channel: Channel): string[] {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { isNonNullish } from "@utils/guards";
|
import { isNonNullish } from "@utils/guards";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
@ -55,12 +56,12 @@ export default definePlugin({
|
||||||
find: ".UserProfileSections.USER_INFO_CONNECTIONS:",
|
find: ".UserProfileSections.USER_INFO_CONNECTIONS:",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
|
match: /(?<={user:(\i),onClose:(\i)}\);)(?=case \i\.\i\.MUTUAL_FRIENDS)/,
|
||||||
replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs($1,$2);"
|
replace: "case \"MUTUAL_GDMS\":return $self.renderMutualGDMs({user: $1, onClose: $2});"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
renderMutualGDMs(user: User, onClose: () => void) {
|
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
|
||||||
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
|
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
|
||||||
<Clickable
|
<Clickable
|
||||||
className={ProfileListClasses.listRow}
|
className={ProfileListClasses.listRow}
|
||||||
|
@ -99,5 +100,5 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
</ScrollerThin>
|
</ScrollerThin>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
});
|
});
|
||||||
|
|
50
src/plugins/noServerEmojis/index.ts
Normal file
50
src/plugins/noServerEmojis/index.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
shownEmojis: {
|
||||||
|
description: "The types of emojis to show in the autocomplete menu.",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
default: "onlyUnicode",
|
||||||
|
options: [
|
||||||
|
{ label: "Only unicode emojis", value: "onlyUnicode" },
|
||||||
|
{ label: "Unicode emojis and server emojis from current server", value: "currentServer" },
|
||||||
|
{ label: "Unicode emojis and all server emojis (Discord default)", value: "all" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "NoServerEmojis",
|
||||||
|
authors: [Devs.UlyssesZhan],
|
||||||
|
description: "Do not show server emojis in the autocomplete menu.",
|
||||||
|
settings,
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "}searchWithoutFetchingLatest(",
|
||||||
|
replacement: {
|
||||||
|
match: /searchWithoutFetchingLatest.{20,300}get\((\i).{10,40}?reduce\(\((\i),(\i)\)=>\{/,
|
||||||
|
replace: "$& if ($self.shouldSkip($1, $3)) return $2;"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shouldSkip(guildId: string, emoji: any) {
|
||||||
|
if (emoji.type !== "GUILD_EMOJI") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (settings.store.shownEmojis === "onlyUnicode") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (settings.store.shownEmojis === "currentServer") {
|
||||||
|
return emoji.guildId !== guildId;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
|
@ -26,6 +26,7 @@ const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||||
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
|
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
|
||||||
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
|
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
|
||||||
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
|
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
|
||||||
|
const TidalMatcher = /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/;
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
spotify: {
|
spotify: {
|
||||||
|
@ -42,6 +43,11 @@ const settings = definePluginSettings({
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Open Epic Games links in the Epic Games Launcher",
|
description: "Open Epic Games links in the Epic Games Launcher",
|
||||||
default: true,
|
default: true,
|
||||||
|
},
|
||||||
|
tidal: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Open Tidal links in the Tidal app",
|
||||||
|
default: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,7 +55,7 @@ const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof impo
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "OpenInApp",
|
name: "OpenInApp",
|
||||||
description: "Open Spotify, Steam and Epic Games URLs in their respective apps instead of your browser",
|
description: "Open Spotify, Tidal, Steam and Epic Games URLs in their respective apps instead of your browser",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
|
@ -127,6 +133,19 @@ export default definePlugin({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tidal: {
|
||||||
|
if (!settings.store.tidal) break tidal;
|
||||||
|
|
||||||
|
const match = TidalMatcher.exec(url);
|
||||||
|
if (!match) break tidal;
|
||||||
|
|
||||||
|
const [, type, id] = match;
|
||||||
|
VencordNative.native.openExternal(`tidal://${type}/${id}`);
|
||||||
|
|
||||||
|
event?.preventDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// in case short url didn't end up being something we can handle
|
// in case short url didn't end up being something we can handle
|
||||||
if (event?.defaultPrevented) {
|
if (event?.defaultPrevented) {
|
||||||
window.open(url, "_blank");
|
window.open(url, "_blank");
|
||||||
|
|
|
@ -16,14 +16,30 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { GuildStore, RestAPI } from "@webpack/common";
|
import { Constants, GuildStore, i18n, RestAPI } from "@webpack/common";
|
||||||
|
|
||||||
const Messages = findByPropsLazy("GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION");
|
|
||||||
const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment");
|
const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment");
|
||||||
|
|
||||||
|
function showDisableInvites(guildId: string) {
|
||||||
|
// Once the experiment is removed, this should keep working
|
||||||
|
const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
|
||||||
|
// @ts-ignore
|
||||||
|
return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableInvites(guildId: string) {
|
||||||
|
const guild = GuildStore.getGuild(guildId);
|
||||||
|
const features = [...guild.features, "INVITES_DISABLED"];
|
||||||
|
RestAPI.patch({
|
||||||
|
url: Constants.Endpoints.GUILD(guildId),
|
||||||
|
body: { features },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "PauseInvitesForever",
|
name: "PauseInvitesForever",
|
||||||
tags: ["DisableInvitesForever"],
|
tags: ["DisableInvitesForever"],
|
||||||
|
@ -33,42 +49,29 @@ export default definePlugin({
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION",
|
find: "Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION",
|
||||||
replacement: [{
|
group: true,
|
||||||
match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/,
|
replacement: [
|
||||||
replace: "children: $self.renderInvitesLabel(arguments[0].guildId, setChecked)",
|
{
|
||||||
},
|
match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/,
|
||||||
{
|
replace: "children: $self.renderInvitesLabel({guildId:arguments[0].guildId,setChecked})",
|
||||||
match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/,
|
},
|
||||||
replace: "$1,setChecked=$2"
|
{
|
||||||
}]
|
match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/,
|
||||||
|
replace: "$1,setChecked=$2"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
showDisableInvites(guildId: string) {
|
renderInvitesLabel: ErrorBoundary.wrap(({ guildId, setChecked }) => {
|
||||||
// Once the experiment is removed, this should keep working
|
|
||||||
const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
|
|
||||||
// @ts-ignore
|
|
||||||
return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
|
|
||||||
},
|
|
||||||
|
|
||||||
disableInvites(guildId: string) {
|
|
||||||
const guild = GuildStore.getGuild(guildId);
|
|
||||||
const features = [...guild.features, "INVITES_DISABLED"];
|
|
||||||
RestAPI.patch({
|
|
||||||
url: `/guilds/${guild.id}`,
|
|
||||||
body: { features },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renderInvitesLabel(guildId: string, setChecked: Function) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION}
|
{i18n.Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION}
|
||||||
{this.showDisableInvites(guildId) && <a role="button" onClick={() => {
|
{showDisableInvites(guildId) && <a role="button" onClick={() => {
|
||||||
setChecked(true);
|
setChecked(true);
|
||||||
this.disableInvites(guildId);
|
disableInvites(guildId);
|
||||||
}}> Pause Indefinitely.</a>}
|
}}> Pause Indefinitely.</a>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { Flex } from "@components/Flex";
|
||||||
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
||||||
import { getUniqueUsername } from "@utils/discord";
|
import { getUniqueUsername } from "@utils/discord";
|
||||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
|
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, Text, Tooltip, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||||
import type { Guild } from "discord-types/general";
|
import type { Guild } from "discord-types/general";
|
||||||
|
|
||||||
import { settings } from "..";
|
import { settings } from "..";
|
||||||
|
@ -112,7 +112,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
||||||
<div
|
<div
|
||||||
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
|
className={cl("perms-list-item", { "perms-list-item-active": selectedItemIndex === index })}
|
||||||
onContextMenu={e => {
|
onContextMenu={e => {
|
||||||
if ((settings.store as any).unsafeViewAsRole && permission.type === PermissionType.Role)
|
if (permission.type === PermissionType.Role)
|
||||||
ContextMenuApi.openContextMenu(e, () => (
|
ContextMenuApi.openContextMenu(e, () => (
|
||||||
<RoleContextMenu
|
<RoleContextMenu
|
||||||
guild={guild}
|
guild={guild}
|
||||||
|
@ -120,6 +120,14 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
||||||
onClose={modalProps.onClose}
|
onClose={modalProps.onClose}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
else if (permission.type === PermissionType.User) {
|
||||||
|
ContextMenuApi.openContextMenu(e, () => (
|
||||||
|
<UserContextMenu
|
||||||
|
userId={permission.id!}
|
||||||
|
onClose={modalProps.onClose}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
|
{(permission.type === PermissionType.Role || permission.type === PermissionType.Owner) && (
|
||||||
|
@ -200,24 +208,53 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
||||||
aria-label="Role Options"
|
aria-label="Role Options"
|
||||||
>
|
>
|
||||||
<Menu.MenuItem
|
<Menu.MenuItem
|
||||||
id="vc-pw-view-as-role"
|
id="vc-copy-role-id"
|
||||||
label="View As Role"
|
label={i18n.Messages.COPY_ID_ROLE}
|
||||||
action={() => {
|
action={() => {
|
||||||
const role = GuildStore.getRole(guild.id, roleId);
|
Clipboard.copy(roleId);
|
||||||
if (!role) return;
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
onClose();
|
{(settings.store as any).unsafeViewAsRole && (
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-pw-view-as-role"
|
||||||
|
label={i18n.Messages.VIEW_AS_ROLE}
|
||||||
|
action={() => {
|
||||||
|
const role = GuildStore.getRole(guild.id, roleId);
|
||||||
|
if (!role) return;
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
onClose();
|
||||||
type: "IMPERSONATE_UPDATE",
|
|
||||||
guildId: guild.id,
|
FluxDispatcher.dispatch({
|
||||||
data: {
|
type: "IMPERSONATE_UPDATE",
|
||||||
type: "ROLES",
|
guildId: guild.id,
|
||||||
roles: {
|
data: {
|
||||||
[roleId]: role
|
type: "ROLES",
|
||||||
|
roles: {
|
||||||
|
[roleId]: role
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Menu.Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserContextMenu({ userId, onClose }: { userId: string; onClose: () => void; }) {
|
||||||
|
return (
|
||||||
|
<Menu.Menu
|
||||||
|
navId={cl("user-context-menu")}
|
||||||
|
onClose={ContextMenuApi.closeContextMenu}
|
||||||
|
aria-label="User Options"
|
||||||
|
>
|
||||||
|
<Menu.MenuItem
|
||||||
|
id="vc-copy-user-id"
|
||||||
|
label={i18n.Messages.COPY_ID_USER}
|
||||||
|
action={() => {
|
||||||
|
Clipboard.copy(userId);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Menu.Menu>
|
</Menu.Menu>
|
||||||
|
|
|
@ -21,10 +21,9 @@ import { Devs } from "@utils/constants";
|
||||||
import { makeLazy } from "@utils/lazy";
|
import { makeLazy } from "@utils/lazy";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { UploadHandler, UserUtils } from "@webpack/common";
|
import { DraftType, UploadHandler, UploadManager, UserUtils } from "@webpack/common";
|
||||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||||
|
|
||||||
const DRAFT_TYPE = 0;
|
|
||||||
const DEFAULT_DELAY = 20;
|
const DEFAULT_DELAY = 20;
|
||||||
const DEFAULT_RESOLUTION = 128;
|
const DEFAULT_RESOLUTION = 128;
|
||||||
const FRAMES = 10;
|
const FRAMES = 10;
|
||||||
|
@ -59,9 +58,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
|
||||||
for (const opt of options) {
|
for (const opt of options) {
|
||||||
switch (opt.name) {
|
switch (opt.name) {
|
||||||
case "image":
|
case "image":
|
||||||
const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0];
|
const upload = UploadStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand);
|
||||||
if (upload) {
|
if (upload) {
|
||||||
if (!upload.isImage) throw "Upload is not an image";
|
if (!upload.isImage) {
|
||||||
|
UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
|
||||||
|
throw "Upload is not an image";
|
||||||
|
}
|
||||||
return upload.item.file;
|
return upload.item.file;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -73,10 +75,12 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
|
||||||
return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048");
|
return user.getAvatarURL(noServerPfp ? void 0 : ctx.guild?.id, 2048).replace(/\?size=\d+$/, "?size=2048");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[petpet] Failed to fetch user\n", err);
|
console.error("[petpet] Failed to fetch user\n", err);
|
||||||
|
UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
|
||||||
throw "Failed to fetch user. Check the console for more info.";
|
throw "Failed to fetch user. Check the console for more info.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,6 +134,7 @@ export default definePlugin({
|
||||||
var url = await resolveImage(opts, cmdCtx, noServerPfp);
|
var url = await resolveImage(opts, cmdCtx, noServerPfp);
|
||||||
if (!url) throw "No Image specified!";
|
if (!url) throw "No Image specified!";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
|
||||||
sendBotMessage(cmdCtx.channel.id, {
|
sendBotMessage(cmdCtx.channel.id, {
|
||||||
content: String(err),
|
content: String(err),
|
||||||
});
|
});
|
||||||
|
@ -147,6 +152,8 @@ export default definePlugin({
|
||||||
canvas.width = canvas.height = resolution;
|
canvas.width = canvas.height = resolution;
|
||||||
const ctx = canvas.getContext("2d")!;
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand);
|
||||||
|
|
||||||
for (let i = 0; i < FRAMES; i++) {
|
for (let i = 0; i < FRAMES; i++) {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
@ -174,7 +181,7 @@ export default definePlugin({
|
||||||
const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" });
|
const file = new File([gif.bytesView()], "petpet.gif", { type: "image/gif" });
|
||||||
// Immediately after the command finishes, Discord clears all input, including pending attachments.
|
// Immediately after the command finishes, Discord clears all input, including pending attachments.
|
||||||
// Thus, setTimeout is needed to make this execute after Discord cleared the input
|
// Thus, setTimeout is needed to make this execute after Discord cleared the input
|
||||||
setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DRAFT_TYPE), 10);
|
setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -83,7 +83,7 @@ export default definePlugin({
|
||||||
// Rendering
|
// Rendering
|
||||||
{
|
{
|
||||||
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
|
match: /"renderRow",(\i)=>{(?<="renderDM",.+?(\i\.default),\{channel:.+?)/,
|
||||||
replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2);"
|
replace: "$&if($self.isChannelIndex($1.section, $1.row))return $self.renderChannel($1.section,$1.row,$2)();"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: /"renderSection",(\i)=>{/,
|
match: /"renderSection",(\i)=>{/,
|
||||||
|
@ -320,25 +320,26 @@ export default definePlugin({
|
||||||
</svg>
|
</svg>
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
}),
|
}, { noop: true }),
|
||||||
|
|
||||||
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
|
renderChannel(sectionIndex: number, index: number, ChannelComponent: React.ComponentType<ChannelComponentProps>) {
|
||||||
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
|
return ErrorBoundary.wrap(() => {
|
||||||
|
const { channel, category } = this.getChannel(sectionIndex, index, this.instance.props.channels);
|
||||||
|
|
||||||
if (!channel || !category) return null;
|
if (!channel || !category) return null;
|
||||||
if (this.isChannelHidden(sectionIndex, index)) return null;
|
if (this.isChannelHidden(sectionIndex, index)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChannelComponent
|
<ChannelComponent
|
||||||
channel={channel}
|
channel={channel}
|
||||||
selected={this.instance.props.selectedChannelId === channel.id}
|
selected={this.instance.props.selectedChannelId === channel.id}
|
||||||
>
|
>
|
||||||
{channel.id}
|
{channel.id}
|
||||||
</ChannelComponent>
|
</ChannelComponent>
|
||||||
);
|
);
|
||||||
|
}, { noop: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
|
getChannel(sectionIndex: number, index: number, channels: Record<string, Channel>) {
|
||||||
const category = categories[sectionIndex - 1];
|
const category = categories[sectionIndex - 1];
|
||||||
if (!category) return { channel: null, category: null };
|
if (!category) return { channel: null, category: null };
|
||||||
|
|
|
@ -33,7 +33,7 @@ const PRONOUN_TOOLTIP_PATCH = {
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "PronounDB",
|
name: "PronounDB",
|
||||||
authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven],
|
authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven, Devs.Elvyra],
|
||||||
description: "Adds pronouns to user messages using pronoundb",
|
description: "Adds pronouns to user messages using pronoundb",
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { useAwaiter } from "@utils/react";
|
||||||
import { UserProfileStore, UserStore } from "@webpack/common";
|
import { UserProfileStore, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import { settings } from "./settings";
|
import { settings } from "./settings";
|
||||||
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
|
import { CachePronouns, PronounCode, PronounMapping, PronounsResponse } from "./types";
|
||||||
|
|
||||||
type PronounsWithSource = [string | null, string];
|
type PronounsWithSource = [string | null, string];
|
||||||
const EmptyPronouns: PronounsWithSource = [null, ""];
|
const EmptyPronouns: PronounsWithSource = [null, ""];
|
||||||
|
@ -40,9 +40,9 @@ export const enum PronounSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
// A map of cached pronouns so the same request isn't sent twice
|
// A map of cached pronouns so the same request isn't sent twice
|
||||||
const cache: Record<string, PronounCode> = {};
|
const cache: Record<string, CachePronouns> = {};
|
||||||
// A map of ids and callbacks that should be triggered on fetch
|
// A map of ids and callbacks that should be triggered on fetch
|
||||||
const requestQueue: Record<string, ((pronouns: PronounCode) => void)[]> = {};
|
const requestQueue: Record<string, ((pronouns: string) => void)[]> = {};
|
||||||
|
|
||||||
// Executes all queued requests and calls their callbacks
|
// Executes all queued requests and calls their callbacks
|
||||||
const bulkFetch = debounce(async () => {
|
const bulkFetch = debounce(async () => {
|
||||||
|
@ -50,7 +50,7 @@ const bulkFetch = debounce(async () => {
|
||||||
const pronouns = await bulkFetchPronouns(ids);
|
const pronouns = await bulkFetchPronouns(ids);
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
// Call all callbacks for the id
|
// Call all callbacks for the id
|
||||||
requestQueue[id]?.forEach(c => c(pronouns[id]));
|
requestQueue[id]?.forEach(c => c(pronouns[id] ? extractPronouns(pronouns[id].sets) : ""));
|
||||||
delete requestQueue[id];
|
delete requestQueue[id];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -78,8 +78,8 @@ export function useFormattedPronouns(id: string, useGlobalProfile: boolean = fal
|
||||||
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
|
if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns)
|
||||||
return [discordPronouns, "Discord"];
|
return [discordPronouns, "Discord"];
|
||||||
|
|
||||||
if (result && result !== "unspecified")
|
if (result && result !== PronounMapping.unspecified)
|
||||||
return [formatPronouns(result), "PronounDB"];
|
return [result, "PronounDB"];
|
||||||
|
|
||||||
return [discordPronouns, "Discord"];
|
return [discordPronouns, "Discord"];
|
||||||
}
|
}
|
||||||
|
@ -98,8 +98,9 @@ const NewLineRe = /\n+/g;
|
||||||
|
|
||||||
// Gets the cached pronouns, if you're too impatient for a promise!
|
// Gets the cached pronouns, if you're too impatient for a promise!
|
||||||
export function getCachedPronouns(id: string): string | null {
|
export function getCachedPronouns(id: string): string | null {
|
||||||
const cached = cache[id];
|
const cached = cache[id] ? extractPronouns(cache[id].sets) : undefined;
|
||||||
if (cached && cached !== "unspecified") return cached;
|
|
||||||
|
if (cached && cached !== PronounMapping.unspecified) return cached;
|
||||||
|
|
||||||
return cached || null;
|
return cached || null;
|
||||||
}
|
}
|
||||||
|
@ -125,7 +126,7 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
|
||||||
params.append("ids", ids.join(","));
|
params.append("ids", ids.join(","));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const req = await fetch("https://pronoundb.org/api/v1/lookup-bulk?" + params.toString(), {
|
const req = await fetch("https://pronoundb.org/api/v2/lookup?" + params.toString(), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
|
@ -140,21 +141,24 @@ async function bulkFetchPronouns(ids: string[]): Promise<PronounsResponse> {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If the request errors, treat it as if no pronouns were found for all ids, and log it
|
// If the request errors, treat it as if no pronouns were found for all ids, and log it
|
||||||
console.error("PronounDB fetching failed: ", e);
|
console.error("PronounDB fetching failed: ", e);
|
||||||
const dummyPronouns = Object.fromEntries(ids.map(id => [id, "unspecified"] as const));
|
const dummyPronouns = Object.fromEntries(ids.map(id => [id, { sets: {} }] as const));
|
||||||
Object.assign(cache, dummyPronouns);
|
Object.assign(cache, dummyPronouns);
|
||||||
return dummyPronouns;
|
return dummyPronouns;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatPronouns(pronouns: string): string {
|
export function extractPronouns(pronounSet?: { [locale: string]: PronounCode[] }): string {
|
||||||
|
if (!pronounSet || !pronounSet.en) return PronounMapping.unspecified;
|
||||||
|
// PronounDB returns an empty set instead of {sets: {en: ["unspecified"]}}.
|
||||||
|
const pronouns = pronounSet.en;
|
||||||
const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; };
|
const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; };
|
||||||
// For capitalized pronouns, just return the mapping (it is by default capitalized)
|
|
||||||
if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns];
|
if (pronouns.length === 1) {
|
||||||
// If it is set to lowercase and a special code (any, ask, avoid), then just return the capitalized text
|
// For capitalized pronouns or special codes (any, ask, avoid), we always return the normal (capitalized) string
|
||||||
else if (
|
if (pronounsFormat === PronounsFormat.Capitalized || ["any", "ask", "avoid", "other", "unspecified"].includes(pronouns[0]))
|
||||||
pronounsFormat === PronounsFormat.Lowercase
|
return PronounMapping[pronouns[0]];
|
||||||
&& ["any", "ask", "avoid", "other"].includes(pronouns)
|
else return PronounMapping[pronouns[0]].toLowerCase();
|
||||||
) return PronounMapping[pronouns];
|
}
|
||||||
// Otherwise (lowercase and not a special code), then convert the mapping to lowercase
|
const pronounString = pronouns.map(p => p[0].toUpperCase() + p.slice(1)).join("/");
|
||||||
else return PronounMapping[pronouns].toLowerCase();
|
return pronounsFormat === PronounsFormat.Capitalized ? pronounString : pronounString.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,31 +26,29 @@ export interface UserProfilePronounsProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PronounsResponse {
|
export interface PronounsResponse {
|
||||||
[id: string]: PronounCode;
|
[id: string]: {
|
||||||
|
sets?: {
|
||||||
|
[locale: string]: PronounCode[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CachePronouns {
|
||||||
|
sets?: {
|
||||||
|
[locale: string]: PronounCode[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PronounCode = keyof typeof PronounMapping;
|
export type PronounCode = keyof typeof PronounMapping;
|
||||||
|
|
||||||
export const PronounMapping = {
|
export const PronounMapping = {
|
||||||
hh: "He/Him",
|
he: "He/Him",
|
||||||
hi: "He/It",
|
it: "It/Its",
|
||||||
hs: "He/She",
|
she: "She/Her",
|
||||||
ht: "He/They",
|
they: "They/Them",
|
||||||
ih: "It/Him",
|
|
||||||
ii: "It/Its",
|
|
||||||
is: "It/She",
|
|
||||||
it: "It/They",
|
|
||||||
shh: "She/He",
|
|
||||||
sh: "She/Her",
|
|
||||||
si: "She/It",
|
|
||||||
st: "She/They",
|
|
||||||
th: "They/He",
|
|
||||||
ti: "They/It",
|
|
||||||
ts: "They/She",
|
|
||||||
tt: "They/Them",
|
|
||||||
any: "Any pronouns",
|
any: "Any pronouns",
|
||||||
other: "Other pronouns",
|
other: "Other pronouns",
|
||||||
ask: "Ask me my pronouns",
|
ask: "Ask me my pronouns",
|
||||||
avoid: "Avoid pronouns, use my name",
|
avoid: "Avoid pronouns, use my name",
|
||||||
unspecified: "Unspecified"
|
unspecified: "No pronouns specified.",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
|
import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
|
import { Button, FluxDispatcher, GuildChannelStore, GuildStore, React, ReadStateStore } from "@webpack/common";
|
||||||
|
@ -64,7 +65,7 @@ export default definePlugin({
|
||||||
authors: [Devs.kemo],
|
authors: [Devs.kemo],
|
||||||
dependencies: ["ServerListAPI"],
|
dependencies: ["ServerListAPI"],
|
||||||
|
|
||||||
renderReadAllButton: () => <ReadAllButton />,
|
renderReadAllButton: ErrorBoundary.wrap(ReadAllButton, { noop: true }),
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
|
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
|
||||||
|
|
5
src/plugins/replyTimestamp/README.md
Normal file
5
src/plugins/replyTimestamp/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# ReplyTimestamp
|
||||||
|
|
||||||
|
Shows timestamps on the previews of replied-to messages. Pretty simple.
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/1547062/62e2b67a-e567-4c7a-884d-4640f897f7e0)
|
77
src/plugins/replyTimestamp/index.tsx
Normal file
77
src/plugins/replyTimestamp/index.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { Timestamp } from "@webpack/common";
|
||||||
|
import type { Message } from "discord-types/general";
|
||||||
|
import type { HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
const { getMessageTimestampId } = findByPropsLazy("getMessageTimestampId");
|
||||||
|
const { calendarFormat, dateFormat, isSameDay } = findByPropsLazy("calendarFormat", "dateFormat", "isSameDay", "accessibilityLabelCalendarFormat");
|
||||||
|
const MessageClasses = findByPropsLazy("separator", "latin24CompactTimeStamp");
|
||||||
|
|
||||||
|
function Sep(props: HTMLAttributes<HTMLElement>) {
|
||||||
|
return <i className={MessageClasses.separator} aria-hidden={true} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum ReferencedMessageState {
|
||||||
|
LOADED = 0,
|
||||||
|
NOT_LOADED = 1,
|
||||||
|
DELETED = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReferencedMessage = { state: ReferencedMessageState.LOADED; message: Message; } | { state: ReferencedMessageState.NOT_LOADED | ReferencedMessageState.DELETED; };
|
||||||
|
|
||||||
|
function ReplyTimestamp({
|
||||||
|
referencedMessage,
|
||||||
|
baseMessage,
|
||||||
|
}: {
|
||||||
|
referencedMessage: ReferencedMessage,
|
||||||
|
baseMessage: Message;
|
||||||
|
}) {
|
||||||
|
if (referencedMessage.state !== ReferencedMessageState.LOADED) return null;
|
||||||
|
const refTimestamp = referencedMessage.message.timestamp as any;
|
||||||
|
const baseTimestamp = baseMessage.timestamp as any;
|
||||||
|
return (
|
||||||
|
<Timestamp
|
||||||
|
id={getMessageTimestampId(referencedMessage.message)}
|
||||||
|
className="vc-reply-timestamp"
|
||||||
|
compact={isSameDay(refTimestamp, baseTimestamp)}
|
||||||
|
timestamp={refTimestamp}
|
||||||
|
isInline={false}
|
||||||
|
>
|
||||||
|
<Sep>[</Sep>
|
||||||
|
{isSameDay(refTimestamp, baseTimestamp)
|
||||||
|
? dateFormat(refTimestamp, "LT")
|
||||||
|
: calendarFormat(refTimestamp)
|
||||||
|
}
|
||||||
|
<Sep>]</Sep>
|
||||||
|
</Timestamp>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ReplyTimestamp",
|
||||||
|
description: "Shows a timestamp on replied-message previews",
|
||||||
|
authors: [Devs.Kyuuhachi],
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "renderSingleLineMessage:function()",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<="aria-label":\i,children:\[)(?=\i,\i,\i\])/,
|
||||||
|
replace: "$self.ReplyTimestamp(arguments[0]),"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
ReplyTimestamp: ErrorBoundary.wrap(ReplyTimestamp, { noop: true }),
|
||||||
|
});
|
3
src/plugins/replyTimestamp/style.css
Normal file
3
src/plugins/replyTimestamp/style.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.vc-reply-timestamp {
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
# ShowHiddenThings
|
# ShowHiddenThings
|
||||||
|
|
||||||
Displays various moderator-only elements regardless of permissions.
|
Displays various hidden & moderator-only things regardless of permissions.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -9,3 +9,11 @@ Displays various moderator-only elements regardless of permissions.
|
||||||
|
|
||||||
- Show the invites paused tooltip in the server list
|
- Show the invites paused tooltip in the server list
|
||||||
![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b)
|
![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b)
|
||||||
|
|
||||||
|
- Show the member mod view context menu item in all servers
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/47677887/3dac95dd-841c-4c15-ad87-2db7bd1e4dab)
|
||||||
|
|
||||||
|
- Disable filters in Server Discovery search that hide servers that don't meet discovery criteria
|
||||||
|
|
||||||
|
- Disable filters in Server Discovery search that hide NSFW & disallowed servers
|
||||||
|
|
|
@ -31,13 +31,28 @@ const settings = definePluginSettings({
|
||||||
description: "Show the invites paused tooltip in the server list.",
|
description: "Show the invites paused tooltip in the server list.",
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showModView: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show the member mod view context menu item in all servers.",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
disableDiscoveryFilters: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Disable filters in Server Discovery search that hide servers that don't meet discovery criteria.",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
disableDisallowedDiscoveryFilters: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Disable filters in Server Discovery search that hide NSFW & disallowed servers.",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
|
migratePluginSettings("ShowHiddenThings", "ShowTimeouts");
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ShowHiddenThings",
|
name: "ShowHiddenThings",
|
||||||
tags: ["ShowTimeouts", "ShowInvitesPaused"],
|
tags: ["ShowTimeouts", "ShowInvitesPaused", "ShowModView", "DisableDiscoveryFilters"],
|
||||||
description: "Displays various moderator-only elements regardless of permissions.",
|
description: "Displays various hidden & moderator-only things regardless of permissions.",
|
||||||
authors: [Devs.Dolfies],
|
authors: [Devs.Dolfies],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
|
@ -55,6 +70,39 @@ export default definePlugin({
|
||||||
match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/,
|
match: /\i\.\i\.can\(\i\.Permissions.MANAGE_GUILD,\i\)/,
|
||||||
replace: "true",
|
replace: "true",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "canAccessGuildMemberModViewWithExperiment:",
|
||||||
|
predicate: () => settings.store.showModView,
|
||||||
|
replacement: {
|
||||||
|
match: /return \i\.hasAny\(\i\.computePermissions\(\{user:\i,context:\i,checkElevated:!1\}\),\i\.MemberSafetyPagePermissions\)/,
|
||||||
|
replace: "return true",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "auto_removed:",
|
||||||
|
predicate: () => settings.store.disableDiscoveryFilters,
|
||||||
|
replacement: {
|
||||||
|
match: /filters:\i\.join\(" AND "\),facets:\[/,
|
||||||
|
replace: "facets:["
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "DiscoveryBannedSearchWords.includes",
|
||||||
|
predicate: () => settings.store.disableDisallowedDiscoveryFilters,
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=function\(\){)(?=.{0,130}DiscoveryBannedSearchWords\.includes)/,
|
||||||
|
replace: "return false;"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "Endpoints.GUILD_DISCOVERY_VALID_TERM",
|
||||||
|
predicate: () => settings.store.disableDisallowedDiscoveryFilters,
|
||||||
|
all: true,
|
||||||
|
replacement: {
|
||||||
|
match: /\i\.HTTP\.get\(\{url:\i\.Endpoints\.GUILD_DISCOVERY_VALID_TERM,query:\{term:\i\},oldFormErrors:!0\}\);/g,
|
||||||
|
replace: "Promise.resolve({ body: { valid: true } });"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
settings,
|
settings,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { Message, User } from "discord-types/general";
|
import { Message, User } from "discord-types/general";
|
||||||
|
@ -56,7 +57,7 @@ export default definePlugin({
|
||||||
],
|
],
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
|
renderUsername: ErrorBoundary.wrap(({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
|
||||||
try {
|
try {
|
||||||
const user = userOverride ?? message.author;
|
const user = userOverride ?? message.author;
|
||||||
let { username } = user;
|
let { username } = user;
|
||||||
|
@ -66,14 +67,14 @@ export default definePlugin({
|
||||||
const { nick } = author;
|
const { nick } = author;
|
||||||
const prefix = withMentionPrefix ? "@" : "";
|
const prefix = withMentionPrefix ? "@" : "";
|
||||||
if (username === nick || isRepliedMessage && !settings.store.inReplies)
|
if (username === nick || isRepliedMessage && !settings.store.inReplies)
|
||||||
return prefix + nick;
|
return <>{prefix}{nick}</>;
|
||||||
if (settings.store.mode === "user-nick")
|
if (settings.store.mode === "user-nick")
|
||||||
return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>;
|
return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>;
|
||||||
if (settings.store.mode === "nick-user")
|
if (settings.store.mode === "nick-user")
|
||||||
return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>;
|
return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>;
|
||||||
return prefix + username;
|
return <>{prefix}{username}</>;
|
||||||
} catch {
|
} catch {
|
||||||
return author?.nick;
|
return <>{author?.nick}</>;
|
||||||
}
|
}
|
||||||
},
|
}, { noop: true }),
|
||||||
});
|
});
|
||||||
|
|
8
src/plugins/showTimeoutDuration/README.md
Normal file
8
src/plugins/showTimeoutDuration/README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# ShowTimeoutDuration
|
||||||
|
|
||||||
|
Displays how much longer a user's timeout will last.
|
||||||
|
Either in the timeout icon tooltip, or next to it, configurable via settings!
|
||||||
|
|
||||||
|
![indicator in tooltip](https://github.com/Vendicated/Vencord/assets/45497981/606588a3-2646-40d9-8800-b6307f650136)
|
||||||
|
|
||||||
|
![indicator next to timeout icon](https://github.com/Vendicated/Vencord/assets/45497981/ab9d2101-0fdc-4143-9310-9488f056eeee)
|
106
src/plugins/showTimeoutDuration/index.tsx
Normal file
106
src/plugins/showTimeoutDuration/index.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Margins } from "@utils/margins";
|
||||||
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
|
import { findComponentLazy } from "@webpack";
|
||||||
|
import { ChannelStore, Forms, GuildMemberStore, i18n, Text, Tooltip } from "@webpack/common";
|
||||||
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
const CountDown = findComponentLazy(m => m.prototype?.render?.toString().includes(".MAX_AGE_NEVER"));
|
||||||
|
|
||||||
|
const enum DisplayStyle {
|
||||||
|
Tooltip = "tooltip",
|
||||||
|
Inline = "ssalggnikool"
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
displayStyle: {
|
||||||
|
description: "How to display the timeout duration",
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
restartNeeded: true,
|
||||||
|
options: [
|
||||||
|
{ label: "In the Tooltip", value: DisplayStyle.Tooltip },
|
||||||
|
{ label: "Next to the timeout icon", value: DisplayStyle.Inline, default: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderTimeout(message: Message, inline: boolean) {
|
||||||
|
const guildId = ChannelStore.getChannel(message.channel_id)?.guild_id;
|
||||||
|
if (!guildId) return null;
|
||||||
|
|
||||||
|
const member = GuildMemberStore.getMember(guildId, message.author.id);
|
||||||
|
if (!member?.communicationDisabledUntil) return null;
|
||||||
|
|
||||||
|
const countdown = () => (
|
||||||
|
<CountDown
|
||||||
|
deadline={new Date(member.communicationDisabledUntil!)}
|
||||||
|
showUnits
|
||||||
|
stopAtOneSec
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return inline
|
||||||
|
? countdown()
|
||||||
|
: i18n.Messages.GUILD_ENABLE_COMMUNICATION_TIME_REMAINING.format({
|
||||||
|
username: message.author.username,
|
||||||
|
countdown
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ShowTimeoutDuration",
|
||||||
|
description: "Shows how much longer a user's timeout will last, either in the timeout icon tooltip or next to it",
|
||||||
|
authors: [Devs.Ven],
|
||||||
|
|
||||||
|
settings,
|
||||||
|
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: ".GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /(\i)\.Tooltip,{(text:.{0,30}\.Messages\.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY)/,
|
||||||
|
get replace() {
|
||||||
|
if (settings.store.displayStyle === DisplayStyle.Inline)
|
||||||
|
return "$self.TooltipWrapper,{vcProps:arguments[0],$2";
|
||||||
|
|
||||||
|
return "$1.Tooltip,{text:$self.renderTimeoutDuration(arguments[0])";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
renderTimeoutDuration: ErrorBoundary.wrap(({ message }: { message: Message; }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Forms.FormText>{i18n.Messages.GUILD_COMMUNICATION_DISABLED_ICON_TOOLTIP_BODY}</Forms.FormText>
|
||||||
|
<Forms.FormText className={Margins.top8}>
|
||||||
|
{renderTimeout(message, false)}
|
||||||
|
</Forms.FormText>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, { noop: true }),
|
||||||
|
|
||||||
|
TooltipWrapper: ErrorBoundary.wrap(({ vcProps: { message }, ...tooltipProps }: { vcProps: { message: Message; }; }) => {
|
||||||
|
return (
|
||||||
|
<div className="vc-std-wrapper">
|
||||||
|
<Tooltip {...tooltipProps as any} />
|
||||||
|
|
||||||
|
<Text variant="text-md/normal" color="status-danger">
|
||||||
|
{renderTimeout(message, true)} timeout remaining
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, { noop: true })
|
||||||
|
});
|
4
src/plugins/showTimeoutDuration/styles.css
Normal file
4
src/plugins/showTimeoutDuration/styles.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.vc-std-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
|
@ -18,10 +18,11 @@
|
||||||
|
|
||||||
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
|
||||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
|
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
|
||||||
|
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { FluxDispatcher, React } from "@webpack/common";
|
import { FluxDispatcher, Menu, React } from "@webpack/common";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
showIcon: {
|
showIcon: {
|
||||||
|
@ -30,6 +31,11 @@ const settings = definePluginSettings({
|
||||||
description: "Show an icon for toggling the plugin",
|
description: "Show an icon for toggling the plugin",
|
||||||
restartNeeded: true,
|
restartNeeded: true,
|
||||||
},
|
},
|
||||||
|
contextMenu: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Add option to toggle the functionality in the chat input context menu",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
isEnabled: {
|
isEnabled: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Toggle functionality",
|
description: "Toggle functionality",
|
||||||
|
@ -56,13 +62,37 @@ const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => {
|
||||||
|
const { isEnabled, contextMenu } = settings.use(["isEnabled", "contextMenu"]);
|
||||||
|
if (!contextMenu) return;
|
||||||
|
|
||||||
|
const group = findGroupChildrenByChildId("submit-button", children);
|
||||||
|
|
||||||
|
if (!group) return;
|
||||||
|
|
||||||
|
const idx = group.findIndex(c => c?.props?.id === "submit-button");
|
||||||
|
|
||||||
|
group.splice(idx + 1, 0,
|
||||||
|
<Menu.MenuCheckboxItem
|
||||||
|
id="vc-silent-typing"
|
||||||
|
label="Enable Silent Typing"
|
||||||
|
checked={isEnabled}
|
||||||
|
action={() => settings.store.isEnabled = !settings.store.isEnabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "SilentTyping",
|
name: "SilentTyping",
|
||||||
authors: [Devs.Ven, Devs.Rini],
|
authors: [Devs.Ven, Devs.Rini, Devs.ImBanana],
|
||||||
description: "Hide that you are typing",
|
description: "Hide that you are typing",
|
||||||
dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
|
dependencies: ["CommandsAPI", "ChatInputButtonAPI"],
|
||||||
settings,
|
settings,
|
||||||
|
contextMenus: {
|
||||||
|
"textarea-context": ChatBarContextCheckbox
|
||||||
|
},
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '.dispatch({type:"TYPING_START_LOCAL"',
|
find: '.dispatch({type:"TYPING_START_LOCAL"',
|
||||||
|
|
|
@ -26,10 +26,12 @@ export default definePlugin({
|
||||||
description: "Adds Startup Timings to the Settings menu",
|
description: "Adds Startup Timings to the Settings menu",
|
||||||
authors: [Devs.Megu],
|
authors: [Devs.Megu],
|
||||||
patches: [{
|
patches: [{
|
||||||
find: "UserSettingsSections.PAYMENT_FLOW_MODAL_TEST_PAGE,",
|
find: "Messages.ACTIVITY_SETTINGS",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /{section:\i\.UserSettingsSections\.PAYMENT_FLOW_MODAL_TEST_PAGE/,
|
match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\))/,
|
||||||
replace: '{section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage},$&'
|
replace: (_, commaOrSemi, settings, elements) => "" +
|
||||||
|
`${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
|
||||||
|
`&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
StartupTimingPage
|
StartupTimingPage
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# ThemeAttributes
|
# ThemeAttributes
|
||||||
|
|
||||||
This plugin adds data attributes to various elements inside Discord
|
This plugin adds data attributes and CSS variables to various elements inside Discord
|
||||||
|
|
||||||
This allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible
|
This allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible
|
||||||
|
|
||||||
|
@ -15,6 +15,15 @@ This allows themes to more easily theme those elements or even do things that ot
|
||||||
### Chat Messages
|
### Chat Messages
|
||||||
|
|
||||||
- `data-author-id` contains the id of the author
|
- `data-author-id` contains the id of the author
|
||||||
|
- `data-author-username` contains the username of the author
|
||||||
- `data-is-self` is a boolean indicating whether this is the current user's message
|
- `data-is-self` is a boolean indicating whether this is the current user's message
|
||||||
|
|
||||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)
|
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)
|
||||||
|
|
||||||
|
## CSS Variables
|
||||||
|
|
||||||
|
### Avatars
|
||||||
|
|
||||||
|
`--avatar-url-<resolution>` contains a URL for the users avatar with the size attribute adjusted for the resolutions `128, 256, 512, 1024, 2048, 4096`.
|
||||||
|
|
||||||
|
![image](https://github.com/Vendicated/Vencord/assets/26598490/192ddac0-c827-472f-9933-fa99ff36f723)
|
||||||
|
|
|
@ -9,10 +9,11 @@ import definePlugin from "@utils/types";
|
||||||
import { UserStore } from "@webpack/common";
|
import { UserStore } from "@webpack/common";
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ThemeAttributes",
|
name: "ThemeAttributes",
|
||||||
description: "Adds data attributes to various elements for theming purposes",
|
description: "Adds data attributes to various elements for theming purposes",
|
||||||
authors: [Devs.Ven],
|
authors: [Devs.Ven, Devs.Board],
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
// Add data-tab-id to all tab bar items
|
// Add data-tab-id to all tab bar items
|
||||||
|
@ -32,14 +33,43 @@ export default definePlugin({
|
||||||
match: /\.messageListItem(?=,"aria)/,
|
match: /\.messageListItem(?=,"aria)/,
|
||||||
replace: "$&,...$self.getMessageProps(arguments[0])"
|
replace: "$&,...$self.getMessageProps(arguments[0])"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// add --avatar-url-<resolution> css variable to avatar img elements
|
||||||
|
// popout profiles
|
||||||
|
{
|
||||||
|
find: ".LABEL_WITH_ONLINE_STATUS",
|
||||||
|
replacement: {
|
||||||
|
match: /src:null!=\i\?(\i).{1,50}"aria-hidden":!0/,
|
||||||
|
replace: "$&,style:$self.getAvatarStyles($1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// chat avatars
|
||||||
|
{
|
||||||
|
find: "showCommunicationDisabledStyles",
|
||||||
|
replacement: {
|
||||||
|
match: /src:(\i),"aria-hidden":!0/,
|
||||||
|
replace: "$&,style:$self.getAvatarStyles($1)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
getAvatarStyles(src: string) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
[128, 256, 512, 1024, 2048, 4096].map(size => [
|
||||||
|
`--avatar-url-${size}`,
|
||||||
|
`url(${src.replace(/\d+$/, String(size))})`
|
||||||
|
])
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
getMessageProps(props: { message: Message; }) {
|
getMessageProps(props: { message: Message; }) {
|
||||||
const authorId = props.message?.author?.id;
|
const author = props.message?.author;
|
||||||
|
const authorId = author?.id;
|
||||||
return {
|
return {
|
||||||
"data-author-id": authorId,
|
"data-author-id": authorId,
|
||||||
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id
|
"data-author-username": author?.username,
|
||||||
|
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,9 +40,9 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
|
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
|
||||||
const { autoTranslate } = settings.use(["autoTranslate"]);
|
const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
|
||||||
|
|
||||||
if (!isMainChat) return null;
|
if (!isMainChat || !showChatBarButton) return null;
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
const newState = !autoTranslate;
|
const newState = !autoTranslate;
|
||||||
|
|
|
@ -48,6 +48,11 @@ export const settings = definePluginSettings({
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
|
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
showChatBarButton: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show translate button in chat bar",
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
}).withPrivateSettings<{
|
}).withPrivateSettings<{
|
||||||
showAutoTranslateAlert: boolean;
|
showAutoTranslateAlert: boolean;
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/Co
|
||||||
import { ImageInvisible, ImageVisible } from "@components/Icons";
|
import { ImageInvisible, ImageVisible } from "@components/Icons";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
|
import { Constants, Menu, PermissionsBits, PermissionStore, RestAPI, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
const EMBED_SUPPRESSED = 1 << 2;
|
const EMBED_SUPPRESSED = 1 << 2;
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { channe
|
||||||
icon={isEmbedSuppressed ? ImageVisible : ImageInvisible}
|
icon={isEmbedSuppressed ? ImageVisible : ImageInvisible}
|
||||||
action={() =>
|
action={() =>
|
||||||
RestAPI.patch({
|
RestAPI.patch({
|
||||||
url: `/channels/${channel.id}/messages/${messageId}`,
|
url: Constants.Endpoints.MESSAGE(channel.id, messageId),
|
||||||
body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED }
|
body: { flags: isEmbedSuppressed ? flags & ~EMBED_SUPPRESSED : flags | EMBED_SUPPRESSED }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
7
src/plugins/validReply/README.md
Normal file
7
src/plugins/validReply/README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# ValidReply
|
||||||
|
|
||||||
|
Fixes referenced (replied to) messages showing as "Message could not be loaded".
|
||||||
|
|
||||||
|
Hover the text to load the message!
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/45801973/d3286acf-e822-4b7f-a4e7-8ced18f581af)
|
106
src/plugins/validReply/index.ts
Normal file
106
src/plugins/validReply/index.ts
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { FluxDispatcher, RestAPI } from "@webpack/common";
|
||||||
|
import { Message, User } from "discord-types/general";
|
||||||
|
import { Channel } from "discord-types/general/index.js";
|
||||||
|
|
||||||
|
const enum ReferencedMessageState {
|
||||||
|
Loaded,
|
||||||
|
NotLoaded,
|
||||||
|
Deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Reply {
|
||||||
|
baseAuthor: User,
|
||||||
|
baseMessage: Message;
|
||||||
|
channel: Channel;
|
||||||
|
referencedMessage: { state: ReferencedMessageState; };
|
||||||
|
compact: boolean;
|
||||||
|
isReplyAuthorBlocked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetching = new Map<string, string>();
|
||||||
|
let ReplyStore: any;
|
||||||
|
|
||||||
|
const { createMessageRecord } = findByPropsLazy("createMessageRecord");
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "ValidReply",
|
||||||
|
description: 'Fixes "Message could not be loaded" upon hovering over the reply',
|
||||||
|
authors: [Devs.newwares],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "Messages.REPLY_QUOTE_MESSAGE_NOT_LOADED",
|
||||||
|
replacement: {
|
||||||
|
match: /Messages\.REPLY_QUOTE_MESSAGE_NOT_LOADED/,
|
||||||
|
replace: "$&,onMouseEnter:()=>$self.fetchReply(arguments[0])"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: "ReferencedMessageStore",
|
||||||
|
replacement: {
|
||||||
|
match: /constructor\(\)\{\i\(this,"_channelCaches",new Map\)/,
|
||||||
|
replace: "$&;$self.setReplyStore(this);"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
setReplyStore(store: any) {
|
||||||
|
ReplyStore = store;
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchReply(reply: Reply) {
|
||||||
|
const { channel_id: channelId, message_id: messageId } = reply.baseMessage.messageReference!;
|
||||||
|
|
||||||
|
if (fetching.has(messageId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetching.set(messageId, channelId);
|
||||||
|
|
||||||
|
RestAPI.get({
|
||||||
|
url: `/channels/${channelId}/messages`,
|
||||||
|
query: {
|
||||||
|
limit: 1,
|
||||||
|
around: messageId
|
||||||
|
},
|
||||||
|
retries: 2
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
const reply: Message | undefined = res?.body?.[0];
|
||||||
|
if (!reply) return;
|
||||||
|
|
||||||
|
if (reply.id !== messageId) {
|
||||||
|
ReplyStore.set(channelId, messageId, {
|
||||||
|
state: ReferencedMessageState.Deleted
|
||||||
|
});
|
||||||
|
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_DELETE",
|
||||||
|
channelId: channelId,
|
||||||
|
message: messageId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ReplyStore.set(reply.channel_id, reply.id, {
|
||||||
|
state: ReferencedMessageState.Loaded,
|
||||||
|
message: createMessageRecord(reply)
|
||||||
|
});
|
||||||
|
|
||||||
|
FluxDispatcher.dispatch({
|
||||||
|
type: "MESSAGE_UPDATE",
|
||||||
|
message: reply
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => { })
|
||||||
|
.finally(() => {
|
||||||
|
fetching.delete(messageId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -18,28 +18,30 @@
|
||||||
|
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
|
import { isNonNullish } from "@utils/guards";
|
||||||
import { sleep } from "@utils/misc";
|
import { sleep } from "@utils/misc";
|
||||||
import { Queue } from "@utils/Queue";
|
import { Queue } from "@utils/Queue";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
|
import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common";
|
||||||
import type { ComponentType, ReactNode } from "react";
|
import { type ComponentType, type ReactNode } from "react";
|
||||||
|
|
||||||
// LYING to the type checker here
|
// LYING to the type checker here
|
||||||
const UserFlags = Constants.UserFlags as Record<string, number>;
|
const UserFlags = Constants.UserFlags as Record<string, number>;
|
||||||
const badges: Record<string, ProfileBadge> = {
|
const badges: Record<string, ProfileBadge> = {
|
||||||
"active_developer": { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" },
|
active_developer: { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" },
|
||||||
"bug_hunter_level_1": { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
|
bug_hunter_level_1: { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
|
||||||
"bug_hunter_level_2": { id: "bug_hunter_level_2", description: "Discord Bug Hunter", icon: "848f79194d4be5ff5f81505cbd0ce1e6", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
|
bug_hunter_level_2: { id: "bug_hunter_level_2", description: "Discord Bug Hunter", icon: "848f79194d4be5ff5f81505cbd0ce1e6", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" },
|
||||||
"certified_moderator": { id: "certified_moderator", description: "Moderator Programs Alumni", icon: "fee1624003e2fee35cb398e125dc479b", link: "https://discord.com/safety" },
|
certified_moderator: { id: "certified_moderator", description: "Moderator Programs Alumni", icon: "fee1624003e2fee35cb398e125dc479b", link: "https://discord.com/safety" },
|
||||||
"discord_employee": { id: "staff", description: "Discord Staff", icon: "5e74e9b61934fc1f67c65515d1f7e60d", link: "https://discord.com/company" },
|
discord_employee: { id: "staff", description: "Discord Staff", icon: "5e74e9b61934fc1f67c65515d1f7e60d", link: "https://discord.com/company" },
|
||||||
"hypesquad": { id: "hypesquad", description: "HypeSquad Events", icon: "bf01d1073931f921909045f3a39fd264", link: "https://discord.com/hypesquad" },
|
get staff() { return this.discord_employee; },
|
||||||
"hypesquad_online_house_1": { id: "hypesquad_house_1", description: "HypeSquad Bravery", icon: "8a88d63823d8a71cd5e390baa45efa02", link: "https://discord.com/settings/hypesquad-online" },
|
hypesquad: { id: "hypesquad", description: "HypeSquad Events", icon: "bf01d1073931f921909045f3a39fd264", link: "https://discord.com/hypesquad" },
|
||||||
"hypesquad_online_house_2": { id: "hypesquad_house_2", description: "HypeSquad Brilliance", icon: "011940fd013da3f7fb926e4a1cd2e618", link: "https://discord.com/settings/hypesquad-online" },
|
hypesquad_online_house_1: { id: "hypesquad_house_1", description: "HypeSquad Bravery", icon: "8a88d63823d8a71cd5e390baa45efa02", link: "https://discord.com/settings/hypesquad-online" },
|
||||||
"hypesquad_online_house_3": { id: "hypesquad_house_3", description: "HypeSquad Balance", icon: "3aa41de486fa12454c3761e8e223442e", link: "https://discord.com/settings/hypesquad-online" },
|
hypesquad_online_house_2: { id: "hypesquad_house_2", description: "HypeSquad Brilliance", icon: "011940fd013da3f7fb926e4a1cd2e618", link: "https://discord.com/settings/hypesquad-online" },
|
||||||
"partner": { id: "partner", description: "Partnered Server Owner", icon: "3f9748e53446a137a052f3454e2de41e", link: "https://discord.com/partners" },
|
hypesquad_online_house_3: { id: "hypesquad_house_3", description: "HypeSquad Balance", icon: "3aa41de486fa12454c3761e8e223442e", link: "https://discord.com/settings/hypesquad-online" },
|
||||||
"premium": { id: "premium", description: "Subscriber", icon: "2ba85e8026a8614b640c2837bcdfe21b", link: "https://discord.com/settings/premium" },
|
partner: { id: "partner", description: "Partnered Server Owner", icon: "3f9748e53446a137a052f3454e2de41e", link: "https://discord.com/partners" },
|
||||||
"premium_early_supporter": { id: "early_supporter", description: "Early Supporter", icon: "7060786766c9c840eb3019e725d2b358", link: "https://discord.com/settings/premium" },
|
premium: { id: "premium", description: "Subscriber", icon: "2ba85e8026a8614b640c2837bcdfe21b", link: "https://discord.com/settings/premium" },
|
||||||
"verified_developer": { id: "verified_developer", description: "Early Verified Bot Developer", icon: "6df5892e0f35b051f8b61eace34f4967" },
|
premium_early_supporter: { id: "early_supporter", description: "Early Supporter", icon: "7060786766c9c840eb3019e725d2b358", link: "https://discord.com/settings/premium" },
|
||||||
|
verified_developer: { id: "verified_developer", description: "Early Verified Bot Developer", icon: "6df5892e0f35b051f8b61eace34f4967" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetching = new Set<string>();
|
const fetching = new Set<string>();
|
||||||
|
@ -73,7 +75,7 @@ async function getUser(id: string) {
|
||||||
if (userObj)
|
if (userObj)
|
||||||
return userObj;
|
return userObj;
|
||||||
|
|
||||||
const user: any = await RestAPI.get({ url: `/users/${id}` }).then(response => {
|
const user: any = await RestAPI.get({ url: Constants.Endpoints.USER(id) }).then(response => {
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
type: "USER_UPDATE",
|
type: "USER_UPDATE",
|
||||||
user: response.body,
|
user: response.body,
|
||||||
|
@ -93,7 +95,8 @@ async function getUser(id: string) {
|
||||||
userObj = UserStore.getUser(id);
|
userObj = UserStore.getUser(id);
|
||||||
const fakeBadges: ProfileBadge[] = Object.entries(UserFlags)
|
const fakeBadges: ProfileBadge[] = Object.entries(UserFlags)
|
||||||
.filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag))
|
.filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag))
|
||||||
.map(([key]) => badges[key.toLowerCase()]);
|
.map(([key]) => badges[key.toLowerCase()])
|
||||||
|
.filter(isNonNullish);
|
||||||
if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.("a_")))
|
if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.("a_")))
|
||||||
fakeBadges.push(badges.premium);
|
fakeBadges.push(badges.premium);
|
||||||
|
|
||||||
|
@ -202,6 +205,7 @@ export default definePlugin({
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary noop>
|
<ErrorBoundary noop>
|
||||||
<MentionWrapper
|
<MentionWrapper
|
||||||
|
key={"mention" + data.userId}
|
||||||
RoleMention={RoleMention}
|
RoleMention={RoleMention}
|
||||||
UserMention={UserMention}
|
UserMention={UserMention}
|
||||||
data={data}
|
data={data}
|
||||||
|
|
55
src/plugins/voiceDownload/index.tsx
Normal file
55
src/plugins/voiceDownload/index.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "VoiceDownload",
|
||||||
|
description: "Adds a download to voice messages. (Opens a new browser tab)",
|
||||||
|
authors: [Devs.puv],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "rippleContainer,children",
|
||||||
|
replacement: {
|
||||||
|
match: /\(0,\i\.jsx\).{0,150},children:.{0,50}\("source",{src:(\i)}\)}\)/,
|
||||||
|
replace: "[$&, $self.renderDownload($1)]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
renderDownload(src: string) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="vc-voice-download"
|
||||||
|
href={src}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
aria-label="Download voice message"
|
||||||
|
{...IS_DISCORD_DESKTOP
|
||||||
|
? { target: "_blank" } // open externally
|
||||||
|
: { download: "voice-message.ogg" } // download directly (not supported on discord desktop)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<this.Icon />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
Icon: () => (
|
||||||
|
<svg
|
||||||
|
height="24"
|
||||||
|
width="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 2a1 1 0 0 1 1 1v10.59l3.3-3.3a1 1 0 1 1 1.4 1.42l-5 5a1 1 0 0 1-1.4 0l-5-5a1 1 0 1 1 1.4-1.42l3.3 3.3V3a1 1 0 0 1 1-1ZM3 20a1 1 0 1 0 0 2h18a1 1 0 1 0 0-2H3Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
});
|
12
src/plugins/voiceDownload/style.css
Normal file
12
src/plugins/voiceDownload/style.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.vc-voice-download {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: var(--interactive-normal);
|
||||||
|
margin-left: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-voice-download:hover {
|
||||||
|
color: var(--interactive-active);
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ import { useAwaiter } from "@utils/react";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { chooseFile } from "@utils/web";
|
import { chooseFile } from "@utils/web";
|
||||||
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
import { Button, Card, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
|
import { Button, Card, Constants, FluxDispatcher, Forms, lodash, Menu, MessageActions, PermissionsBits, PermissionStore, RestAPI, SelectedChannelStore, showToast, SnowflakeUtils, Toasts, useEffect, useState } from "@webpack/common";
|
||||||
import { ComponentType } from "react";
|
import { ComponentType } from "react";
|
||||||
|
|
||||||
import { VoiceRecorderDesktop } from "./DesktopRecorder";
|
import { VoiceRecorderDesktop } from "./DesktopRecorder";
|
||||||
|
@ -98,7 +98,7 @@ function sendAudio(blob: Blob, meta: AudioMetadata) {
|
||||||
|
|
||||||
upload.on("complete", () => {
|
upload.on("complete", () => {
|
||||||
RestAPI.post({
|
RestAPI.post({
|
||||||
url: `/channels/${channelId}/messages`,
|
url: Constants.Endpoints.MESSAGES(channelId),
|
||||||
body: {
|
body: {
|
||||||
flags: 1 << 13,
|
flags: 1 << 13,
|
||||||
channel_id: channelId,
|
channel_id: channelId,
|
||||||
|
|
30
src/plugins/webScreenShareFixes.web/index.ts
Normal file
30
src/plugins/webScreenShareFixes.web/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import definePlugin from "@utils/types";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "WebScreenShareFixes",
|
||||||
|
authors: [Devs.Kaitlyn],
|
||||||
|
description: "Removes 2500kbps bitrate cap on chromium and vesktop clients.",
|
||||||
|
enabledByDefault: true,
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "x-google-max-bitrate",
|
||||||
|
replacement: [
|
||||||
|
{
|
||||||
|
match: /"x-google-max-bitrate=".concat\(\i\)/,
|
||||||
|
replace: '"x-google-max-bitrate=".concat("80_000")'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /;level-asymmetry-allowed=1/,
|
||||||
|
replace: ";b=AS:800000;level-asymmetry-allowed=1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
|
@ -23,7 +23,7 @@ import { Queue } from "@utils/Queue";
|
||||||
import { useForceUpdater } from "@utils/react";
|
import { useForceUpdater } from "@utils/react";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||||
import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
|
import { ChannelStore, Constants, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
|
||||||
import { CustomEmoji } from "@webpack/types";
|
import { CustomEmoji } from "@webpack/types";
|
||||||
import { Message, ReactionEmoji, User } from "discord-types/general";
|
import { Message, ReactionEmoji, User } from "discord-types/general";
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ let reactions: Record<string, ReactionCacheEntry>;
|
||||||
function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
|
function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
|
||||||
const key = emoji.name + (emoji.id ? `:${emoji.id}` : "");
|
const key = emoji.name + (emoji.id ? `:${emoji.id}` : "");
|
||||||
return RestAPI.get({
|
return RestAPI.get({
|
||||||
url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${key}`,
|
url: Constants.Endpoints.REACTIONS(msg.channel_id, msg.id, key),
|
||||||
query: {
|
query: {
|
||||||
limit: 100,
|
limit: 100,
|
||||||
type
|
type
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Vencord, a Discord client mod
|
* Vencord, a Discord client mod
|
||||||
* Copyright (c) 2023 Vendicated and contributors
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -13,10 +13,7 @@ import { findByPropsLazy } from "@webpack";
|
||||||
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
|
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
|
||||||
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
|
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
|
||||||
|
|
||||||
const enum ChannelTypes {
|
const { ChannelTypes } = findByPropsLazy("ChannelTypes");
|
||||||
DM = 1,
|
|
||||||
GROUP_DM = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
guild_id: string,
|
guild_id: string,
|
||||||
|
@ -71,15 +68,35 @@ interface Call {
|
||||||
ringing: string[];
|
ringing: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MuteStore = findByPropsLazy("isSuppressEveryoneEnabled");
|
const Notifs = findByPropsLazy("makeTextChatNotification");
|
||||||
const XSLog = new Logger("XSOverlay");
|
const XSLog = new Logger("XSOverlay");
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
ignoreBots: {
|
botNotifications: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Ignore messages from bots",
|
description: "Allow bot notifications",
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
serverNotifications: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Allow server notifications",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
dmNotifications: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Allow Direct Message notifications",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
groupDmNotifications: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Allow Group DM notifications",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
callNotifications: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Allow call notifications",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
pingColor: {
|
pingColor: {
|
||||||
type: OptionType.STRING,
|
type: OptionType.STRING,
|
||||||
description: "User mention color",
|
description: "User mention color",
|
||||||
|
@ -97,8 +114,13 @@ const settings = definePluginSettings({
|
||||||
},
|
},
|
||||||
timeout: {
|
timeout: {
|
||||||
type: OptionType.NUMBER,
|
type: OptionType.NUMBER,
|
||||||
description: "Notif duration (secs)",
|
description: "Notification duration (secs)",
|
||||||
default: 1.0,
|
default: 3,
|
||||||
|
},
|
||||||
|
lengthBasedTimeout: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Extend duration with message length",
|
||||||
|
default: true
|
||||||
},
|
},
|
||||||
opacity: {
|
opacity: {
|
||||||
type: OptionType.SLIDER,
|
type: OptionType.SLIDER,
|
||||||
|
@ -124,7 +146,7 @@ export default definePlugin({
|
||||||
settings,
|
settings,
|
||||||
flux: {
|
flux: {
|
||||||
CALL_UPDATE({ call }: { call: Call; }) {
|
CALL_UPDATE({ call }: { call: Call; }) {
|
||||||
if (call?.ringing?.includes(UserStore.getCurrentUser().id)) {
|
if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) {
|
||||||
const channel = ChannelStore.getChannel(call.channel_id);
|
const channel = ChannelStore.getChannel(call.channel_id);
|
||||||
sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
|
sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
|
||||||
}
|
}
|
||||||
|
@ -134,7 +156,7 @@ export default definePlugin({
|
||||||
try {
|
try {
|
||||||
if (optimistic) return;
|
if (optimistic) return;
|
||||||
const channel = ChannelStore.getChannel(message.channel_id);
|
const channel = ChannelStore.getChannel(message.channel_id);
|
||||||
if (!shouldNotify(message, channel)) return;
|
if (!shouldNotify(message, message.channel_id)) return;
|
||||||
|
|
||||||
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
|
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
|
||||||
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
|
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
|
||||||
|
@ -194,6 +216,7 @@ export default definePlugin({
|
||||||
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`);
|
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// color role mentions (unity styling btw lol)
|
||||||
if (message.mention_roles.length > 0) {
|
if (message.mention_roles.length > 0) {
|
||||||
for (const roleId of message.mention_roles) {
|
for (const roleId of message.mention_roles) {
|
||||||
const role = GuildStore.getRole(channel.guild_id, roleId);
|
const role = GuildStore.getRole(channel.guild_id, roleId);
|
||||||
|
@ -213,6 +236,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// color channel mentions
|
||||||
if (channelMatches) {
|
if (channelMatches) {
|
||||||
for (const cMatch of channelMatches) {
|
for (const cMatch of channelMatches) {
|
||||||
let channelId = cMatch.split("<#")[1];
|
let channelId = cMatch.split("<#")[1];
|
||||||
|
@ -221,6 +245,7 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldIgnoreForChannelType(channel)) return;
|
||||||
sendMsgNotif(titleString, finalMsg, message);
|
sendMsgNotif(titleString, finalMsg, message);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
|
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
|
||||||
|
@ -229,13 +254,19 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function shouldIgnoreForChannelType(channel: Channel) {
|
||||||
|
if (channel.type === ChannelTypes.DM && settings.store.dmNotifications) return false;
|
||||||
|
if (channel.type === ChannelTypes.GROUP_DM && settings.store.groupDmNotifications) return false;
|
||||||
|
else return !settings.store.serverNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
function sendMsgNotif(titleString: string, content: string, message: Message) {
|
function sendMsgNotif(titleString: string, content: string, message: Message) {
|
||||||
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
|
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
|
||||||
const msgData = {
|
const msgData = {
|
||||||
messageType: 1,
|
messageType: 1,
|
||||||
index: 0,
|
index: 0,
|
||||||
timeout: settings.store.timeout,
|
timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
|
||||||
height: calculateHeight(cleanMessage(content)),
|
height: calculateHeight(content),
|
||||||
opacity: settings.store.opacity,
|
opacity: settings.store.opacity,
|
||||||
volume: settings.store.volume,
|
volume: settings.store.volume,
|
||||||
audioPath: settings.store.soundPath,
|
audioPath: settings.store.soundPath,
|
||||||
|
@ -253,8 +284,8 @@ function sendOtherNotif(content: string, titleString: string) {
|
||||||
const msgData = {
|
const msgData = {
|
||||||
messageType: 1,
|
messageType: 1,
|
||||||
index: 0,
|
index: 0,
|
||||||
timeout: settings.store.timeout,
|
timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
|
||||||
height: calculateHeight(cleanMessage(content)),
|
height: calculateHeight(content),
|
||||||
opacity: settings.store.opacity,
|
opacity: settings.store.opacity,
|
||||||
volume: settings.store.volume,
|
volume: settings.store.volume,
|
||||||
audioPath: settings.store.soundPath,
|
audioPath: settings.store.soundPath,
|
||||||
|
@ -267,13 +298,11 @@ function sendOtherNotif(content: string, titleString: string) {
|
||||||
Native.sendToOverlay(msgData);
|
Native.sendToOverlay(msgData);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldNotify(message: Message, channel: Channel) {
|
function shouldNotify(message: Message, channel: string) {
|
||||||
const currentUser = UserStore.getCurrentUser();
|
const currentUser = UserStore.getCurrentUser();
|
||||||
if (message.author.id === currentUser.id) return false;
|
if (message.author.id === currentUser.id) return false;
|
||||||
if (message.author.bot && settings.store.ignoreBots) return false;
|
if (message.author.bot && !settings.store.botNotifications) return false;
|
||||||
if (MuteStore.allowAllMessages(channel) || message.mention_everyone && !MuteStore.isSuppressEveryoneEnabled(message.guild_id)) return true;
|
return Notifs.shouldNotify(message, channel);
|
||||||
|
|
||||||
return message.mentions.some(m => m.id === currentUser.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateHeight(content: string) {
|
function calculateHeight(content: string) {
|
||||||
|
@ -283,6 +312,9 @@ function calculateHeight(content: string) {
|
||||||
return 250;
|
return 250;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanMessage(content: string) {
|
function calculateTimeout(content: string) {
|
||||||
return content.replace(new RegExp("<[^>]*>", "g"), "");
|
if (content.length <= 100) return 3;
|
||||||
|
if (content.length <= 200) return 4;
|
||||||
|
if (content.length <= 300) return 5;
|
||||||
|
return 6;
|
||||||
}
|
}
|
||||||
|
|
|
@ -266,6 +266,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "Dziurwa",
|
name: "Dziurwa",
|
||||||
id: 1001086404203389018n
|
id: 1001086404203389018n
|
||||||
},
|
},
|
||||||
|
arHSM: {
|
||||||
|
name: "arHSM",
|
||||||
|
id: 841509053422632990n
|
||||||
|
},
|
||||||
F53: {
|
F53: {
|
||||||
name: "F53",
|
name: "F53",
|
||||||
id: 280411966126948353n
|
id: 280411966126948353n
|
||||||
|
@ -374,10 +378,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "ProffDea",
|
name: "ProffDea",
|
||||||
id: 609329952180928513n
|
id: 609329952180928513n
|
||||||
},
|
},
|
||||||
|
UlyssesZhan: {
|
||||||
|
name: "UlyssesZhan",
|
||||||
|
id: 586808226058862623n
|
||||||
|
},
|
||||||
ant0n: {
|
ant0n: {
|
||||||
name: "ant0n",
|
name: "ant0n",
|
||||||
id: 145224646868860928n
|
id: 145224646868860928n
|
||||||
},
|
},
|
||||||
|
Board: {
|
||||||
|
name: "BoardTM",
|
||||||
|
id: 285475344817848320n,
|
||||||
|
},
|
||||||
philipbry: {
|
philipbry: {
|
||||||
name: "philipbry",
|
name: "philipbry",
|
||||||
id: 554994003318276106n
|
id: 554994003318276106n
|
||||||
|
@ -414,6 +426,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "Kyuuhachi",
|
name: "Kyuuhachi",
|
||||||
id: 236588665420251137n,
|
id: 236588665420251137n,
|
||||||
},
|
},
|
||||||
|
nin0dev: {
|
||||||
|
name: "nin0dev",
|
||||||
|
id: 886685857560539176n
|
||||||
|
},
|
||||||
Elvyra: {
|
Elvyra: {
|
||||||
name: "Elvyra",
|
name: "Elvyra",
|
||||||
id: 708275751816003615n,
|
id: 708275751816003615n,
|
||||||
|
@ -426,6 +442,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "newwares",
|
name: "newwares",
|
||||||
id: 421405303951851520n
|
id: 421405303951851520n
|
||||||
},
|
},
|
||||||
|
puv: {
|
||||||
|
name: "puv",
|
||||||
|
id: 469441552251355137n
|
||||||
|
},
|
||||||
Kodarru: {
|
Kodarru: {
|
||||||
name: "Kodarru",
|
name: "Kodarru",
|
||||||
id: 785227396218748949n
|
id: 785227396218748949n
|
||||||
|
@ -450,10 +470,26 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "Oleh Polisan",
|
name: "Oleh Polisan",
|
||||||
id: 242305263313485825n
|
id: 242305263313485825n
|
||||||
},
|
},
|
||||||
|
HAHALOSAH: {
|
||||||
|
name: "HAHALOSAH",
|
||||||
|
id: 903418691268513883n
|
||||||
|
},
|
||||||
GabiRP: {
|
GabiRP: {
|
||||||
name: "GabiRP",
|
name: "GabiRP",
|
||||||
id: 507955112027750401n
|
id: 507955112027750401n
|
||||||
}
|
},
|
||||||
|
ImBanana: {
|
||||||
|
name: "Im_Banana",
|
||||||
|
id: 635250116688871425n
|
||||||
|
},
|
||||||
|
xocherry: {
|
||||||
|
name: "xocherry",
|
||||||
|
id: 221288171013406720n
|
||||||
|
},
|
||||||
|
ScattrdBlade: {
|
||||||
|
name: "ScattrdBlade",
|
||||||
|
id: 678007540608532491n
|
||||||
|
},
|
||||||
} satisfies Record<string, Dev>);
|
} satisfies Record<string, Dev>);
|
||||||
|
|
||||||
// iife so #__PURE__ works correctly
|
// iife so #__PURE__ works correctly
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MessageObject } from "@api/MessageEvents";
|
import { MessageObject } from "@api/MessageEvents";
|
||||||
import { ChannelStore, ComponentDispatch, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
|
import { ChannelStore, ComponentDispatch, Constants, FluxDispatcher, GuildStore, InviteActions, MaskedLink, MessageActions, ModalImageClasses, PrivateChannelsStore, RestAPI, SelectedChannelStore, SelectedGuildStore, UserProfileActions, UserProfileStore, UserSettingsActionCreators, UserUtils } from "@webpack/common";
|
||||||
import { Guild, Message, User } from "discord-types/general";
|
import { Guild, Message, User } from "discord-types/general";
|
||||||
|
|
||||||
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
|
import { ImageModal, ModalRoot, ModalSize, openModal } from "./modal";
|
||||||
|
@ -162,7 +162,7 @@ export async function fetchUserProfile(id: string, options?: FetchUserProfileOpt
|
||||||
FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_START", userId: id });
|
FluxDispatcher.dispatch({ type: "USER_PROFILE_FETCH_START", userId: id });
|
||||||
|
|
||||||
const { body } = await RestAPI.get({
|
const { body } = await RestAPI.get({
|
||||||
url: `/users/${id}/profile`,
|
url: Constants.Endpoints.USER_PROFILE(id),
|
||||||
query: {
|
query: {
|
||||||
with_mutual_guilds: false,
|
with_mutual_guilds: false,
|
||||||
with_mutual_friends_count: false,
|
with_mutual_friends_count: false,
|
||||||
|
|
24
src/utils/mergeDefaults.ts
Normal file
24
src/utils/mergeDefaults.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively merges defaults into an object and returns the same object
|
||||||
|
* @param obj Object
|
||||||
|
* @param defaults Defaults
|
||||||
|
* @returns obj
|
||||||
|
*/
|
||||||
|
export function mergeDefaults<T>(obj: T, defaults: T): T {
|
||||||
|
for (const key in defaults) {
|
||||||
|
const v = defaults[key];
|
||||||
|
if (typeof v === "object" && !Array.isArray(v)) {
|
||||||
|
obj[key] ??= {} as any;
|
||||||
|
mergeDefaults(obj[key], v);
|
||||||
|
} else {
|
||||||
|
obj[key] ??= v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
|
@ -20,25 +20,6 @@ import { Clipboard, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
import { DevsById } from "./constants";
|
import { DevsById } from "./constants";
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively merges defaults into an object and returns the same object
|
|
||||||
* @param obj Object
|
|
||||||
* @param defaults Defaults
|
|
||||||
* @returns obj
|
|
||||||
*/
|
|
||||||
export function mergeDefaults<T>(obj: T, defaults: T): T {
|
|
||||||
for (const key in defaults) {
|
|
||||||
const v = defaults[key];
|
|
||||||
if (typeof v === "object" && !Array.isArray(v)) {
|
|
||||||
obj[key] ??= {} as any;
|
|
||||||
mergeDefaults(obj[key], v);
|
|
||||||
} else {
|
|
||||||
obj[key] ??= v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls .join(" ") on the arguments
|
* Calls .join(" ") on the arguments
|
||||||
* classes("one", "two") => "one two"
|
* classes("one", "two") => "one two"
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PatchReplacement, ReplaceFn } from "./types";
|
import { Patch, PatchReplacement, ReplaceFn } from "./types";
|
||||||
|
|
||||||
export function canonicalizeMatch<T extends RegExp | string>(match: T): T {
|
export function canonicalizeMatch<T extends RegExp | string>(match: T): T {
|
||||||
if (typeof match === "string") return match;
|
if (typeof match === "string") return match;
|
||||||
|
@ -55,3 +55,9 @@ export function canonicalizeReplacement(replacement: Pick<PatchReplacement, "mat
|
||||||
);
|
);
|
||||||
Object.defineProperties(replacement, descriptors);
|
Object.defineProperties(replacement, descriptors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canonicalizeFind(patch: Patch) {
|
||||||
|
const descriptors = Object.getOwnPropertyDescriptors(patch);
|
||||||
|
descriptors.find = canonicalizeDescriptor(descriptors.find, canonicalizeMatch);
|
||||||
|
Object.defineProperties(patch, descriptors);
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import { showNotification } from "@api/Notifications";
|
import { showNotification } from "@api/Notifications";
|
||||||
import { PlainSettings, Settings } from "@api/Settings";
|
import { PlainSettings, Settings } from "@api/Settings";
|
||||||
import { Toasts } from "@webpack/common";
|
import { moment, Toasts } from "@webpack/common";
|
||||||
import { deflateSync, inflateSync } from "fflate";
|
import { deflateSync, inflateSync } from "fflate";
|
||||||
|
|
||||||
import { getCloudAuth, getCloudUrl } from "./cloud";
|
import { getCloudAuth, getCloudUrl } from "./cloud";
|
||||||
|
@ -49,7 +49,7 @@ export async function exportSettings({ minify }: { minify?: boolean; } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadSettingsBackup() {
|
export async function downloadSettingsBackup() {
|
||||||
const filename = "vencord-settings-backup.json";
|
const filename = `vencord-settings-backup-${moment().format("YYYY-MM-DD")}.json`;
|
||||||
const backup = await exportSettings();
|
const backup = await exportSettings();
|
||||||
const data = new TextEncoder().encode(backup);
|
const data = new TextEncoder().encode(backup);
|
||||||
|
|
||||||
|
|
|
@ -29,14 +29,19 @@ export default function definePlugin<P extends PluginDef>(p: P & Record<string,
|
||||||
export type ReplaceFn = (match: string, ...groups: string[]) => string;
|
export type ReplaceFn = (match: string, ...groups: string[]) => string;
|
||||||
|
|
||||||
export interface PatchReplacement {
|
export interface PatchReplacement {
|
||||||
|
/** The match for the patch replacement. If you use a string it will be implicitly converted to a RegExp */
|
||||||
match: string | RegExp;
|
match: string | RegExp;
|
||||||
|
/** The replacement string or function which returns the string for the patch replacement */
|
||||||
replace: string | ReplaceFn;
|
replace: string | ReplaceFn;
|
||||||
|
/** A function which returns whether this patch replacement should be applied */
|
||||||
predicate?(): boolean;
|
predicate?(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Patch {
|
export interface Patch {
|
||||||
plugin: string;
|
plugin: string;
|
||||||
find: string;
|
/** A string or RegExp which is only include/matched in the module code you wish to patch. Prefer only using a RegExp if a simple string test is not enough */
|
||||||
|
find: string | RegExp;
|
||||||
|
/** The replacement(s) for the module being patched */
|
||||||
replacement: PatchReplacement | PatchReplacement[];
|
replacement: PatchReplacement | PatchReplacement[];
|
||||||
/** Whether this patch should apply to multiple modules */
|
/** Whether this patch should apply to multiple modules */
|
||||||
all?: boolean;
|
all?: boolean;
|
||||||
|
@ -44,6 +49,7 @@ export interface Patch {
|
||||||
noWarn?: boolean;
|
noWarn?: boolean;
|
||||||
/** Only apply this set of replacements if all of them succeed. Use this if your replacements depend on each other */
|
/** Only apply this set of replacements if all of them succeed. Use this if your replacements depend on each other */
|
||||||
group?: boolean;
|
group?: boolean;
|
||||||
|
/** A function which returns whether this patch should be applied */
|
||||||
predicate?(): boolean;
|
predicate?(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,12 +28,7 @@ export const Flux: t.Flux = findByPropsLazy("connectStores");
|
||||||
|
|
||||||
export type GenericStore = t.FluxStore & Record<string, any>;
|
export type GenericStore = t.FluxStore & Record<string, any>;
|
||||||
|
|
||||||
export enum DraftType {
|
export const { DraftType }: { DraftType: typeof t.DraftType; } = findByPropsLazy("DraftType");
|
||||||
ChannelMessage = 0,
|
|
||||||
ThreadSettings = 1,
|
|
||||||
FirstThreadMessage = 2,
|
|
||||||
ApplicationLauncherCommand = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
export let MessageStore: Omit<Stores.MessageStore, "getMessages"> & {
|
export let MessageStore: Omit<Stores.MessageStore, "getMessages"> & {
|
||||||
getMessages(chanId: string): any;
|
getMessages(chanId: string): any;
|
||||||
|
@ -65,7 +60,6 @@ export let DraftStore: t.DraftStore;
|
||||||
/**
|
/**
|
||||||
* React hook that returns stateful data for one or more stores
|
* 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
|
* You might need a custom comparator (4th argument) if your store data is an object
|
||||||
*
|
|
||||||
* @param stores The stores to listen to
|
* @param stores The stores to listen to
|
||||||
* @param mapper A function that returns the data you need
|
* @param mapper A function that returns the data you need
|
||||||
* @param dependencies An array of reactive values which the hook depends on. Use this if your mapper or equality function depends on the value of another hook
|
* @param dependencies An array of reactive values which the hook depends on. Use this if your mapper or equality function depends on the value of another hook
|
||||||
|
@ -73,13 +67,13 @@ export let DraftStore: t.DraftStore;
|
||||||
*
|
*
|
||||||
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
|
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const useStateFromStores = proxyLazy(() => findByProps("useStateFromStores").useStateFromStores) as <T>(
|
export const useStateFromStores = proxyLazy(() => findByProps("useStateFromStores").useStateFromStores) as <T>(
|
||||||
stores: t.FluxStore[],
|
stores: t.FluxStore[],
|
||||||
mapper: () => T,
|
mapper: () => T,
|
||||||
dependencies?: any,
|
dependencies?: any,
|
||||||
isEqual?: (old: T, newer: T) => boolean
|
isEqual?: (old: T, newer: T) => boolean
|
||||||
) => T;
|
) => T;
|
||||||
// why the fuck cant i get rid of this stupid fucking conflict
|
|
||||||
|
|
||||||
waitForStore("DraftStore", s => DraftStore = s);
|
waitForStore("DraftStore", s => DraftStore = s);
|
||||||
waitForStore("UserStore", s => UserStore = s);
|
waitForStore("UserStore", s => UserStore = s);
|
||||||
|
|
16
src/webpack/common/types/stores.d.ts
vendored
16
src/webpack/common/types/stores.d.ts
vendored
|
@ -173,6 +173,15 @@ export class DraftStore extends FluxStore {
|
||||||
getThreadSettings(channelId: string): any | null;
|
getThreadSettings(channelId: string): any | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DraftType {
|
||||||
|
ChannelMessage,
|
||||||
|
ThreadSettings,
|
||||||
|
FirstThreadMessage,
|
||||||
|
ApplicationLauncherCommand,
|
||||||
|
Poll,
|
||||||
|
SlashCommand,
|
||||||
|
}
|
||||||
|
|
||||||
export class GuildStore extends FluxStore {
|
export class GuildStore extends FluxStore {
|
||||||
getGuild(guildId: string): Guild;
|
getGuild(guildId: string): Guild;
|
||||||
getGuildCount(): number;
|
getGuildCount(): number;
|
||||||
|
@ -182,3 +191,10 @@ export class GuildStore extends FluxStore {
|
||||||
getRoles(guildId: string): Record<string, Role>;
|
getRoles(guildId: string): Record<string, Role>;
|
||||||
getAllGuildRoles(): Record<string, Record<string, Role>>;
|
getAllGuildRoles(): Record<string, Record<string, Role>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type useStateFromStores = <T>(
|
||||||
|
stores: t.FluxStore[],
|
||||||
|
mapper: () => T,
|
||||||
|
dependencies?: any,
|
||||||
|
isEqual?: (old: T, newer: T) => boolean
|
||||||
|
) => T;
|
||||||
|
|
|
@ -119,6 +119,8 @@ export function showToast(message: string, type = ToastType.MESSAGE) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserUtils = findByPropsLazy("getUser", "fetchCurrentUser") as { getUser: (id: string) => Promise<User>; };
|
export const UserUtils = findByPropsLazy("getUser", "fetchCurrentUser") as { getUser: (id: string) => Promise<User>; };
|
||||||
|
|
||||||
|
export const UploadManager = findByPropsLazy("clearAll", "addFile");
|
||||||
export const UploadHandler = findByPropsLazy("showUploadFileSizeExceededError", "promptToUpload") as {
|
export const UploadHandler = findByPropsLazy("showUploadFileSizeExceededError", "promptToUpload") as {
|
||||||
promptToUpload: (files: File[], channel: Channel, draftType: Number) => void;
|
promptToUpload: (files: File[], channel: Channel, draftType: Number) => void;
|
||||||
};
|
};
|
||||||
|
|
|
@ -257,7 +257,12 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
|
||||||
for (let i = 0; i < patches.length; i++) {
|
for (let i = 0; i < patches.length; i++) {
|
||||||
const patch = patches[i];
|
const patch = patches[i];
|
||||||
if (patch.predicate && !patch.predicate()) continue;
|
if (patch.predicate && !patch.predicate()) continue;
|
||||||
if (!code.includes(patch.find)) continue;
|
|
||||||
|
const moduleMatches = typeof patch.find === "string"
|
||||||
|
? code.includes(patch.find)
|
||||||
|
: patch.find.test(code);
|
||||||
|
|
||||||
|
if (!moduleMatches) continue;
|
||||||
|
|
||||||
patchedBy.add(patch.plugin);
|
patchedBy.add(patch.plugin);
|
||||||
|
|
||||||
|
|
|
@ -432,7 +432,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, rawChunkIds, entryPointId] = match;
|
const [, rawChunkIds, entryPointId] = match;
|
||||||
if (Number.isNaN(entryPointId)) {
|
if (Number.isNaN(Number(entryPointId))) {
|
||||||
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
|
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
|
||||||
logger.warn(err, "Code:", code, "Matcher:", matcher);
|
logger.warn(err, "Code:", code, "Matcher:", matcher);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue