Merge branch 'dev' into main

This commit is contained in:
byeoon 2024-05-15 09:47:42 -04:00 committed by GitHub
commit e9cb2eccec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 1770 additions and 411 deletions

View file

@ -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"
] ]
} }

View file

@ -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": {

View file

@ -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;

View file

@ -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";

View file

@ -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";

View file

@ -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")}>

View file

@ -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}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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");

View file

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

View file

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

View file

@ -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;
}; };

View file

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

View file

@ -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) {

View file

@ -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[]) {

View file

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

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

View file

@ -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())}

View file

@ -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
} }
] ]
}], }],

View file

@ -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,

View file

@ -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

View file

@ -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, "", ""]

View file

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

View file

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

View file

@ -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]
} }

View file

@ -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;
} }

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

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

View file

@ -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

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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" +
")" + ")" +

View file

@ -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[] {

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import ErrorBoundary from "@components/ErrorBoundary";
import { 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>
); );
} })
}); });

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

View file

@ -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");

View file

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

View file

@ -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>

View file

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

View file

@ -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 };

View file

@ -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: [
{ {

View file

@ -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();
} }

View file

@ -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;

View file

@ -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);

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

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

View file

@ -0,0 +1,3 @@
.vc-reply-timestamp {
margin-right: 0.25em;
}

View file

@ -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

View file

@ -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,

View file

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

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

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

View file

@ -0,0 +1,4 @@
.vc-std-wrapper {
display: flex;
align-items: center;
}

View file

@ -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"',

View file

@ -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

View file

@ -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)

View file

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

View file

@ -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;

View file

@ -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;

View file

@ -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 }
}) })
} }

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

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

View file

@ -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}

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

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

View file

@ -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,

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

View file

@ -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

View file

@ -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;
} }

View file

@ -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

View file

@ -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,

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

View file

@ -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"

View file

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

View file

@ -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);

View file

@ -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;
} }

View file

@ -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);

View file

@ -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;

View file

@ -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;
}; };

View file

@ -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);

View file

@ -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);