Merge branch 'dev' into feat/usercss
This commit is contained in:
commit
c25e8ac8c1
37 changed files with 598 additions and 77 deletions
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
|
@ -1,9 +1,6 @@
|
||||||
name: test
|
name: test
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": "explicit"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.6.4",
|
"version": "1.6.5",
|
||||||
"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": {
|
||||||
|
|
|
@ -76,7 +76,11 @@ const globNativesPlugin = {
|
||||||
if (!await existsAsync(dirPath)) continue;
|
if (!await existsAsync(dirPath)) continue;
|
||||||
const plugins = await readdir(dirPath);
|
const plugins = await readdir(dirPath);
|
||||||
for (const p of plugins) {
|
for (const p of plugins) {
|
||||||
if (!await existsAsync(join(dirPath, p, "native.ts"))) continue;
|
const nativePath = join(dirPath, p, "native.ts");
|
||||||
|
const indexNativePath = join(dirPath, p, "native/index.ts");
|
||||||
|
|
||||||
|
if (!(await existsAsync(nativePath)) && !(await existsAsync(indexNativePath)))
|
||||||
|
continue;
|
||||||
|
|
||||||
const nameParts = p.split(".");
|
const nameParts = p.split(".");
|
||||||
const namePartsWithoutTarget = nameParts.length === 1 ? nameParts : nameParts.slice(0, -1);
|
const namePartsWithoutTarget = nameParts.length === 1 ? nameParts : nameParts.slice(0, -1);
|
||||||
|
|
|
@ -335,15 +335,15 @@ function runTime(token: string) {
|
||||||
await (wreq as any).el(sym);
|
await (wreq as any).el(sym);
|
||||||
delete Object.prototype[sym];
|
delete Object.prototype[sym];
|
||||||
|
|
||||||
const validChunksEntryPoints = [] as string[];
|
const validChunksEntryPoints = new Set<string>();
|
||||||
const validChunks = [] as string[];
|
const validChunks = new Set<string>();
|
||||||
const invalidChunks = [] as string[];
|
const invalidChunks = new Set<string>();
|
||||||
|
|
||||||
if (!chunks) throw new Error("Failed to get chunks");
|
if (!chunks) throw new Error("Failed to get chunks");
|
||||||
|
|
||||||
chunksLoop:
|
|
||||||
for (const entryPoint in chunks) {
|
for (const entryPoint in chunks) {
|
||||||
const chunkIds = chunks[entryPoint];
|
const chunkIds = chunks[entryPoint];
|
||||||
|
let invalidEntryPoint = false;
|
||||||
|
|
||||||
for (const id of chunkIds) {
|
for (const id of chunkIds) {
|
||||||
if (!wreq.u(id)) continue;
|
if (!wreq.u(id)) continue;
|
||||||
|
@ -353,14 +353,16 @@ function runTime(token: string) {
|
||||||
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
||||||
|
|
||||||
if (isWasm) {
|
if (isWasm) {
|
||||||
invalidChunks.push(id);
|
invalidChunks.add(id);
|
||||||
continue chunksLoop;
|
invalidEntryPoint = true;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
validChunks.push(id);
|
validChunks.add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
validChunksEntryPoints.push(entryPoint);
|
if (!invalidEntryPoint)
|
||||||
|
validChunksEntryPoints.add(entryPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entryPoint of validChunksEntryPoints) {
|
for (const entryPoint of validChunksEntryPoints) {
|
||||||
|
@ -373,7 +375,7 @@ function runTime(token: string) {
|
||||||
const allChunks = Function("return " + (wreq.u.toString().match(/(?<=\()\{.+?\}/s)?.[0] ?? "null"))() as Record<string | number, string[]> | null;
|
const allChunks = Function("return " + (wreq.u.toString().match(/(?<=\()\{.+?\}/s)?.[0] ?? "null"))() as Record<string | number, string[]> | null;
|
||||||
if (!allChunks) throw new Error("Failed to get all chunks");
|
if (!allChunks) throw new Error("Failed to get all chunks");
|
||||||
const chunksLeft = Object.keys(allChunks).filter(id => {
|
const chunksLeft = Object.keys(allChunks).filter(id => {
|
||||||
return !(validChunks.includes(id) || invalidChunks.includes(id));
|
return !(validChunks.has(id) || invalidChunks.has(id));
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const id of chunksLeft) {
|
for (const id of chunksLeft) {
|
||||||
|
|
|
@ -25,7 +25,7 @@ import type { PartialDeep } from "type-fest";
|
||||||
|
|
||||||
import { Argument } from "./types";
|
import { Argument } from "./types";
|
||||||
|
|
||||||
const MessageSender = findByPropsLazy("receiveMessage");
|
const MessageCreator = findByPropsLazy("createBotMessage");
|
||||||
|
|
||||||
export function generateId() {
|
export function generateId() {
|
||||||
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
|
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
|
||||||
|
@ -38,9 +38,9 @@ export function generateId() {
|
||||||
* @returns {Message}
|
* @returns {Message}
|
||||||
*/
|
*/
|
||||||
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
|
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
|
||||||
const botMessage = MessageActions.createBotMessage({ channelId, content: "", embeds: [] });
|
const botMessage = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] });
|
||||||
|
|
||||||
MessageSender.receiveMessage(channelId, mergeDefaults(message, botMessage));
|
MessageActions.receiveMessage(channelId, mergeDefaults(message, botMessage));
|
||||||
|
|
||||||
return message as Message;
|
return message as Message;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,21 @@ export interface Settings {
|
||||||
frameless: boolean;
|
frameless: boolean;
|
||||||
transparent: boolean;
|
transparent: boolean;
|
||||||
winCtrlQ: boolean;
|
winCtrlQ: boolean;
|
||||||
macosTranslucency: boolean;
|
macosVibrancyStyle:
|
||||||
|
| "content"
|
||||||
|
| "fullscreen-ui"
|
||||||
|
| "header"
|
||||||
|
| "hud"
|
||||||
|
| "menu"
|
||||||
|
| "popover"
|
||||||
|
| "selection"
|
||||||
|
| "sidebar"
|
||||||
|
| "titlebar"
|
||||||
|
| "tooltip"
|
||||||
|
| "under-page"
|
||||||
|
| "window"
|
||||||
|
| undefined;
|
||||||
|
macosTranslucency: boolean | undefined;
|
||||||
disableMinSize: boolean;
|
disableMinSize: boolean;
|
||||||
winNativeTitleBar: boolean;
|
winNativeTitleBar: boolean;
|
||||||
plugins: {
|
plugins: {
|
||||||
|
@ -80,7 +94,9 @@ const DefaultSettings: Settings = {
|
||||||
frameless: false,
|
frameless: false,
|
||||||
transparent: false,
|
transparent: false,
|
||||||
winCtrlQ: false,
|
winCtrlQ: false,
|
||||||
macosTranslucency: false,
|
// Replaced by macosVibrancyStyle
|
||||||
|
macosTranslucency: undefined,
|
||||||
|
macosVibrancyStyle: undefined,
|
||||||
disableMinSize: false,
|
disableMinSize: false,
|
||||||
winNativeTitleBar: false,
|
winNativeTitleBar: false,
|
||||||
plugins: {},
|
plugins: {},
|
||||||
|
|
|
@ -108,7 +108,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
||||||
function renderDiff() {
|
function renderDiff() {
|
||||||
return diff?.map(p => {
|
return diff?.map(p => {
|
||||||
const color = p.added ? "lime" : p.removed ? "red" : "grey";
|
const color = p.added ? "lime" : p.removed ? "red" : "grey";
|
||||||
return <div style={{ color, userSelect: "text" }}>{p.value}</div>;
|
return <div style={{ color, userSelect: "text", wordBreak: "break-all", lineBreak: "anywhere" }}>{p.value}</div>;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,15 @@ function VencordSettings() {
|
||||||
|
|
||||||
const isWindows = navigator.platform.toLowerCase().startsWith("win");
|
const isWindows = navigator.platform.toLowerCase().startsWith("win");
|
||||||
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
const isMac = navigator.platform.toLowerCase().startsWith("mac");
|
||||||
|
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
|
||||||
|
|
||||||
|
// One-time migration of the old setting to the new one if necessary.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (settings.macosTranslucency === true && !settings.macosVibrancyStyle) {
|
||||||
|
settings.macosVibrancyStyle = "sidebar";
|
||||||
|
settings.macosTranslucency = undefined;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const Switches: Array<false | {
|
const Switches: Array<false | {
|
||||||
key: KeysOfType<typeof settings, boolean>;
|
key: KeysOfType<typeof settings, boolean>;
|
||||||
|
@ -89,11 +98,6 @@ function VencordSettings() {
|
||||||
title: "Disable minimum window size",
|
title: "Disable minimum window size",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
},
|
},
|
||||||
IS_DISCORD_DESKTOP && isMac && {
|
|
||||||
key: "macosTranslucency",
|
|
||||||
title: "Enable translucent window",
|
|
||||||
note: "Requires a full restart"
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -152,6 +156,71 @@ function VencordSettings() {
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
|
|
||||||
|
|
||||||
|
{needsVibrancySettings && <>
|
||||||
|
<Forms.FormTitle tag="h5">Window vibrancy style (requires restart)</Forms.FormTitle>
|
||||||
|
<Select
|
||||||
|
className={Margins.bottom20}
|
||||||
|
placeholder="Window vibrancy style"
|
||||||
|
options={[
|
||||||
|
// Sorted from most opaque to most transparent
|
||||||
|
{
|
||||||
|
label: "No vibrancy", default: !settings.macosTranslucency, value: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Under Page (window tinting)",
|
||||||
|
value: "under-page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Content",
|
||||||
|
value: "content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Window",
|
||||||
|
value: "window"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Selection",
|
||||||
|
value: "selection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Titlebar",
|
||||||
|
value: "titlebar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Header",
|
||||||
|
value: "header"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Sidebar (old value for transparent windows)",
|
||||||
|
value: "sidebar",
|
||||||
|
default: settings.macosTranslucency
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tooltip",
|
||||||
|
value: "tooltip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Menu",
|
||||||
|
value: "menu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Popover",
|
||||||
|
value: "popover"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Fullscreen UI (transparent but slightly muted)",
|
||||||
|
value: "fullscreen-ui"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "HUD (Most transparent)",
|
||||||
|
value: "hud"
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
select={v => settings.macosVibrancyStyle = v}
|
||||||
|
isSelected={v => settings.macosVibrancyStyle === v}
|
||||||
|
serialize={identity} />
|
||||||
|
</>}
|
||||||
|
|
||||||
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
|
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
|
||||||
</SettingsTab>
|
</SettingsTab>
|
||||||
);
|
);
|
||||||
|
|
|
@ -85,9 +85,15 @@ if (!IS_VANILLA) {
|
||||||
options.backgroundColor = "#00000000";
|
options.backgroundColor = "#00000000";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.macosTranslucency && process.platform === "darwin") {
|
const needsVibrancy = process.platform === "darwin" || (settings.macosVibrancyStyle || settings.macosTranslucency);
|
||||||
|
|
||||||
|
if (needsVibrancy) {
|
||||||
options.backgroundColor = "#00000000";
|
options.backgroundColor = "#00000000";
|
||||||
options.vibrancy = "sidebar";
|
if (settings.macosTranslucency) {
|
||||||
|
options.vibrancy = "sidebar";
|
||||||
|
} else if (settings.macosVibrancyStyle) {
|
||||||
|
options.vibrancy = settings.macosVibrancyStyle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.env.DISCORD_PRELOAD = original;
|
process.env.DISCORD_PRELOAD = original;
|
||||||
|
|
|
@ -46,6 +46,13 @@ export default definePlugin({
|
||||||
match: /(?<=\.activityEmoji,.+?animate:)\i/,
|
match: /(?<=\.activityEmoji,.+?animate:)\i/,
|
||||||
replace: "!0"
|
replace: "!0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".animatedBannerHoverLayer,onMouseEnter:",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=guildBanner:\i,animate:)\i/,
|
||||||
|
replace: "!0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -124,6 +124,18 @@ export const defaultRules = [
|
||||||
"t@*.x.com",
|
"t@*.x.com",
|
||||||
"s@*.x.com",
|
"s@*.x.com",
|
||||||
"ref_*@*.x.com",
|
"ref_*@*.x.com",
|
||||||
|
"t@*.fixupx.com",
|
||||||
|
"s@*.fixupx.com",
|
||||||
|
"ref_*@*.fixupx.com",
|
||||||
|
"t@*.fxtwitter.com",
|
||||||
|
"s@*.fxtwitter.com",
|
||||||
|
"ref_*@*.fxtwitter.com",
|
||||||
|
"t@*.twittpr.com",
|
||||||
|
"s@*.twittpr.com",
|
||||||
|
"ref_*@*.twittpr.com",
|
||||||
|
"t@*.fixvx.com",
|
||||||
|
"s@*.fixvx.com",
|
||||||
|
"ref_*@*.fixvx.com",
|
||||||
"tt_medium",
|
"tt_medium",
|
||||||
"tt_content",
|
"tt_content",
|
||||||
"lr@yandex.*",
|
"lr@yandex.*",
|
||||||
|
|
|
@ -60,7 +60,7 @@ async function embedDidMount(this: Component<Props>) {
|
||||||
|
|
||||||
if (hasTitle) {
|
if (hasTitle) {
|
||||||
embed.dearrow.oldTitle = embed.rawTitle;
|
embed.dearrow.oldTitle = embed.rawTitle;
|
||||||
embed.rawTitle = titles[0].title;
|
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasThumb) {
|
if (hasThumb) {
|
||||||
|
|
|
@ -215,6 +215,9 @@ function initWs(isManual = false) {
|
||||||
case "ModuleId":
|
case "ModuleId":
|
||||||
results = Object.keys(search(parsedArgs[0]));
|
results = Object.keys(search(parsedArgs[0]));
|
||||||
break;
|
break;
|
||||||
|
case "ComponentByCode":
|
||||||
|
results = findAll(filters.componentByCode(...parsedArgs));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return reply("Unknown Find Type " + type);
|
return reply("Unknown Find Type " + type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -359,7 +359,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// Separate patch for allowing using custom app icons
|
// Separate patch for allowing using custom app icons
|
||||||
{
|
{
|
||||||
find: "location:\"AppIconHome\"",
|
find: ".FreemiumAppIconIds.DEFAULT&&(",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
|
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
|
||||||
replace: "true"
|
replace: "true"
|
||||||
|
@ -787,7 +787,14 @@ export default definePlugin({
|
||||||
if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId))
|
if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId))
|
||||||
break stickerBypass;
|
break stickerBypass;
|
||||||
|
|
||||||
const link = this.getStickerLink(sticker.id);
|
// [12/12/2023]
|
||||||
|
// Work around an annoying bug where getStickerLink will return StickerType.GIF,
|
||||||
|
// but will give us a normal non animated png for no reason
|
||||||
|
// TODO: Remove this workaround when it's not needed anymore
|
||||||
|
let link = this.getStickerLink(sticker.id);
|
||||||
|
if (sticker.format_type === StickerType.GIF && link.includes(".png")) {
|
||||||
|
link = link.replace(".png", ".gif");
|
||||||
|
}
|
||||||
if (sticker.format_type === StickerType.APNG) {
|
if (sticker.format_type === StickerType.APNG) {
|
||||||
this.sendAnimatedSticker(link, sticker.id, channelId);
|
this.sendAnimatedSticker(link, sticker.id, channelId);
|
||||||
return { cancel: true };
|
return { cancel: true };
|
||||||
|
|
|
@ -14,10 +14,12 @@ export default definePlugin({
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "handleImageLoad=",
|
find: "handleImageLoad=",
|
||||||
replacement: {
|
replacement: [
|
||||||
match: /(?<=getSrc\(\i\){.+?format:)\i/,
|
{
|
||||||
replace: "null"
|
match: /(?<=getSrc\(\i\){.+?return )\i\.SUPPORTS_WEBP.+?:(?=\i&&\(\i="png"\))/,
|
||||||
}
|
replace: ""
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,21 +28,22 @@ import style from "./style.css?managed";
|
||||||
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
|
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
|
||||||
|
|
||||||
function makeIcon(showCurrentGame?: boolean) {
|
function makeIcon(showCurrentGame?: boolean) {
|
||||||
|
const controllerIcon = "M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z";
|
||||||
return function () {
|
return function () {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||||
width="20"
|
{showCurrentGame ? (
|
||||||
height="20"
|
<path fill="currentColor" d={controllerIcon} />
|
||||||
viewBox="0 0 24 24"
|
) : (
|
||||||
>
|
<>
|
||||||
<path fill="currentColor" mask="url(#gameActivityMask)" d="M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z" />
|
<mask id="gameActivityMask" >
|
||||||
{!showCurrentGame && <>
|
<rect fill="white" x="0" y="0" width="24" height="24" />
|
||||||
<mask id="gameActivityMask" >
|
<path fill="black" d="M23.27 4.73 19.27 .73 -.27 20.27 3.73 24.27Z" />
|
||||||
<rect fill="white" x="0" y="0" width="24" height="24" />
|
</mask>
|
||||||
<path fill="black" d="M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z" />
|
<path fill="var(--status-danger)" mask="url(#gameActivityMask)" d={controllerIcon} />
|
||||||
</mask>
|
<path fill="var(--status-danger)" d="M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4Z" />
|
||||||
<path fill="var(--status-danger)" d="M23 2.27 21.73 1 1 21.73 2.27 23 23 2.27Z" />
|
</>
|
||||||
</>}
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,7 +22,7 @@ import definePlugin from "@utils/types";
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "iLoveSpam",
|
name: "iLoveSpam",
|
||||||
description: "Do not hide messages from 'likely spammers'",
|
description: "Do not hide messages from 'likely spammers'",
|
||||||
authors: [Devs.botato, Devs.Animal],
|
authors: [Devs.botato, Devs.Nyako],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "hasFlag:{writable",
|
find: "hasFlag:{writable",
|
||||||
|
|
|
@ -72,6 +72,7 @@ export default definePlugin({
|
||||||
if (event.detail < 2) return;
|
if (event.detail < 2) return;
|
||||||
if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return;
|
if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return;
|
||||||
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;
|
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;
|
||||||
|
if (msg.deleted === true) return;
|
||||||
|
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id)) return;
|
if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id)) return;
|
||||||
|
@ -81,6 +82,9 @@ export default definePlugin({
|
||||||
} else {
|
} else {
|
||||||
if (!settings.store.enableDoubleClickToReply) return;
|
if (!settings.store.enableDoubleClickToReply) return;
|
||||||
|
|
||||||
|
const EPHEMERAL = 64;
|
||||||
|
if (msg.hasFlag(EPHEMERAL)) return;
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
type: "CREATE_PENDING_REPLY",
|
type: "CREATE_PENDING_REPLY",
|
||||||
channel,
|
channel,
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
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 { findByProps } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
|
||||||
|
const { updateGuildNotificationSettings } = findByPropsLazy("updateGuildNotificationSettings");
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
guild: {
|
guild: {
|
||||||
|
@ -63,7 +65,7 @@ export default definePlugin({
|
||||||
|
|
||||||
handleMute(guildId: string | null) {
|
handleMute(guildId: string | null) {
|
||||||
if (guildId === "@me" || guildId === "null" || guildId == null) return;
|
if (guildId === "@me" || guildId === "null" || guildId == null) return;
|
||||||
findByProps("updateGuildNotificationSettings").updateGuildNotificationSettings(guildId,
|
updateGuildNotificationSettings(guildId,
|
||||||
{
|
{
|
||||||
muted: settings.store.guild,
|
muted: settings.store.guild,
|
||||||
suppress_everyone: settings.store.everyone,
|
suppress_everyone: settings.store.everyone,
|
||||||
|
|
3
src/plugins/notificationVolume/README.md
Normal file
3
src/plugins/notificationVolume/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# NotificationVolume
|
||||||
|
|
||||||
|
Set a separate volume for notifications and in-app sounds (e.g. messages, call sound, mute/unmute) helping your ears stay healthy for many years to come.
|
35
src/plugins/notificationVolume/index.ts
Normal file
35
src/plugins/notificationVolume/index.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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({
|
||||||
|
notificationVolume: {
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
description: "Notification volume",
|
||||||
|
markers: [0, 25, 50, 75, 100],
|
||||||
|
default: 100,
|
||||||
|
stickToMarkers: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "NotificationVolume",
|
||||||
|
description: "Save your ears and set a separate volume for notifications and in-app sounds",
|
||||||
|
authors: [Devs.philipbry],
|
||||||
|
settings,
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
find: "_ensureAudio(){",
|
||||||
|
replacement: {
|
||||||
|
match: /onloadeddata=\(\)=>\{.\.volume=/,
|
||||||
|
replace: "$&$self.settings.store.notificationVolume/100*"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
|
@ -28,7 +28,8 @@ export default definePlugin({
|
||||||
start() {
|
start() {
|
||||||
fetch("https://raw.githubusercontent.com/adryd325/oneko.js/8fa8a1864aa71cd7a794d58bc139e755e96a236c/oneko.js")
|
fetch("https://raw.githubusercontent.com/adryd325/oneko.js/8fa8a1864aa71cd7a794d58bc139e755e96a236c/oneko.js")
|
||||||
.then(x => x.text())
|
.then(x => x.text())
|
||||||
.then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.gif"))
|
.then(s => s.replace("./oneko.gif", "https://raw.githubusercontent.com/adryd325/oneko.js/14bab15a755d0e35cd4ae19c931d96d306f99f42/oneko.gif")
|
||||||
|
.replace("(isReducedMotion)", "(false)"))
|
||||||
.then(eval);
|
.then(eval);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -55,13 +55,13 @@ const Icons = {
|
||||||
};
|
};
|
||||||
type Platform = keyof typeof Icons;
|
type Platform = keyof typeof Icons;
|
||||||
|
|
||||||
const StatusUtils = findByPropsLazy("getStatusColor", "StatusTypes");
|
const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes");
|
||||||
|
|
||||||
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
|
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
|
||||||
const tooltip = platform[0].toUpperCase() + platform.slice(1);
|
const tooltip = platform[0].toUpperCase() + platform.slice(1);
|
||||||
const Icon = Icons[platform] ?? Icons.desktop;
|
const Icon = Icons[platform] ?? Icons.desktop;
|
||||||
|
|
||||||
return <Icon color={`var(--${StatusUtils.getStatusColor(status)}`} tooltip={tooltip} small={small} />;
|
return <Icon color={StatusUtils.useStatusFillColor(status)} tooltip={tooltip} small={small} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatus = (id: string): Record<Platform, string> => PresenceStore.getState()?.clientStatuses?.[id];
|
const getStatus = (id: string): Record<Platform, string> => PresenceStore.getState()?.clientStatuses?.[id];
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default definePlugin({
|
||||||
start() {
|
start() {
|
||||||
addButton("QuickMention", msg => {
|
addButton("QuickMention", msg => {
|
||||||
const channel = ChannelStore.getChannel(msg.channel_id);
|
const channel = ChannelStore.getChannel(msg.channel_id);
|
||||||
if (!PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null;
|
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: "Quick Mention",
|
label: "Quick Mention",
|
||||||
|
|
|
@ -18,27 +18,28 @@
|
||||||
|
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
let ERROR_CODES: any;
|
let ERROR_CODES: any;
|
||||||
const CODES_URL =
|
|
||||||
"https://raw.githubusercontent.com/facebook/react/17.0.2/scripts/error-codes/codes.json";
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ReactErrorDecoder",
|
name: "ReactErrorDecoder",
|
||||||
description: 'Replaces "Minifed React Error" with the actual error.',
|
description: 'Replaces "Minifed React Error" with the actual error.',
|
||||||
authors: [Devs.Cyn],
|
authors: [Devs.Cyn, Devs.maisymoe],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '"https://reactjs.org/docs/error-decoder.html?invariant="',
|
find: '"https://reactjs.org/docs/error-decoder.html?invariant="',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(function .\(.\)){(for\(var .="https:\/\/reactjs\.org\/docs\/error-decoder\.html\?invariant="\+.,.=1;.<arguments\.length;.\+\+\).\+="&args\[\]="\+encodeURIComponent\(arguments\[.\]\);return"Minified React error #"\+.\+"; visit "\+.\+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings.")}/,
|
match: /(function .\(.\)){(for\(var .="https:\/\/reactjs\.org\/docs\/error-decoder\.html\?invariant="\+.,.=1;.<arguments\.length;.\+\+\).\+="&args\[\]="\+encodeURIComponent\(arguments\[.\]\);return"Minified React error #"\+.\+"; visit "\+.\+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings.")}/,
|
||||||
replace: (_, func, original) =>
|
replace: (_, func, original) =>
|
||||||
`${func}{var decoded=Vencord.Plugins.plugins.ReactErrorDecoder.decodeError.apply(null, arguments);if(decoded)return decoded;${original}}`,
|
`${func}{var decoded=$self.decodeError.apply(null, arguments);if(decoded)return decoded;${original}}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
|
const CODES_URL = `https://raw.githubusercontent.com/facebook/react/v${React.version}/scripts/error-codes/codes.json`;
|
||||||
|
|
||||||
ERROR_CODES = await fetch(CODES_URL)
|
ERROR_CODES = await fetch(CODES_URL)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.catch(e => console.error("[ReactErrorDecoder] Failed to fetch React error codes\n", e));
|
.catch(e => console.error("[ReactErrorDecoder] Failed to fetch React error codes\n", e));
|
||||||
|
|
|
@ -42,6 +42,13 @@ export default definePlugin({
|
||||||
match: /codeBlock:\{react\((\i),(\i),(\i)\)\{/,
|
match: /codeBlock:\{react\((\i),(\i),(\i)\)\{/,
|
||||||
replace: "$&return $self.renderHighlighter($1,$2,$3);"
|
replace: "$&return $self.renderHighlighter($1,$2,$3);"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: ".PREVIEW_NUM_LINES",
|
||||||
|
replacement: {
|
||||||
|
match: /(?<=function \i\((\i)\)\{)(?=let\{text:\i,language:)/,
|
||||||
|
replace: "return $self.renderHighlighter({lang:$1.language,content:$1.text});"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
start: async () => {
|
start: async () => {
|
||||||
|
|
|
@ -77,7 +77,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// Do not check for unreads when selecting the render level if the channel is hidden
|
// Do not check for unreads when selecting the render level if the channel is hidden
|
||||||
{
|
{
|
||||||
match: /(?=!\(0,\i\.getHasImportantUnread\)\(this\.record\))/,
|
match: /(?<=&&)(?=!\i\.\i\.hasUnread\(this\.record\.id\))/,
|
||||||
replace: "$self.isHiddenChannel(this.record)||"
|
replace: "$self.isHiddenChannel(this.record)||"
|
||||||
},
|
},
|
||||||
// Make channels we dont have access to be the same level as normal ones
|
// Make channels we dont have access to be the same level as normal ones
|
||||||
|
@ -334,12 +334,12 @@ export default definePlugin({
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Remove the divider and the open chat button for the HiddenChannelLockScreen
|
// Remove the divider and the open chat button for the HiddenChannelLockScreen
|
||||||
match: /"more-options-popout"\)\),(?<=let{channel:(\i).+?inCall:(\i).+?)/,
|
match: /"more-options-popout"\)\),(?<=channel:(\i).+?inCall:(\i).+?)/,
|
||||||
replace: (m, channel, inCall) => `${m}${inCall}||!$self.isHiddenChannel(${channel},true)&&`
|
replace: (m, channel, inCall) => `${m}${inCall}||!$self.isHiddenChannel(${channel},true)&&`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Remove invite users button for the HiddenChannelLockScreen
|
// Remove invite users button for the HiddenChannelLockScreen
|
||||||
match: /"popup".{0,100}?if\((?<=let{channel:(\i).+?inCall:(\i).+?)/,
|
match: /"popup".{0,100}?if\((?<=channel:(\i).+?inCall:(\i).+?)/,
|
||||||
replace: (m, channel, inCall) => `${m}(${inCall}||!$self.isHiddenChannel(${channel},true))&&`
|
replace: (m, channel, inCall) => `${m}(${inCall}||!$self.isHiddenChannel(${channel},true))&&`
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -55,13 +55,19 @@ export default definePlugin({
|
||||||
replace: "return [$self.renderPlayer(),$1]"
|
replace: "return [$self.renderPlayer(),$1]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Adds POST and a Marker to the SpotifyAPI (so we can easily find it)
|
|
||||||
{
|
{
|
||||||
find: ".PLAYER_DEVICES",
|
find: ".PLAYER_DEVICES",
|
||||||
replacement: {
|
replacement: [{
|
||||||
|
// Adds POST and a Marker to the SpotifyAPI (so we can easily find it)
|
||||||
match: /get:(\i)\.bind\(null,(\i\.\i)\.get\)/,
|
match: /get:(\i)\.bind\(null,(\i\.\i)\.get\)/,
|
||||||
replace: "post:$1.bind(null,$2.post),$&"
|
replace: "post:$1.bind(null,$2.post),$&"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
// Spotify Connect API returns status 202 instead of 204 when skipping tracks.
|
||||||
|
// Discord rejects 202 which causes the request to send twice. This patch prevents this.
|
||||||
|
match: /202===\i\.status/,
|
||||||
|
replace: "false",
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
// Discord doesn't give you the repeat kind, only a boolean
|
// Discord doesn't give you the repeat kind, only a boolean
|
||||||
{
|
{
|
||||||
|
|
|
@ -46,10 +46,10 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: ".hasAvailableBurstCurrency)",
|
find: ".trackEmojiSearchEmpty,200",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(?<=\.useBurstReactionsExperiment.{0,20})useState\(!1\)(?=.+?(\i===\i\.EmojiIntention.REACTION))/,
|
match: /(\.trackEmojiSearchEmpty,200(?=.+?isBurstReaction:(\i).+?(\i===\i\.EmojiIntention.REACTION)).+?\[\2,\i\]=\i\.useState\().+?\)/,
|
||||||
replace: "useState($self.settings.store.superReactByDefault && $1)"
|
replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.settings.store.superReactByDefault&&${isReactionIntention})`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -21,7 +21,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 { findExportedComponentLazy, findStoreLazy } from "@webpack";
|
import { findExportedComponentLazy, findStoreLazy } from "@webpack";
|
||||||
import { ChannelStore, GuildMemberStore, i18n, RelationshipStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
import { ChannelStore, GuildMemberStore, i18n, RelationshipStore, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
|
||||||
|
|
||||||
import { buildSeveralUsers } from "../typingTweaks";
|
import { buildSeveralUsers } from "../typingTweaks";
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
|
||||||
return oldKeys.length === currentKeys.length && currentKeys.every(key => old[key] != null);
|
return oldKeys.length === currentKeys.length && currentKeys.every(key => old[key] != null);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const currentChannelId: string = useStateFromStores([SelectedChannelStore], () => SelectedChannelStore.getChannelId());
|
||||||
const guildId = ChannelStore.getChannel(channelId).guild_id;
|
const guildId = ChannelStore.getChannel(channelId).guild_id;
|
||||||
|
|
||||||
if (!settings.store.includeMutedChannels) {
|
if (!settings.store.includeMutedChannels) {
|
||||||
|
@ -55,6 +55,10 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
|
||||||
if (isChannelMuted) return null;
|
if (isChannelMuted) return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!settings.store.includeCurrentChannel) {
|
||||||
|
if (currentChannelId === channelId) return null;
|
||||||
|
}
|
||||||
|
|
||||||
const myId = UserStore.getCurrentUser()?.id;
|
const myId = UserStore.getCurrentUser()?.id;
|
||||||
|
|
||||||
const typingUsersArray = Object.keys(typingUsers).filter(id => id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers));
|
const typingUsersArray = Object.keys(typingUsers).filter(id => id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers));
|
||||||
|
@ -101,6 +105,11 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
|
includeCurrentChannel: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Whether to show the typing indicator for the currently selected channel",
|
||||||
|
default: true
|
||||||
|
},
|
||||||
includeMutedChannels: {
|
includeMutedChannels: {
|
||||||
type: OptionType.BOOLEAN,
|
type: OptionType.BOOLEAN,
|
||||||
description: "Whether to show the typing indicator for muted channels.",
|
description: "Whether to show the typing indicator for muted channels.",
|
||||||
|
|
|
@ -20,9 +20,11 @@ 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 { saveFile } from "@utils/web";
|
import { saveFile } from "@utils/web";
|
||||||
import { findByProps } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { Clipboard, ComponentDispatch } from "@webpack/common";
|
import { Clipboard, ComponentDispatch } from "@webpack/common";
|
||||||
|
|
||||||
|
const ctxMenuCallbacks = findByPropsLazy("contextMenuCallbackNative");
|
||||||
|
|
||||||
async function fetchImage(url: string) {
|
async function fetchImage(url: string) {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (res.status !== 200) return;
|
if (res.status !== 200) return;
|
||||||
|
@ -55,7 +57,6 @@ export default definePlugin({
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (settings.store.addBack) {
|
if (settings.store.addBack) {
|
||||||
const ctxMenuCallbacks = findByProps("contextMenuCallbackNative");
|
|
||||||
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
|
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
|
||||||
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
|
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
|
||||||
this.changedListeners = true;
|
this.changedListeners = true;
|
||||||
|
@ -64,7 +65,6 @@ export default definePlugin({
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this.changedListeners) {
|
if (this.changedListeners) {
|
||||||
const ctxMenuCallbacks = findByProps("contextMenuCallbackNative");
|
|
||||||
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
|
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
|
||||||
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
|
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
|
||||||
}
|
}
|
||||||
|
|
15
src/plugins/xsOverlay.desktop/README.md
Normal file
15
src/plugins/xsOverlay.desktop/README.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# XSOverlay Notifier
|
||||||
|
|
||||||
|
Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR.
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40)
|
||||||
|
|
||||||
|
![](https://github.com/Vendicated/Vencord/assets/24845294/f15eff61-2d52-4620-bcab-808ecb1606d2)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
- Enable this plugin
|
||||||
|
- Set plugin settings as desired
|
||||||
|
- Open XSOverlay
|
||||||
|
- get ping spammed
|
288
src/plugins/xsOverlay.desktop/index.ts
Normal file
288
src/plugins/xsOverlay.desktop/index.ts
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
/*
|
||||||
|
* 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 { makeRange } from "@components/PluginSettings/components";
|
||||||
|
import { Devs } from "@utils/constants";
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import definePlugin, { OptionType, PluginNative } from "@utils/types";
|
||||||
|
import { findByPropsLazy } from "@webpack";
|
||||||
|
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
|
||||||
|
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
|
||||||
|
|
||||||
|
const enum ChannelTypes {
|
||||||
|
DM = 1,
|
||||||
|
GROUP_DM = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
guild_id: string,
|
||||||
|
attachments: MessageAttachment[],
|
||||||
|
author: User,
|
||||||
|
channel_id: string,
|
||||||
|
components: any[],
|
||||||
|
content: string,
|
||||||
|
edited_timestamp: string,
|
||||||
|
embeds: Embed[],
|
||||||
|
sticker_items?: Sticker[],
|
||||||
|
flags: number,
|
||||||
|
id: string,
|
||||||
|
member: GuildMember,
|
||||||
|
mention_everyone: boolean,
|
||||||
|
mention_roles: string[],
|
||||||
|
mentions: Mention[],
|
||||||
|
nonce: string,
|
||||||
|
pinned: false,
|
||||||
|
referenced_message: any,
|
||||||
|
timestamp: string,
|
||||||
|
tts: boolean,
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Mention {
|
||||||
|
avatar: string,
|
||||||
|
avatar_decoration_data: any,
|
||||||
|
discriminator: string,
|
||||||
|
global_name: string,
|
||||||
|
id: string,
|
||||||
|
public_flags: number,
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Sticker {
|
||||||
|
t: "Sticker";
|
||||||
|
description: string;
|
||||||
|
format_type: number;
|
||||||
|
guild_id: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tags: string;
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Call {
|
||||||
|
channel_id: string,
|
||||||
|
guild_id: string,
|
||||||
|
message_id: string,
|
||||||
|
region: string,
|
||||||
|
ringing: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MuteStore = findByPropsLazy("isSuppressEveryoneEnabled");
|
||||||
|
const XSLog = new Logger("XSOverlay");
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
ignoreBots: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Ignore messages from bots",
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
pingColor: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "User mention color",
|
||||||
|
default: "#7289da"
|
||||||
|
},
|
||||||
|
channelPingColor: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "Channel mention color",
|
||||||
|
default: "#8a2be2"
|
||||||
|
},
|
||||||
|
soundPath: {
|
||||||
|
type: OptionType.STRING,
|
||||||
|
description: "Notification sound (default/warning/error)",
|
||||||
|
default: "default"
|
||||||
|
},
|
||||||
|
timeout: {
|
||||||
|
type: OptionType.NUMBER,
|
||||||
|
description: "Notif duration (secs)",
|
||||||
|
default: 1.0,
|
||||||
|
},
|
||||||
|
opacity: {
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
description: "Notif opacity",
|
||||||
|
default: 1,
|
||||||
|
markers: makeRange(0, 1, 0.1)
|
||||||
|
},
|
||||||
|
volume: {
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
description: "Volume",
|
||||||
|
default: 0.2,
|
||||||
|
markers: makeRange(0, 1, 0.1)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Native = VencordNative.pluginHelpers.XsOverlay as PluginNative<typeof import("./native")>;
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
name: "XSOverlay",
|
||||||
|
description: "Forwards discord notifications to XSOverlay, for easy viewing in VR",
|
||||||
|
authors: [Devs.Nyako],
|
||||||
|
tags: ["vr", "notify"],
|
||||||
|
settings,
|
||||||
|
flux: {
|
||||||
|
CALL_UPDATE({ call }: { call: Call; }) {
|
||||||
|
if (call?.ringing?.includes(UserStore.getCurrentUser().id)) {
|
||||||
|
const channel = ChannelStore.getChannel(call.channel_id);
|
||||||
|
sendOtherNotif("Incoming call", `${channel.name} is calling you...`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MESSAGE_CREATE({ message, optimistic }: { message: Message; optimistic: boolean; }) {
|
||||||
|
// Apparently without this try/catch, discord's socket connection dies if any part of this errors
|
||||||
|
try {
|
||||||
|
if (optimistic) return;
|
||||||
|
const channel = ChannelStore.getChannel(message.channel_id);
|
||||||
|
if (!shouldNotify(message, channel)) return;
|
||||||
|
|
||||||
|
const pingColor = settings.store.pingColor.replaceAll("#", "").trim();
|
||||||
|
const channelPingColor = settings.store.channelPingColor.replaceAll("#", "").trim();
|
||||||
|
let finalMsg = message.content;
|
||||||
|
let titleString = "";
|
||||||
|
|
||||||
|
if (channel.guild_id) {
|
||||||
|
const guild = GuildStore.getGuild(channel.guild_id);
|
||||||
|
titleString = `${message.author.username} (${guild.name}, #${channel.name})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
switch (channel.type) {
|
||||||
|
case ChannelTypes.DM:
|
||||||
|
titleString = message.author.username.trim();
|
||||||
|
break;
|
||||||
|
case ChannelTypes.GROUP_DM:
|
||||||
|
const channelName = channel.name.trim() ?? channel.rawRecipients.map(e => e.username).join(", ");
|
||||||
|
titleString = `${message.author.username} (${channelName})`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.referenced_message) {
|
||||||
|
titleString += " (reply)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.embeds.length > 0) {
|
||||||
|
finalMsg += " [embed] ";
|
||||||
|
if (message.content === "") {
|
||||||
|
finalMsg = "sent message embed(s)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.sticker_items) {
|
||||||
|
finalMsg += " [sticker] ";
|
||||||
|
if (message.content === "") {
|
||||||
|
finalMsg = "sent a sticker";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const images = message.attachments.filter(e =>
|
||||||
|
typeof e?.content_type === "string"
|
||||||
|
&& e?.content_type.startsWith("image")
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
images.forEach(img => {
|
||||||
|
finalMsg += ` [image: ${img.filename}] `;
|
||||||
|
});
|
||||||
|
|
||||||
|
message.attachments.filter(a => a && !a.content_type?.startsWith("image")).forEach(a => {
|
||||||
|
finalMsg += ` [attachment: ${a.filename}] `;
|
||||||
|
});
|
||||||
|
|
||||||
|
// make mentions readable
|
||||||
|
if (message.mentions.length > 0) {
|
||||||
|
finalMsg = finalMsg.replace(/<@!?(\d{17,20})>/g, (_, id) => `<color=#${pingColor}><b>@${UserStore.getUser(id)?.username || "unknown-user"}</color></b>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.mention_roles.length > 0) {
|
||||||
|
for (const roleId of message.mention_roles) {
|
||||||
|
const role = GuildStore.getGuild(channel.guild_id).roles[roleId];
|
||||||
|
if (!role) continue;
|
||||||
|
const roleColor = role.colorString ?? `#${pingColor}`;
|
||||||
|
finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make emotes and channel mentions readable
|
||||||
|
const emoteMatches = finalMsg.match(new RegExp("(<a?:\\w+:\\d+>)", "g"));
|
||||||
|
const channelMatches = finalMsg.match(new RegExp("<(#\\d+)>", "g"));
|
||||||
|
|
||||||
|
if (emoteMatches) {
|
||||||
|
for (const eMatch of emoteMatches) {
|
||||||
|
finalMsg = finalMsg.replace(new RegExp(`${eMatch}`, "g"), `:${eMatch.split(":")[1]}:`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelMatches) {
|
||||||
|
for (const cMatch of channelMatches) {
|
||||||
|
let channelId = cMatch.split("<#")[1];
|
||||||
|
channelId = channelId.substring(0, channelId.length - 1);
|
||||||
|
finalMsg = finalMsg.replace(new RegExp(`${cMatch}`, "g"), `<b><color=#${channelPingColor}>#${ChannelStore.getChannel(channelId).name}</color></b>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMsgNotif(titleString, finalMsg, message);
|
||||||
|
} catch (err) {
|
||||||
|
XSLog.error(`Failed to catch MESSAGE_CREATE: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
const msgData = {
|
||||||
|
messageType: 1,
|
||||||
|
index: 0,
|
||||||
|
timeout: settings.store.timeout,
|
||||||
|
height: calculateHeight(cleanMessage(content)),
|
||||||
|
opacity: settings.store.opacity,
|
||||||
|
volume: settings.store.volume,
|
||||||
|
audioPath: settings.store.soundPath,
|
||||||
|
title: titleString,
|
||||||
|
content: content,
|
||||||
|
useBase64Icon: true,
|
||||||
|
icon: result,
|
||||||
|
sourceApp: "Vencord"
|
||||||
|
};
|
||||||
|
Native.sendToOverlay(msgData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendOtherNotif(content: string, titleString: string) {
|
||||||
|
const msgData = {
|
||||||
|
messageType: 1,
|
||||||
|
index: 0,
|
||||||
|
timeout: settings.store.timeout,
|
||||||
|
height: calculateHeight(cleanMessage(content)),
|
||||||
|
opacity: settings.store.opacity,
|
||||||
|
volume: settings.store.volume,
|
||||||
|
audioPath: settings.store.soundPath,
|
||||||
|
title: titleString,
|
||||||
|
content: content,
|
||||||
|
useBase64Icon: false,
|
||||||
|
icon: null,
|
||||||
|
sourceApp: "Vencord"
|
||||||
|
};
|
||||||
|
Native.sendToOverlay(msgData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldNotify(message: Message, channel: Channel) {
|
||||||
|
const currentUser = UserStore.getCurrentUser();
|
||||||
|
if (message.author.id === currentUser.id) return false;
|
||||||
|
if (message.author.bot && settings.store.ignoreBots) return false;
|
||||||
|
if (MuteStore.allowAllMessages(channel) || message.mention_everyone && !MuteStore.isSuppressEveryoneEnabled(message.guild_id)) return true;
|
||||||
|
|
||||||
|
return message.mentions.some(m => m.id === currentUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateHeight(content: string) {
|
||||||
|
if (content.length <= 100) return 100;
|
||||||
|
if (content.length <= 200) return 150;
|
||||||
|
if (content.length <= 300) return 200;
|
||||||
|
return 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanMessage(content: string) {
|
||||||
|
return content.replace(new RegExp("<[^>]*>", "g"), "");
|
||||||
|
}
|
16
src/plugins/xsOverlay.desktop/native.ts
Normal file
16
src/plugins/xsOverlay.desktop/native.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2023 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createSocket, Socket } from "dgram";
|
||||||
|
|
||||||
|
let xsoSocket: Socket;
|
||||||
|
|
||||||
|
export function sendToOverlay(_, data: any) {
|
||||||
|
data.icon = Buffer.from(data.icon).toString("base64");
|
||||||
|
const json = JSON.stringify(data);
|
||||||
|
xsoSocket ??= createSocket("udp4");
|
||||||
|
xsoSocket.send(json, 42069, "127.0.0.1");
|
||||||
|
}
|
|
@ -78,8 +78,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "Samu",
|
name: "Samu",
|
||||||
id: 702973430449832038n,
|
id: 702973430449832038n,
|
||||||
},
|
},
|
||||||
Animal: {
|
Nyako: {
|
||||||
name: "Animal",
|
name: "nyako",
|
||||||
id: 118437263754395652n
|
id: 118437263754395652n
|
||||||
},
|
},
|
||||||
MaiKokain: {
|
MaiKokain: {
|
||||||
|
@ -387,10 +387,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
name: "ant0n",
|
name: "ant0n",
|
||||||
id: 145224646868860928n
|
id: 145224646868860928n
|
||||||
},
|
},
|
||||||
|
philipbry: {
|
||||||
|
name: "philipbry",
|
||||||
|
id: 554994003318276106n
|
||||||
|
},
|
||||||
Korbo: {
|
Korbo: {
|
||||||
name: "Korbo",
|
name: "Korbo",
|
||||||
id: 455856406420258827n
|
id: 455856406420258827n
|
||||||
},
|
},
|
||||||
|
maisymoe: {
|
||||||
|
name: "maisy",
|
||||||
|
id: 257109471589957632n,
|
||||||
|
},
|
||||||
} satisfies Record<string, Dev>);
|
} satisfies Record<string, Dev>);
|
||||||
|
|
||||||
// iife so #__PURE__ works correctly
|
// iife so #__PURE__ works correctly
|
||||||
|
|
|
@ -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 { findByProps, findByPropsLazy } from "@webpack";
|
import { findByPropsLazy, findExportedComponentLazy } from "@webpack";
|
||||||
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
|
import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react";
|
||||||
|
|
||||||
import { LazyComponent } from "./react";
|
import { LazyComponent } from "./react";
|
||||||
|
@ -118,7 +118,7 @@ export type ImageModal = ComponentType<{
|
||||||
shouldHideMediaOptions?: boolean;
|
shouldHideMediaOptions?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const ImageModal = LazyComponent(() => findByProps("ImageModal").ImageModal as ImageModal);
|
export const ImageModal = findExportedComponentLazy("ImageModal") as ImageModal;
|
||||||
|
|
||||||
export const ModalRoot = LazyComponent(() => Modals.ModalRoot);
|
export const ModalRoot = LazyComponent(() => Modals.ModalRoot);
|
||||||
export const ModalHeader = LazyComponent(() => Modals.ModalHeader);
|
export const ModalHeader = LazyComponent(() => Modals.ModalHeader);
|
||||||
|
|
Loading…
Reference in a new issue