Merge branch 'dev' into feat/usercss

This commit is contained in:
Lewis Crichton 2023-12-13 23:20:31 +00:00 committed by GitHub
commit c25e8ac8c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 598 additions and 77 deletions

View file

@ -1,9 +1,6 @@
name: test
on:
push:
branches:
- main
- dev
pull_request:
branches:
- main

View file

@ -1,7 +1,7 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.6.4",
"version": "1.6.5",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {

View file

@ -76,7 +76,11 @@ const globNativesPlugin = {
if (!await existsAsync(dirPath)) continue;
const plugins = await readdir(dirPath);
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 namePartsWithoutTarget = nameParts.length === 1 ? nameParts : nameParts.slice(0, -1);

View file

@ -335,15 +335,15 @@ function runTime(token: string) {
await (wreq as any).el(sym);
delete Object.prototype[sym];
const validChunksEntryPoints = [] as string[];
const validChunks = [] as string[];
const invalidChunks = [] as string[];
const validChunksEntryPoints = new Set<string>();
const validChunks = new Set<string>();
const invalidChunks = new Set<string>();
if (!chunks) throw new Error("Failed to get chunks");
chunksLoop:
for (const entryPoint in chunks) {
const chunkIds = chunks[entryPoint];
let invalidEntryPoint = false;
for (const id of chunkIds) {
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"));
if (isWasm) {
invalidChunks.push(id);
continue chunksLoop;
invalidChunks.add(id);
invalidEntryPoint = true;
continue;
}
validChunks.push(id);
validChunks.add(id);
}
validChunksEntryPoints.push(entryPoint);
if (!invalidEntryPoint)
validChunksEntryPoints.add(entryPoint);
}
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;
if (!allChunks) throw new Error("Failed to get all chunks");
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) {

View file

@ -25,7 +25,7 @@ import type { PartialDeep } from "type-fest";
import { Argument } from "./types";
const MessageSender = findByPropsLazy("receiveMessage");
const MessageCreator = findByPropsLazy("createBotMessage");
export function generateId() {
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
@ -38,9 +38,9 @@ export function generateId() {
* @returns {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;
}

View file

@ -38,7 +38,21 @@ export interface Settings {
frameless: boolean;
transparent: 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;
winNativeTitleBar: boolean;
plugins: {
@ -80,7 +94,9 @@ const DefaultSettings: Settings = {
frameless: false,
transparent: false,
winCtrlQ: false,
macosTranslucency: false,
// Replaced by macosVibrancyStyle
macosTranslucency: undefined,
macosVibrancyStyle: undefined,
disableMinSize: false,
winNativeTitleBar: false,
plugins: {},

View file

@ -108,7 +108,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
function renderDiff() {
return diff?.map(p => {
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>;
});
}

View file

@ -48,6 +48,15 @@ function VencordSettings() {
const isWindows = navigator.platform.toLowerCase().startsWith("win");
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 | {
key: KeysOfType<typeof settings, boolean>;
@ -89,11 +98,6 @@ function VencordSettings() {
title: "Disable minimum window size",
note: "Requires a full restart"
},
IS_DISCORD_DESKTOP && isMac && {
key: "macosTranslucency",
title: "Enable translucent window",
note: "Requires a full restart"
}
];
return (
@ -152,6 +156,71 @@ function VencordSettings() {
</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} />}
</SettingsTab>
);

View file

@ -85,9 +85,15 @@ if (!IS_VANILLA) {
options.backgroundColor = "#00000000";
}
if (settings.macosTranslucency && process.platform === "darwin") {
const needsVibrancy = process.platform === "darwin" || (settings.macosVibrancyStyle || settings.macosTranslucency);
if (needsVibrancy) {
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;

View file

@ -46,6 +46,13 @@ export default definePlugin({
match: /(?<=\.activityEmoji,.+?animate:)\i/,
replace: "!0"
}
},
{
find: ".animatedBannerHoverLayer,onMouseEnter:",
replacement: {
match: /(?<=guildBanner:\i,animate:)\i/,
replace: "!0"
}
}
]
});

View file

@ -124,6 +124,18 @@ export const defaultRules = [
"t@*.x.com",
"s@*.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_content",
"lr@yandex.*",

View file

@ -60,7 +60,7 @@ async function embedDidMount(this: Component<Props>) {
if (hasTitle) {
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title;
embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1");
}
if (hasThumb) {

View file

@ -215,6 +215,9 @@ function initWs(isManual = false) {
case "ModuleId":
results = Object.keys(search(parsedArgs[0]));
break;
case "ComponentByCode":
results = findAll(filters.componentByCode(...parsedArgs));
break;
default:
return reply("Unknown Find Type " + type);
}

View file

@ -359,7 +359,7 @@ export default definePlugin({
},
// Separate patch for allowing using custom app icons
{
find: "location:\"AppIconHome\"",
find: ".FreemiumAppIconIds.DEFAULT&&(",
replacement: {
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
replace: "true"
@ -787,7 +787,14 @@ export default definePlugin({
if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId))
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) {
this.sendAnimatedSticker(link, sticker.id, channelId);
return { cancel: true };

View file

@ -14,10 +14,12 @@ export default definePlugin({
patches: [
{
find: "handleImageLoad=",
replacement: {
match: /(?<=getSrc\(\i\){.+?format:)\i/,
replace: "null"
}
replacement: [
{
match: /(?<=getSrc\(\i\){.+?return )\i\.SUPPORTS_WEBP.+?:(?=\i&&\(\i="png"\))/,
replace: ""
}
]
}
]
});

View file

@ -28,21 +28,22 @@ import style from "./style.css?managed";
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
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 (
<svg
width="20"
height="20"
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" />
{!showCurrentGame && <>
<mask id="gameActivityMask" >
<rect fill="white" x="0" y="0" width="24" height="24" />
<path fill="black" d="M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z" />
</mask>
<path fill="var(--status-danger)" d="M23 2.27 21.73 1 1 21.73 2.27 23 23 2.27Z" />
</>}
<svg width="20" height="20" viewBox="0 0 24 24">
{showCurrentGame ? (
<path fill="currentColor" d={controllerIcon} />
) : (
<>
<mask id="gameActivityMask" >
<rect fill="white" x="0" y="0" width="24" height="24" />
<path fill="black" d="M23.27 4.73 19.27 .73 -.27 20.27 3.73 24.27Z" />
</mask>
<path fill="var(--status-danger)" mask="url(#gameActivityMask)" d={controllerIcon} />
<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" />
</>
)}
</svg>
);
};

View file

@ -22,7 +22,7 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "iLoveSpam",
description: "Do not hide messages from 'likely spammers'",
authors: [Devs.botato, Devs.Animal],
authors: [Devs.botato, Devs.Nyako],
patches: [
{
find: "hasFlag:{writable",

View file

@ -72,6 +72,7 @@ export default definePlugin({
if (event.detail < 2) return;
if (settings.store.requireModifier && !event.ctrlKey && !event.shiftKey) return;
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;
if (msg.deleted === true) return;
if (isMe) {
if (!settings.store.enableDoubleClickToEdit || EditStore.isEditing(channel.id, msg.id)) return;
@ -81,6 +82,9 @@ export default definePlugin({
} else {
if (!settings.store.enableDoubleClickToReply) return;
const EPHEMERAL = 64;
if (msg.hasFlag(EPHEMERAL)) return;
FluxDispatcher.dispatch({
type: "CREATE_PENDING_REPLY",
channel,

View file

@ -19,7 +19,9 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByProps } from "@webpack";
import { findByPropsLazy } from "@webpack";
const { updateGuildNotificationSettings } = findByPropsLazy("updateGuildNotificationSettings");
const settings = definePluginSettings({
guild: {
@ -63,7 +65,7 @@ export default definePlugin({
handleMute(guildId: string | null) {
if (guildId === "@me" || guildId === "null" || guildId == null) return;
findByProps("updateGuildNotificationSettings").updateGuildNotificationSettings(guildId,
updateGuildNotificationSettings(guildId,
{
muted: settings.store.guild,
suppress_everyone: settings.store.everyone,

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

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

View file

@ -28,7 +28,8 @@ export default definePlugin({
start() {
fetch("https://raw.githubusercontent.com/adryd325/oneko.js/8fa8a1864aa71cd7a794d58bc139e755e96a236c/oneko.js")
.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);
},

View file

@ -55,13 +55,13 @@ const 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 tooltip = platform[0].toUpperCase() + platform.slice(1);
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];

View file

@ -31,7 +31,7 @@ export default definePlugin({
start() {
addButton("QuickMention", msg => {
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 {
label: "Quick Mention",

View file

@ -18,27 +18,28 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { React } from "@webpack/common";
let ERROR_CODES: any;
const CODES_URL =
"https://raw.githubusercontent.com/facebook/react/17.0.2/scripts/error-codes/codes.json";
export default definePlugin({
name: "ReactErrorDecoder",
description: 'Replaces "Minifed React Error" with the actual error.',
authors: [Devs.Cyn],
authors: [Devs.Cyn, Devs.maisymoe],
patches: [
{
find: '"https://reactjs.org/docs/error-decoder.html?invariant="',
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.")}/,
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() {
const CODES_URL = `https://raw.githubusercontent.com/facebook/react/v${React.version}/scripts/error-codes/codes.json`;
ERROR_CODES = await fetch(CODES_URL)
.then(res => res.json())
.catch(e => console.error("[ReactErrorDecoder] Failed to fetch React error codes\n", e));

View file

@ -42,6 +42,13 @@ export default definePlugin({
match: /codeBlock:\{react\((\i),(\i),(\i)\)\{/,
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 () => {

View file

@ -77,7 +77,7 @@ export default definePlugin({
},
// 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)||"
},
// Make channels we dont have access to be the same level as normal ones
@ -334,12 +334,12 @@ export default definePlugin({
replacement: [
{
// 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)&&`
},
{
// 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))&&`
},
]

View file

@ -55,13 +55,19 @@ export default definePlugin({
replace: "return [$self.renderPlayer(),$1]"
}
},
// Adds POST and a Marker to the SpotifyAPI (so we can easily find it)
{
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\)/,
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
{

View file

@ -46,10 +46,10 @@ export default definePlugin({
}
},
{
find: ".hasAvailableBurstCurrency)",
find: ".trackEmojiSearchEmpty,200",
replacement: {
match: /(?<=\.useBurstReactionsExperiment.{0,20})useState\(!1\)(?=.+?(\i===\i\.EmojiIntention.REACTION))/,
replace: "useState($self.settings.store.superReactByDefault && $1)"
match: /(\.trackEmojiSearchEmpty,200(?=.+?isBurstReaction:(\i).+?(\i===\i\.EmojiIntention.REACTION)).+?\[\2,\i\]=\i\.useState\().+?\)/,
replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.settings.store.superReactByDefault&&${isReactionIntention})`
}
}
],

View file

@ -21,7 +21,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
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";
@ -47,7 +47,7 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
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;
if (!settings.store.includeMutedChannels) {
@ -55,6 +55,10 @@ function TypingIndicator({ channelId }: { channelId: string; }) {
if (isChannelMuted) return null;
}
if (!settings.store.includeCurrentChannel) {
if (currentChannelId === channelId) return null;
}
const myId = UserStore.getCurrentUser()?.id;
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({
includeCurrentChannel: {
type: OptionType.BOOLEAN,
description: "Whether to show the typing indicator for the currently selected channel",
default: true
},
includeMutedChannels: {
type: OptionType.BOOLEAN,
description: "Whether to show the typing indicator for muted channels.",

View file

@ -20,9 +20,11 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { saveFile } from "@utils/web";
import { findByProps } from "@webpack";
import { findByPropsLazy } from "@webpack";
import { Clipboard, ComponentDispatch } from "@webpack/common";
const ctxMenuCallbacks = findByPropsLazy("contextMenuCallbackNative");
async function fetchImage(url: string) {
const res = await fetch(url);
if (res.status !== 200) return;
@ -55,7 +57,6 @@ export default definePlugin({
start() {
if (settings.store.addBack) {
const ctxMenuCallbacks = findByProps("contextMenuCallbackNative");
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
this.changedListeners = true;
@ -64,7 +65,6 @@ export default definePlugin({
stop() {
if (this.changedListeners) {
const ctxMenuCallbacks = findByProps("contextMenuCallbackNative");
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
}

View 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

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

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

View file

@ -78,8 +78,8 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Samu",
id: 702973430449832038n,
},
Animal: {
name: "Animal",
Nyako: {
name: "nyako",
id: 118437263754395652n
},
MaiKokain: {
@ -387,10 +387,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "ant0n",
id: 145224646868860928n
},
philipbry: {
name: "philipbry",
id: 554994003318276106n
},
Korbo: {
name: "Korbo",
id: 455856406420258827n
},
maisymoe: {
name: "maisy",
id: 257109471589957632n,
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly

View file

@ -16,7 +16,7 @@
* 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 { LazyComponent } from "./react";
@ -118,7 +118,7 @@ export type ImageModal = ComponentType<{
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 ModalHeader = LazyComponent(() => Modals.ModalHeader);