Merge branch 'dev'

This commit is contained in:
thororen1234 2025-09-12 13:40:47 -04:00
commit ddff504228
No known key found for this signature in database
32 changed files with 1245 additions and 442 deletions

View file

@ -5,9 +5,9 @@ body:
- type: markdown
attributes:
value: |
![Are you a developer? No? This form is not for you!](https://github.com/Vendicated/Vencord/blob/main/.github/ISSUE_TEMPLATE/developer-banner.png?raw=true)
![Are you a developer? No? This form is not for you!](https://github.com/Equicord/Equicord/blob/main/.github/ISSUE_TEMPLATE/developer-banner.png?raw=true)
GitHub Issues are for development, not support! Please use our [support server](https://vencord.dev/discord) unless you are a Vencord Developer.
GitHub Issues are for development, not support! Please use our [support server](https://equicord.org/discord) unless you are a Vencord Developer.
- type: textarea
id: content

View file

@ -6,11 +6,11 @@
Equicord is a fork of [Vencord](https://github.com/Vendicated/Vencord), with over 300+ plugins.
You can join our [discord server](https://equicord.org/discord) for commits, changes, chat or even support.
You can join our [Discord server](https://equicord.org/discord) for commits, changes, chatting, or even support.
### Included Plugins
Our included plugins can be found [here](https://equicord.org/plugins)
Our included plugins can be found [here](https://equicord.org/plugins).
## Installing / Uninstalling
@ -90,7 +90,7 @@ Thank you to [Vendicated](https://github.com/Vendicated) for creating [Vencord](
## Disclaimer
Discord is trademark of Discord Inc. and solely mentioned for the sake of descriptivity.
Discord is trademark of Discord Inc., and solely mentioned for the sake of descriptivity.
Mentioning it does not imply any affiliation with or endorsement by Discord Inc.
Vencord is not connected to Equicord and as such, all donation links go to Vendicated's donation link.
@ -101,8 +101,8 @@ Client modifications are against Discord’s Terms of Service.
However, Discord is pretty indifferent about them and there are no known cases of users getting banned for using client mods! So you should generally be fine if you don’t use plugins that implement abusive behaviour. But no worries, all inbuilt plugins are safe to use!
Regardless, if your account is essential to you and getting disabled would be a disaster for you, you should probably not use any client mods (not exclusive to Equicord), just to be safe
Regardless, if your account is essential to you and getting disabled would be a disaster for you, you should probably not use any client mods (not exclusive to Equicord), just to be safe.
Additionally, make sure not to post screenshots with Equicord in a server where you might get banned for it
Additionally, make sure not to post screenshots with Equicord in a server where you might get banned for it.
</details>

View file

@ -41,7 +41,7 @@
"@ffmpeg/ffmpeg": "^0.12.10",
"@ffmpeg/util": "^0.12.1",
"@intrnl/xxhash64": "^0.1.2",
"@types/less": "^3.0.6",
"@types/less": "^3.0.8",
"@types/stylus": "^0.48.42",
"@types/tinycolor2": "^1.4.6",
"@vap/core": "0.0.12",
@ -56,8 +56,9 @@
"virtual-merge": "^1.0.1"
},
"devDependencies": {
"@electron/asar": "^3.2.10",
"@electron/asar": "^4.0.1",
"@stylistic/eslint-plugin": "^4.2.0",
"@types/adm-zip": "^0.5.7",
"@types/chrome": "^0.0.312",
"@types/diff": "^7.0.2",
"@types/lodash": "^4.17.14",
@ -66,6 +67,7 @@
"@types/react-dom": "^19.0.4",
"@types/yazl": "^2.4.5",
"@vencord/discord-types": "link:packages/discord-types",
"adm-zip": "^0.5.16",
"diff": "^7.0.0",
"esbuild": "^0.25.1",
"eslint": "9.20.1",
@ -77,7 +79,7 @@
"highlight.js": "11.11.1",
"html-minifier-terser": "^7.2.0",
"moment": "^2.22.2",
"puppeteer-core": "^24.4.0",
"puppeteer-core": "^24.19.0",
"standalone-electron-types": "^34.2.0",
"stylelint": "^16.17.0",
"stylelint-config-standard": "^37.0.0",
@ -87,14 +89,13 @@
"type-fest": "^4.38.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.28.0",
"typescript-transform-paths": "^3.5.5",
"zip-local": "^0.3.5"
"typescript-transform-paths": "^3.5.5"
},
"packageManager": "pnpm@10.14.0",
"pnpm": {
"patchedDependencies": {
"eslint@9.20.1": "patches/eslint@9.20.1.patch",
"@types/less@3.0.6": "patches/@types__less@3.0.6.patch"
"@types/less@3.0.8": "patches/@types__less@3.0.8.patch"
},
"peerDependencyRules": {
"ignoreMissing": [
@ -111,7 +112,10 @@
},
"onlyBuiltDependencies": [
"esbuild"
]
],
"overrides": {
"@eslint/plugin-kit@<0.3.4": ">=0.3.4"
}
},
"engines": {
"node": ">=18"

577
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -21,10 +21,10 @@
import { readFileSync } from "fs";
import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
import { join } from "path";
import Zip from "zip-local";
import path, { join } from "path";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, IS_COMPANION_TEST, VERSION, commonRendererPlugins, buildOrWatchAll, stringifyValues } from "./common.mjs";
import AdmZip from "adm-zip";
/**
* @type {import("esbuild").BuildOptions}
@ -203,14 +203,15 @@ if (!process.argv.includes("--skip-extension")) {
buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
]);
Zip.zip("dist/browser/chromium-unpacked", (_err, zip) => {
zip.compress().save("dist/extension-chrome.zip");
console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
});
Zip.zip("dist/browser/firefox-unpacked", (_err, zip) => {
zip.compress().save("dist/extension-firefox.zip");
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
});
const chromiumZip = new AdmZip();
chromiumZip.addLocalFolder("dist/browser/chromium-unpacked");
chromiumZip.writeZip(path.resolve("dist/extension-chrome.zip"));
console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
const firefoxZip = new AdmZip();
firefoxZip.addLocalFolder("dist/browser/firefox-unpacked");
firefoxZip.writeZip(path.resolve("dist/extension-firefox.zip"));
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
} else {
await appendCssRuntime;
}

View file

@ -155,7 +155,7 @@ async function init() {
syncSettings();
if (!IS_WEB && !IS_UPDATER_DISABLED) {
if (!IS_WEB && !IS_UPDATER_DISABLED || !IS_DEV) {
runUpdateCheck();
// this tends to get really annoying, so only do this if the user has auto-update without notification enabled

View file

@ -22,10 +22,10 @@ import { CheckedTextInput } from "@components/CheckedTextInput";
import { Grid } from "@components/Grid";
import { Link } from "@components/Link";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { authorizeCloud, checkCloudUrlCsp, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { Margins } from "@utils/margins";
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
import { Alerts, Button, Forms, Switch, Tooltip, useState } from "@webpack/common";
function validateUrl(url: string) {
try {
@ -37,8 +37,6 @@ function validateUrl(url: string) {
}
async function eraseAllData() {
if (!await checkCloudUrlCsp()) return;
const res = await fetch(new URL("/v1/", getCloudUrl()), {
method: "DELETE",
headers: { Authorization: await getCloudAuth() }
@ -119,15 +117,31 @@ function SettingsSyncSection() {
function CloudTab() {
const settings = useSettings(["cloud.authenticated", "cloud.url"]);
const [inputKey, setInputKey] = useState(0);
async function changeUrl(url: string) {
settings.cloud.url = url;
settings.cloud.authenticated = false;
await deauthorizeCloud();
await authorizeCloud();
setInputKey(prev => prev + 1);
}
return (
<SettingsTab title="Equicord Cloud">
<Forms.FormSection title="Cloud Settings" className={Margins.top16}>
<Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
Equicord comes with a cloud integration that adds goodies like settings sync across devices.
It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
can host it yourself.
Equicord comes with a cloud integration allowing settings to be synced across apps and devices.
<br />
We use Vencord's <Link href="https://github.com/Vencord/Backend">cloud source code</Link> to provide our cloud instance which has a 32MB backup cap.
<br />
Our <Link href="https://vencord.dev/cloud/privacy">privacy policy</Link> allows you to see what information we store, how we use it, and data retention.
<br />
The Vencord Cloud repository is AGPL 3.0 licensed so you can host it yourself it you would like.
<br />
You can swap between Equicord and Vencord's different cloud instances below if needed.
</Forms.FormText>
<Switch
key="backend"
@ -147,12 +161,12 @@ function CloudTab() {
Which backend to use when using cloud integrations.
</Forms.FormText>
<CheckedTextInput
key="backendUrl"
key={`backendUrl-${inputKey}`}
value={settings.cloud.url}
onChange={async v => {
settings.cloud.url = v;
settings.cloud.authenticated = false;
deauthorizeCloud();
await deauthorizeCloud();
}}
validate={validateUrl}
/>
@ -162,8 +176,8 @@ function CloudTab() {
size={Button.Sizes.MEDIUM}
disabled={!settings.cloud.authenticated}
onClick={async () => {
await deauthorizeCloud();
settings.cloud.authenticated = false;
await deauthorizeCloud();
await authorizeCloud();
}}
>
@ -184,6 +198,12 @@ function CloudTab() {
>
Erase All Data
</Button>
<Button size={Button.Sizes.MEDIUM} onClick={() => changeUrl("https://cloud.equicord.org/")}>
Use Equicord's Cloud
</Button>
<Button size={Button.Sizes.MEDIUM} onClick={() => changeUrl("https://api.vencord.dev/")}>
Use Vencord's Cloud
</Button>
</Grid>
<Forms.FormDivider className={Margins.top16} />

View file

@ -0,0 +1,80 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu, ChannelStore, NavigationRouter, FluxDispatcher, MessageActions, MessageStore } from "@webpack/common";
interface ChannelSelectEvent {
type: "CHANNEL_SELECT";
channelId: string | null;
guildId: string | null;
}
let lastChan = "0";
function jumpToPresent(channelId, { limit }: { limit?: number } = {}) {
MessageActions.trackJump(channelId, null, "Present");
const jump = { present: true };
if (MessageStore.hasPresent(channelId)) {
FluxDispatcher.dispatch({
type: "LOAD_MESSAGES_SUCCESS_CACHED",
jump,
channelId,
limit
});
} else {
MessageActions.fetchMessages({
channelId,
limit,
jump
});
}
}
function autoJump(channel: any) {
const guildId = channel.guild_id ?? "@me";
const channelId = channel.id;
if (channelId === lastChan) return;
NavigationRouter.transitionTo(`/channels/${guildId}/${channelId}`);
lastChan = channelId;
jumpToPresent(channelId, { limit: 100 });
}
const MenuPatch: NavContextMenuPatchCallback = (children, { channel }) => {
children.push(
<Menu.MenuItem
id="auto-jump"
label="Jump to Last Message"
action={() => autoJump(channel)}
/>
);
};
export default definePlugin({
name: "AutoJump",
description: "Automatically jumps to the last message in selected channel & adds a context-menu option to jump to the last message.",
authors: [EquicordDevs.omaw],
contextMenus: {
"channel-context": MenuPatch,
"user-context": MenuPatch,
"thread-context": MenuPatch
},
flux: {
async CHANNEL_SELECT({ guildId, channelId }: ChannelSelectEvent) {
if (!guildId || !channelId) return;
const channel = ChannelStore.getChannel(channelId);
if (!channel || lastChan === channel.id) return;
autoJump(channel);
}
}
});

View file

@ -4,9 +4,11 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher, UserStore } from "@webpack/common";
const settings = definePluginSettings({
autoFillArguments: {
@ -16,6 +18,13 @@ const settings = definePluginSettings({
}
});
function fetchIndex(target: object) {
FluxDispatcher.dispatch({
type: "APPLICATION_COMMAND_INDEX_FETCH_REQUEST",
target
});
}
export default definePlugin({
name: "BetterCommands",
description: "Enhances the command system with miscellaneous improvements.",
@ -32,5 +41,52 @@ export default definePlugin({
},
]
}
],
commands: [
{
name: "refresh",
description: "Refreshes Discord application commands locally",
options: [
{
name: "user",
description: "specific user to try and refresh",
type: ApplicationCommandOptionType.USER,
}
],
inputType: ApplicationCommandInputType.BOT,
execute: async (opts, ctx) => {
try {
const channelId = ctx.channel.id;
const guildId = ctx.guild?.id;
const user = findOption(opts, "user") as string;
sendBotMessage(ctx.channel.id, {
content: "Refreshing application commands...",
});
fetchIndex({ type: "channel", channelId });
if (guildId) fetchIndex({ type: "guild", guildId });
if (user) {
const target = await UserStore.getUser(user);
fetchIndex(
target.bot
? { type: "application", applicationId: target.id }
: { type: "user" }
);
}
sendBotMessage(ctx.channel.id, {
content: "Commands refreshed successfully!",
});
} catch (err) {
console.error("[Refresh Command] Error refreshing commands:", err);
sendBotMessage(ctx.channel.id, {
content: "Failed to refresh commands. Check the console for details.",
});
}
},
},
]
});

View file

@ -14,8 +14,8 @@ import { colors, DATASTORE_KEY } from "./index";
const cl = classNameFactory("vc-customColors-");
export function SetColorModal({ userId, modalProps }: { userId: string, modalProps: ModalProps; }) {
const initialColor = parseInt(colors[userId], 16) || 372735;
export function SetColorModal({ id, modalProps }: { id: string, modalProps: ModalProps; }) {
const initialColor = parseInt(colors[id], 16) || 372735;
// color picker default to current color set for user (if null it's 0x05afff :3 )
const [colorPickerColor, setColorPickerColor] = useState(initialColor);
@ -32,13 +32,13 @@ export function SetColorModal({ userId, modalProps }: { userId: string, modalPro
}
async function saveUserColor() {
colors[userId] = colorPickerColor.toString(16).padStart(6, "0");
colors[id] = colorPickerColor.toString(16).padStart(6, "0");
await set(DATASTORE_KEY, colors);
modalProps.onClose();
}
async function deleteUserColor() {
delete colors[userId];
delete colors[id];
await set(DATASTORE_KEY, colors);
modalProps.onClose();
}

View file

@ -12,9 +12,9 @@ import { definePluginSettings, Settings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import { openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { User } from "@vencord/discord-types";
import { Channel, User } from "@vencord/discord-types";
import { extractAndLoadChunksLazy } from "@webpack";
import { Menu } from "@webpack/common";
import { ChannelStore, Menu, SelectedChannelStore } from "@webpack/common";
import { SetColorModal } from "./SetColorModal";
@ -52,20 +52,37 @@ const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: {
icon={ColorIcon}
action={async () => {
await requireSettingsMenu();
openModal(modalProps => <SetColorModal userId={user.id} modalProps={modalProps} />);
openModal(modalProps => <SetColorModal id={user.id} modalProps={modalProps} />);
}}
/>
);
children.push(<Menu.MenuSeparator />, setCustomColorItem);
};
export function getCustomColorString(userId: string | undefined, withHash?: boolean): string | undefined {
if (!userId) return;
if (!colors[userId] || !Settings.plugins.CustomUserColors.enabled) return;
if (withHash) return `#${colors[userId]}`;
return colors[userId];
const channelContextMenuPatch: NavContextMenuPatchCallback = (children, { channel }: { channel: Channel; }) => {
if (channel?.id == null) return;
const setCustomColorItem = (
<Menu.MenuItem
label="Set Color"
id="set-color"
icon={ColorIcon}
action={async () => {
await requireSettingsMenu();
openModal(modalProps => <SetColorModal id={channel.id} modalProps={modalProps} />);
}}
/>
);
children.push(<Menu.MenuSeparator />, setCustomColorItem);
};
export function getCustomColorString(id: string | undefined, withHash?: boolean): string | undefined {
if (!id) return;
if (!colors[id] || !Settings.plugins.CustomUserColors.enabled) return;
if (withHash) return `#${colors[id]}`;
return colors[id];
}
const settings = definePluginSettings({
@ -85,7 +102,10 @@ export default definePlugin({
name: "CustomUserColors",
description: "Lets you add a custom color to any user, anywhere! Highly recommend to use with typingTweaks and roleColorEverywhere",
authors: [EquicordDevs.mochienya],
contextMenus: { "user-context": userContextMenuPatch },
contextMenus: {
"user-context": userContextMenuPatch,
"gdm-context": channelContextMenuPatch,
},
settings,
requireSettingsMenu,
getCustomColorString,
@ -135,10 +155,12 @@ export default definePlugin({
wrapMessageColorProps(colorProps: { colorString: string, colorStrings?: Record<"primaryColor" | "secondaryColor" | "tertiaryColor", string>; }, context: any) {
try {
const channelId = SelectedChannelStore.getChannelId();
const channel = ChannelStore.getChannel(channelId);
const isDM = channel.isDM() || channel.isMultiUserDM();
const colorString = this.colorIfServer(context);
if (colorString === colorProps.colorString) {
return colorProps;
}
if (colorString === colorProps.colorString) return colorProps;
if (!settings.store.colorInServers && !isDM) return colorProps;
return {
...colorProps,
@ -156,8 +178,9 @@ export default definePlugin({
},
colorDMList(context: any): string | undefined {
const userId = context?.user?.id;
const colorString = getCustomColorString(userId, true);
const id = context?.user?.id ?? context?.channel?.id;
const colorString = getCustomColorString(id, true);
return colorString ?? "inherit";
},

View file

@ -7,15 +7,14 @@
import { definePluginSettings } from "@api/Settings";
import { Logger } from "@utils/Logger";
import { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { Forms, SearchableSelect, useEffect, useState } from "@webpack/common";
import { findByCodeLazy } from "@webpack";
import { Forms, MediaEngineStore, SearchableSelect, useEffect, useState } from "@webpack/common";
interface PickerProps {
streamMediaSelection: any[];
streamMedia: any[];
}
const mediaEngine = findByPropsLazy("getMediaEngine");
const getDesktopSources = findByCodeLazy("desktop sources");
export const settings = definePluginSettings({
@ -26,7 +25,7 @@ export const settings = definePluginSettings({
});
export async function getCurrentMedia() {
const media = mediaEngine.getMediaEngine();
const media = MediaEngineStore.getMediaEngine();
const sources = [
...(await getDesktopSources(media, ["screen"], null) ?? []),
...(await getDesktopSources(media, ["window", "application"], null) ?? [])
@ -63,7 +62,7 @@ function StreamSimplePicker({ streamMediaSelection, streamMedia }: PickerProps)
function ScreenSetting() {
const { streamMedia } = settings.use(["streamMedia"]);
const media = mediaEngine.getMediaEngine();
const media = MediaEngineStore.getMediaEngine();
const [streamMediaSelection, setStreamMediaSelection] = useState<any[]>([]);
const [loading, setLoading] = useState(true);

View file

@ -0,0 +1,20 @@
.vc-newPluginsManager-restart-tooltip {
max-width: 280px;
}
.vc-newPluginsManager-restart-list {
padding-left: 1.25em;
}
.vc-newPluginsManager-restart-list li {
list-style: disc;
}
.vc-newPluginsManager-close > div {
display: flex;
}
.vc-newPluginsManager-disable-wrapper {
display: flex;
margin: auto 0;
}

View file

@ -4,47 +4,36 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { useSettings } from "@api/Settings";
import "./NewPluginsModal.css";
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { PluginCard } from "@components/settings/tabs/plugins/PluginCard";
import ErrorBoundary from "@components/ErrorBoundary";
import { PluginCard } from "@components/settings/tabs/plugins";
import { ChangeList } from "@utils/ChangeList";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Alerts, Button, Flex, Forms, Parser, React, Text, Tooltip, useMemo } from "@webpack/common";
import { classes, Margins } from "@utils/index";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useForceUpdater } from "@utils/react";
import { findComponentByCodeLazy } from "@webpack";
import { Button, Flex, Forms, React, Text, Tooltip, useMemo } from "@webpack/common";
import { JSX } from "react";
import Plugins from "~plugins";
import { getNewPlugins, writeKnownPlugins } from "./knownPlugins";
import { getNewPlugins, getNewSettings, KnownPluginSettingsMap, writeKnownSettings } from "./knownSettings";
const cl = classNameFactory("vc-plugins-");
const Checkbox = findComponentByCodeLazy(".checkboxWrapperDisabled:");
let hasSeen = false;
// Most of this was stolen from PluginSettings directly.
export function NewPluginsModal({ modalProps, newPlugins }: { modalProps: ModalProps; newPlugins: Set<string>; }) {
export function NewPluginsModal({ modalProps, newPlugins, newSettings }: { modalProps: ModalProps; newPlugins: Set<string>; newSettings: KnownPluginSettingsMap; }) {
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []);
React.useEffect(() => {
return () => void (changes.hasChanges && Alerts.show({
title: "Restart required",
body: (
<>
<p>The following plugins require a restart:</p>
<div>{changes.map((s, i) => (
<>
{i > 0 && ", "}
{Parser.parse("`" + s + "`")}
</>
))}</div>
</>
),
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: () => location.reload()
}));
}, []);
let updateContinueButton = () => { };
const depMap = React.useMemo(() => {
const o = {} as Record<string, string[]>;
@ -60,8 +49,13 @@ export function NewPluginsModal({ modalProps, newPlugins }: { modalProps: ModalP
return o;
}, []);
const sortedPlugins = useMemo(() => [...newPlugins].map(pn => Plugins[pn])
.sort((a, b) => a.name.localeCompare(b.name)), []);
const mapPlugins = (array: string[]) => array.map(pn => Plugins[pn])
.sort((a, b) => a.name.localeCompare(b.name));
const sortedPlugins = useMemo(() => [
...mapPlugins([...newPlugins]),
...mapPlugins([...newSettings.keys()].filter(p => !newPlugins.has(p)))
], []);
const plugins = [] as JSX.Element[];
const requiredPlugins = [] as JSX.Element[];
@ -83,10 +77,14 @@ export function NewPluginsModal({ modalProps, newPlugins }: { modalProps: ModalP
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={name => changes.handleChange(name)}
onRestartNeeded={name => {
changes.handleChange(name);
updateContinueButton();
}}
disabled={true}
plugin={p}
key={p.name}
isNew={newPlugins.has(p.name)}
/>
)}
</Tooltip>
@ -94,10 +92,14 @@ export function NewPluginsModal({ modalProps, newPlugins }: { modalProps: ModalP
} else {
plugins.push(
<PluginCard
onRestartNeeded={name => changes.handleChange(name)}
onRestartNeeded={name => {
changes.handleChange(name);
updateContinueButton();
}}
disabled={false}
plugin={p}
key={p.name}
isNew={newPlugins.has(p.name)}
/>
);
}
@ -106,7 +108,17 @@ export function NewPluginsModal({ modalProps, newPlugins }: { modalProps: ModalP
return <ModalRoot {...modalProps} size={ModalSize.MEDIUM} >
<ModalHeader>
<Text variant="heading-lg/semibold">New Plugins ({[...plugins, ...requiredPlugins].length})</Text>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>New Plugins and Settings ({[...plugins, ...requiredPlugins].length})</Text>
<Tooltip text="Dismiss for this session">
{tooltipProps =>
<div {...tooltipProps}>
<ModalCloseButton
onClick={modalProps.onClose}
className={classes(cl("close"), "vc-newPluginsManager-close")}
/>
</div>
}
</Tooltip>
</ModalHeader>
<ModalContent>
<div className={cl("grid")}>
@ -115,39 +127,79 @@ export function NewPluginsModal({ modalProps, newPlugins }: { modalProps: ModalP
</ModalContent>
<ModalFooter>
<Flex direction={Flex.Direction.HORIZONTAL_REVERSE}>
<Button
color={Button.Colors.GREEN}
onClick={async () => {
await writeKnownPlugins();
modalProps.onClose();
}}
>
Continue
</Button>
<ContinueButton
close={modalProps.onClose}
changes={changes}
callback={(v: () => void) => updateContinueButton = v}
/>
</Flex>
<Flex direction={Flex.Direction.HORIZONTAL}>
<div className="vc-newPluginsManager-disable-wrapper">
<Checkbox
type="inverted"
value={!settings?.plugins?.NewPluginsManager?.enabled}
onChange={() => {
Settings.plugins.NewPluginsManager.enabled = !settings?.plugins?.NewPluginsManager?.enabled;
}}
>
<Text>Don't show this again</Text>
</Checkbox>
</div>
</Flex>
</ModalFooter>
</ModalRoot>;
}
function ContinueButton(props: { callback: (update: () => void) => void; changes: ChangeList<string>; close: () => any; }) {
const update = useForceUpdater();
props.callback(update);
return <Tooltip
tooltipClassName="vc-newPluginsManager-restart-tooltip"
text={<>
The following plugins require a restart:
<div className={Margins.bottom8} />
<ul className="vc-newPluginsManager-restart-list">
{props.changes.map(p => <li key={p}>{p}</li>)}
</ul>
</>}
shouldShow={props.changes.hasChanges}
>
{tooltipProps =>
<Button
{...tooltipProps}
color={Button.Colors.GREEN}
onClick={async () => {
await writeKnownSettings();
props.changes.hasChanges ? location.reload() : props.close();
}}
>
{props.changes.hasChanges ? "Restart" : "Continue"}
</Button>
}
</Tooltip>;
}
function makeDependencyList(deps: string[]) {
return (
<React.Fragment>
<Forms.FormText>This plugin is required by:</Forms.FormText>
{deps.map((dep: string) => <Forms.FormText key={cl("dep-text")} className={cl("dep-text")}>{dep}</Forms.FormText>)}
{deps.map((dep: string) => <Forms.FormText key={dep} className={cl("dep-text")}>{dep}</Forms.FormText>)}
</React.Fragment>
);
}
export async function openNewPluginsModal() {
const newPlugins = await getNewPlugins();
if (newPlugins.size && !hasSeen) {
const newSettings = await getNewSettings();
if ((newPlugins.size || newSettings.size) && !hasSeen) {
hasSeen = true;
openModal(modalProps => (
const modalKey = openModal(modalProps => <ErrorBoundary noop onError={() => { closeModal(modalKey); }}>
<NewPluginsModal
modalProps={modalProps}
newPlugins={newPlugins}
newSettings={newSettings}
/>
));
</ErrorBoundary>);
}
}

View file

@ -7,18 +7,22 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { KNOWN_PLUGINS_DATA_KEY } from "./knownPlugins";
import * as KnownSettings from "./knownSettings";
import { KNOWN_PLUGINS_LEGACY_DATA_KEY, KNOWN_SETTINGS_DATA_KEY } from "./knownSettings";
import { openNewPluginsModal } from "./NewPluginsModal";
export default definePlugin({
name: "NewPluginsManager",
description: "Utility that notifies you when new plugins are added to Equicord",
description: "Utility that notifies you when new plugins are added to Vencord",
authors: [Devs.Sqaaakoi],
enabledByDefault: true,
flux: {
async POST_CONNECTION_OPEN() {
openNewPluginsModal();
}
},
openNewPluginsModal,
KNOWN_PLUGINS_DATA_KEY
KNOWN_PLUGINS_LEGACY_DATA_KEY,
KNOWN_SETTINGS_DATA_KEY,
KnownSettings
});

View file

@ -1,32 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import plugins from "~plugins";
export const KNOWN_PLUGINS_DATA_KEY = "NewPluginsManager_KnownPlugins";
export async function getKnownPlugins(): Promise<Set<string>> {
let knownPlugins = await DataStore.get(KNOWN_PLUGINS_DATA_KEY) as string[];
if (knownPlugins === undefined) {
knownPlugins = Object.keys(plugins);
DataStore.set(KNOWN_PLUGINS_DATA_KEY, knownPlugins);
}
return new Set(knownPlugins);
}
export async function getNewPlugins(): Promise<Set<string>> {
const currentPlugins = Object.keys(plugins);
const knownPlugins = await getKnownPlugins();
return new Set(currentPlugins.filter(p => !knownPlugins.has(p)));
}
export async function writeKnownPlugins(): Promise<void> {
const currentPlugins = Object.keys(plugins);
const knownPlugins = await getKnownPlugins();
DataStore.set(KNOWN_PLUGINS_DATA_KEY, [...new Set([...currentPlugins, ...knownPlugins])]);
}

View file

@ -0,0 +1,91 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import plugins from "~plugins";
export type KnownPluginSettingsMap = Map<string, Set<string>>;
export const KNOWN_PLUGINS_LEGACY_DATA_KEY = "NewPluginsManager_KnownPlugins";
export const KNOWN_SETTINGS_DATA_KEY = "NewPluginsManager_KnownSettings";
function getSettingsSetForPlugin(plugin: string): Set<string> {
const settings = plugins[plugin]?.settings?.def || {};
return new Set(Object.keys(settings).filter(setting => setting !== "enabled"));
}
function getCurrentSettings(pluginList: string[]): KnownPluginSettingsMap {
return new Map(pluginList.map(name => [
name,
getSettingsSetForPlugin(name)
]));
}
export async function getKnownSettings(): Promise<KnownPluginSettingsMap> {
let map = await DataStore.get(KNOWN_SETTINGS_DATA_KEY) as KnownPluginSettingsMap;
if (map === undefined) {
const knownPlugins = await DataStore.get(KNOWN_PLUGINS_LEGACY_DATA_KEY) ?? [] as string[];
const Plugins = [...Object.keys(plugins), ...knownPlugins];
map = getCurrentSettings(Plugins);
DataStore.set(KNOWN_SETTINGS_DATA_KEY, map);
}
return map;
}
export async function getNewSettings(): Promise<KnownPluginSettingsMap> {
const map = getCurrentSettings(Object.keys(plugins));
const knownSettings = await getKnownSettings();
map.forEach((settings, plugin) => {
const filteredSettings = [...settings].filter(setting => !knownSettings.get(plugin)?.has(setting));
if (!filteredSettings.length) return map.delete(plugin);
map.set(plugin, new Set(filteredSettings));
});
return map;
}
export async function getKnownPlugins(): Promise<Set<string>> {
const knownSettings = await getKnownSettings();
return new Set(knownSettings.keys());
}
export async function getNewPlugins(): Promise<Set<string>> {
const currentPlugins = Object.keys(plugins);
const knownPlugins = await getKnownPlugins();
return new Set(currentPlugins.filter(p => !knownPlugins.has(p)));
}
export async function writeKnownSettings() {
const currentSettings = getCurrentSettings(Object.keys(plugins));
const knownSettings = await getKnownSettings();
const allSettings = new Map();
new Set([...currentSettings.keys(), ...knownSettings.keys()]).forEach(plugin => {
allSettings.set(plugin, new Set([
...(currentSettings.get(plugin) || []),
...(knownSettings.get(plugin) || [])
]));
});
await DataStore.set(KNOWN_SETTINGS_DATA_KEY, allSettings);
}
export async function debugWipeSomeData() {
const settings = await getKnownSettings();
settings.forEach((value, key) => {
if (Math.random() > 0.8) {
if (Math.random() > 0.5) return settings.set(key, new Set([...value].filter(() => Math.random() > 0.5)));
return settings.delete(key);
}
});
await DataStore.set(KNOWN_SETTINGS_DATA_KEY, settings);
}
export async function editRawData(patcher: (data: KnownPluginSettingsMap) => (Promise<any> | any)) {
if (!patcher) return;
const map = await DataStore.get(KNOWN_SETTINGS_DATA_KEY) as KnownPluginSettingsMap;
const newMap = new Map(map);
await patcher(newMap);
await DataStore.set(KNOWN_SETTINGS_DATA_KEY, newMap ?? map);
}

View file

@ -16,7 +16,7 @@ export const GuildlessServerListItemComponent = findComponentByCodeLazy("tooltip
export const GuildedServerListItemPillComponent = findComponentByCodeLazy('"pill":"empty"');
export const ServerListItemLowerBadgeComponent = findComponentByCodeLazy("STATUS_DANGER,disableColor:");
export const ServerListItemUpperBadgeComponent = findComponentByCodeLazy("STATUS_DANGER,shape:", "iconBadge,");
export const RadioGroup = findComponentByCodeLazy("radioItemIconClassName,");
export const RadioGroup = findComponentByCodeLazy("\"label\",\"description\"]);");
export const QuestTile = findComponentByCodeLazy(".rowIndex,trackGuildAndChannelMetadata");
export enum QuestStatus {

View file

@ -10,7 +10,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { debounce } from "@shared/debounce";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { makeRange, OptionType } from "@utils/types";
import { findByCode, findByProps, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { ChannelRouter, ChannelStore, ContextMenuApi, GuildStore, MediaEngineStore, Menu, PermissionsBits, PermissionStore, React, SelectedChannelStore, Toasts, UserStore, VoiceActions, VoiceStateStore } from "@webpack/common";
import style from "./styles.css?managed";
@ -18,6 +18,8 @@ import style from "./styles.css?managed";
const ChatVoiceIcon = findComponentByCodeLazy("0l1.8-1.8c.17");
const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON");
const ChannelActions = findByPropsLazy("selectChannel", "selectVoiceChannel");
const startStream = findByCodeLazy('type:"STREAM_START"');
const getDesktopSources = findByCodeLazy("desktop sources");
const valueOperation = [
{ label: "More than", value: "<", default: false },
@ -770,9 +772,7 @@ function JoinVc(channelID) {
}
async function autoStream() {
const startStream = findByCode('type:"STREAM_START"');
const mediaEngine = findByProps("getMediaEngine").getMediaEngine();
const getDesktopSources = findByCode("desktop sources");
const mediaEngine = MediaEngineStore.getMediaEngine();
const selected = SelectedChannelStore.getVoiceChannelId();
if (!selected) return;
const channel = ChannelStore.getChannel(selected);

View file

@ -0,0 +1,34 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Tooltip } from "@webpack/common";
import type { ReactElement } from "react";
export interface ToneIndicatorProps {
indicator: string;
desc: string;
}
export default function ToneIndicator({
indicator,
desc,
}: ToneIndicatorProps): ReactElement {
return (
<Tooltip text={desc}>
{tooltipProps => (
<span
{...tooltipProps}
style={{
color: "var(--text-normal)",
userSelect: "text",
}}
>
/{indicator}
</span>
)}
</Tooltip>
);
}

View file

@ -0,0 +1,142 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { React } from "@webpack/common";
import { type ReactNode } from "react";
import indicatorsDefault from "./indicators";
import ToneIndicator from "./ToneIndicator";
const settings = definePluginSettings({
customIndicators: {
type: OptionType.STRING,
description: "Custom tone indicators (format: jk=Joking; srs=Serious)",
default: "",
},
maxIndicators: {
type: OptionType.NUMBER,
description: "Maximum number of tone indicators to show per message",
default: 5,
},
});
function getCustomIndicators(): Record<string, string> {
const raw = settings.store.customIndicators || "";
const result: Record<string, string> = {};
raw.split("; ").forEach(entry => {
const [key, ...rest] = entry.split("=");
if (key && rest.length > 0) {
result[key.trim().toLowerCase()] = rest.join("=").trim();
}
});
return result;
}
function getIndicator(text: string): string | null {
text = text.toLowerCase();
const customIndicators = getCustomIndicators();
return (
customIndicators[text] ||
customIndicators[`_${text}`] ||
indicatorsDefault.get(text) ||
indicatorsDefault.get(`_${text}`) ||
null
);
}
function splitTextWithIndicators(text: string): ReactNode[] {
const nodes: ReactNode[] = [];
let lastIndex = 0;
let count = 0;
const regex = /\/([a-z]{1,20})(?=\s|$|[^\w])/gi;
let match: RegExpExecArray | null;
while ((match = regex.exec(text)) && count < settings.store.maxIndicators) {
const indicator = match[1];
const desc = getIndicator(indicator);
if (!desc) continue;
if (match.index > lastIndex) {
nodes.push(text.slice(lastIndex, match.index));
}
nodes.push(
<ToneIndicator
key={`ti-${match.index}`}
indicator={indicator}
desc={desc}
/>,
);
lastIndex = regex.lastIndex;
count++;
}
if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
return nodes;
}
function patchChildrenTree(children: any): any {
const transform = (node: any): any => {
if (node == null) return node;
if (typeof node === "string") {
if (!/\/[a-z]+/i.test(node)) return node;
const parts = splitTextWithIndicators(node);
return parts.length === 1 ? parts[0] : parts;
}
if (node?.props?.children != null) {
const c = node.props.children;
if (Array.isArray(c)) {
node.props.children = c.map(transform).flat();
} else {
node.props.children = transform(c);
}
return node;
}
return node;
};
if (Array.isArray(children)) return children.map(transform).flat();
return transform(children);
}
export default definePlugin({
name: "ToneIndicators",
description: "Show tooltips for tone indicators like /srs, /gen, etc. in sent messages.",
authors: [EquicordDevs.justjxke],
settings,
patches: [
{
find: '["strong","em","u","text","inlineCode","s","spoiler"]',
replacement: [
{
match: /(?=return\{hasSpoilerEmbeds:\i,content:(\i)\})/,
replace: (_: any, content: string) =>
`${content}=$self.patchToneIndicators(${content});`,
},
],
},
],
patchToneIndicators(content: any): any {
try {
return patchChildrenTree(content);
} catch {
return content;
}
},
});

View file

@ -0,0 +1,62 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
const defaultIndicators = new Map(
Object.entries({
j: "Joking",
hj: "Half Joking",
s: "Sarcastic",
_sarc: "Sarcastic", // Alias for s
gen: "Genuine",
_g: "Genuine", // Alias for gen,
genq: "Genuine Question",
_gq: "Genuine Question", // Alias for genq
srs: "Serious",
nsrs: "Non-Serious",
pos: "Positive Connotation",
_pc: "Positive Connotation", // Alias for pos
neu: "Neutral Connotation",
neg: "Negative Connotation",
_nc: "Negative Connotation", // Alias for neg
p: "Platonic",
r: "Romantic",
a: "Alterous",
c: "Copypasta",
l: "Lyrics",
_ly: "Lyrics", // Alias for l
lh: "Light-Hearted",
nm: "Not Mad",
lu: "A Little Upset",
nbh: "Not Directed At Anybody Here",
nay: "Not Directed At You",
ay: "Directed At You",
nsb: "Not Subtweeting",
x: "Sexual Intent",
_sx: "Sexual Intent", // Alias for x
nx: "Non-Sexual Intent",
_nsx: "Non-Sexual Intent", // Alias for nx
rh: "Rhetorical Question",
_rt: "Rhetorical Question", // Alias for rh
t: "Teasing",
ij: "Inside Joke",
m: "Metaphorically",
li: "Literally",
hyp: "Hyperbole",
ex: "Exaggeration",
f: "Fake",
q: "Quote",
th: "Threat",
cb: "Clickbait",
ref: "Reference",
nf: "Not Forced",
nbr: "Not Being Rude",
ot: "Off-Topic",
nj: "Not Joking",
naj: "Not A Joke",
}),
);
export default defaultIndicators;

View file

@ -142,10 +142,10 @@ export default definePlugin({
]
},
{
find: "profileCardUsernameRow,children",
find: "profileCardUsernameRow,children:",
replacement: {
match: /(?<=accountProfileCard.{0,50}displayProfile:(\i).*?badges:)(\i)/,
replace: "[...$self.getBadges($1),...$2]"
match: /badges:(\i)(?<=displayProfile:(\i).+?)/,
replace: "badges:[...$self.getBadges($2),...$1]"
}
}
],

View file

@ -284,6 +284,8 @@ export default definePlugin({
// Allow users to use custom client themes
{
find: "customUserThemeSettings:{",
// Discord has two separate modules for treatments 1 and 2
all: true,
replacement: {
match: /(?<=\i=)\(0,\i\.\i\)\(\i\.\i\.TIER_2\)(?=,|;)/g,
replace: "true"

View file

@ -2,7 +2,7 @@
display: none;
}
.messagelogger-deleted:is(.messagelogger-deleted-attachment,
.messagelogger-deleted :is(.messagelogger-deleted-attachment,
.emoji,
[data-type="sticker"],
[class*="embedIframe"],
@ -95,4 +95,4 @@
.messagelogger-diff-disabled .messagelogger-diff-removed [class*="emojiContainer"]::after,
.messagelogger-diff-disabled .messagelogger-diff-removed img.emoji::after {
display: none !important;
}
}

View file

@ -17,119 +17,184 @@
*/
import { definePluginSettings, migratePluginSetting } from "@api/Settings";
import { Devs } from "@utils/constants";
import { runtimeHashMessageKey } from "@utils/intlHash";
import { Devs, EquicordDevs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { Message } from "@vencord/discord-types";
import { i18n, MessageStore, RelationshipStore } from "@webpack/common";
import { MessageStore, RelationshipStore } from "@webpack/common";
interface MessageDeleteProps {
// Internal intl message for BLOCKED_MESSAGE_COUNT
collapsedReason: () => any;
interface ChannelStreamDividerProps {
type: "DIVIDER",
content?: string,
contentKey?: string,
unreadId?: string,
}
// Remove this migration once enough time has passed
migratePluginSetting("NoBlockedMessages", "ignoreMessages", "ignoreBlockedMessages");
interface ChannelStreamMessageProps {
type: "MESSAGE",
content: Message,
}
interface ChannelStreamGroupProps {
type: "MESSAGE_GROUP_BLOCKED" | "MESSAGE_GROUP_IGNORED",
content: ChannelStreamMessageProps[] | any,
}
migratePluginSetting("NoBlockedMessages", "disableNotifications", "ignoreMessages");
migratePluginSetting("NoBlockedMessages", "alsoHideIgnoredUsers", "applyToIgnoredUsers");
const settings = definePluginSettings({
ignoreMessages: {
description: "Completely ignores incoming messages from blocked and ignored (if enabled) users",
alsoHideIgnoredUsers: {
description: "Also hide messages from ignored users.",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false
},
disableNotifications: {
description: "Hide new message notifications for blocked/ignored users. Always true if \"Default Hide Users\" is enabled below and the user triggering the notification is not exempted in \"Override Users\".",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
restartNeeded: false
},
hideRepliesToBlockedMessages: {
description: "Hides replies to blocked messages.",
hideBlockedUserReplies: {
description: "Hide replies to blocked/ignored users.",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: false,
},
applyToIgnoredUsers: {
description: "Additionally apply to 'ignored' users",
defaultHideUsers: {
type: OptionType.BOOLEAN,
description: "If enabled, messages from blocked/ignored users will be completely hidden and any messages from user IDs in the override list will be collapsed (default Discord behavior) instead. If disabled, messages from blocked/ignored users will be collapsed and any messages from user IDs in the override list will be completely hidden instead.",
default: true,
restartNeeded: false
}
restartNeeded: false,
},
overrideUsers: {
type: OptionType.STRING,
description: "Comma separated list of user IDs which will be hidden or collapsed instead of the default behavior selected above.",
restartNeeded: false,
default: ""
},
});
export default definePlugin({
name: "NoBlockedMessages",
description: "Hides all blocked/ignored messages from chat completely",
authors: [Devs.rushii, Devs.Samu, Devs.jamesbt365],
description: "Hide all blocked/ignored messages from chat completely.",
authors: [Devs.rushii, Devs.Samu, Devs.jamesbt365, Devs.Elvyra, EquicordDevs.Etorix],
settings,
patches: [
{
find: ".__invalid_blocked,",
replacement: [
{
match: /let{expanded:\i,[^}]*?collapsedReason[^}]*}/,
replace: "if($self.shouldHide(arguments[0]))return null;$&"
}
]
},
...[
'"MessageStore"',
'"ReadStateStore"'
].map(find => ({
find,
predicate: () => settings.store.ignoreMessages,
replacement: [
{
match: /(?<=function (\i)\((\i)\){)(?=.*MESSAGE_CREATE:\1)/,
replace: (_, _funcName, props) => `if($self.shouldIgnoreMessage(${props}.message)||$self.isReplyToBlocked(${props}.message))return;`
replace: (_, _funcName, props) => `if($self.disableNotification(${props}.message)){return;};`
}
]
})),
{
find: "referencedUsernameProfile,referencedAvatarProfile",
find: '"forum-post-action-bar-"',
replacement: [
{
match: /(?=\(0,\i.jsx\)\(\i.\i,\{offset)/,
replace: "$&!$self.isReplyToBlocked(arguments[0].message)&&",
match: /(\i).map\(/,
replace: "$self.filterStream($1).map("
}
],
]
},
],
shouldIgnoreMessage(message: Message) {
hideBlockedMessage(userId: string) {
const overrideUsers = settings.store.overrideUsers.split(",").map(id => id.trim()).filter(id => id.length > 0);
if (settings.store.defaultHideUsers) {
// If hidden by default, overriding shows messages as collapsed.
return overrideUsers.includes(userId);
} else {
// If collapsed by default, overriding hides messages.
return !overrideUsers.includes(userId);
}
},
shouldKeepMessage(message: Message) {
const blocked = this.isBlocked(message);
const replyToBlocked = this.isReplyToBlocked(message);
if (blocked) return [this.hideBlockedMessage(message.author.id), true];
if (replyToBlocked) return [this.hideBlockedMessage(replyToBlocked.author.id), true];
// [Message Visible, Author Blocked/Ignored]
return [true, false];
},
disableNotification(message: Message) {
if (!message) return false;
const messageFilteredData = this.shouldKeepMessage(message);
const messageHidden = !messageFilteredData[0];
const messageBlocked = messageFilteredData[1];
// Always disable notifications for completely hidden messages as not doing so will
// cause the client to scroll up into the loaded messages in search of an
// unread message which it can't find because it was filtered.
return messageHidden || (messageBlocked && settings.store.disableNotifications);
},
filterStream(channelStream: [ChannelStreamGroupProps | ChannelStreamMessageProps | ChannelStreamDividerProps]) {
const { alsoHideIgnoredUsers, disableNotifications, hideBlockedUserReplies, defaultHideUsers, overrideUsers } = settings.use();
const newChannelStream = channelStream.map(item => {
if (item.type === "MESSAGE_GROUP_BLOCKED" || (item.type === "MESSAGE_GROUP_IGNORED" && alsoHideIgnoredUsers)) {
const groupItem = item as ChannelStreamGroupProps;
const filteredContent = groupItem.content.filter((item: ChannelStreamMessageProps) => {
return item.type !== "MESSAGE" || this.shouldKeepMessage(item.content)[0];
});
return filteredContent.some(item => item.type === "MESSAGE") ? { ...groupItem, content: filteredContent } : null;
}
return (item.type !== "MESSAGE" || this.shouldKeepMessage((item as ChannelStreamMessageProps).content)[0]) ? item : null;
}).filter(item => item !== null);
let lastItem = newChannelStream[newChannelStream.length - 1];
// Remove the NEW Message divider if it is the last item,
// implying the messages it was announcing got filtered.
while (lastItem && lastItem.type === "DIVIDER" && lastItem.unreadId !== undefined) {
newChannelStream.pop();
lastItem = newChannelStream[newChannelStream.length - 1];
}
return newChannelStream;
},
isReplyToBlocked(message: Message) {
if (!settings.store.hideBlockedUserReplies) return false;
try {
// Messages received from the non-focused channel may have a referenced_message property.
let repliedMessage: Message | null = (message as any).referenced_message || null;
if (!repliedMessage) {
const messageReference = message.messageReference || (message as any).message_reference;
repliedMessage = messageReference ? MessageStore.getMessage(messageReference.channel_id, messageReference.message_id) : null;
}
return repliedMessage && this.isBlocked(repliedMessage) ? repliedMessage : false;
} catch (e) {
new Logger("NoBlockedMessages").error("Failed to check if referenced message is blocked or ignored:", e);
}
},
isBlocked(message: Message) {
try {
if (RelationshipStore.isBlocked(message.author.id)) {
return true;
}
return settings.store.applyToIgnoredUsers && RelationshipStore.isIgnored(message.author.id);
return settings.store.alsoHideIgnoredUsers && RelationshipStore.isIgnored(message.author.id);
} catch (e) {
new Logger("NoBlockedMessages").error("Failed to check if user is blocked or ignored:", e);
return false;
new Logger("NoBlockedMessages").error("Failed to check if message is blocked or ignored:", e);
}
},
shouldHide(props: MessageDeleteProps): boolean {
try {
const collapsedReason = props.collapsedReason();
const blockedReason = i18n.t[runtimeHashMessageKey("BLOCKED_MESSAGE_COUNT")]();
const ignoredReason = settings.store.applyToIgnoredUsers
? i18n.t[runtimeHashMessageKey("IGNORED_MESSAGE_COUNT")]()
: null;
return collapsedReason === blockedReason || collapsedReason === ignoredReason;
} catch (e) {
console.error(e);
return false;
}
},
isReplyToBlocked(message: Message) {
if (!settings.store.hideRepliesToBlockedMessages) return false;
try {
const { messageReference } = message;
if (!messageReference) return false;
const replyMessage = MessageStore.getMessage(messageReference.channel_id, messageReference.message_id);
return replyMessage ? this.shouldIgnoreMessage(replyMessage) : false;
} catch (e) {
new Logger("NoBlockedMessages").error("Failed to check if referenced message is blocked:", e);
}
}
});

View file

@ -64,7 +64,7 @@ export const settings = definePluginSettings({
},
disableCreateDMButton: {
type: OptionType.BOOLEAN,
description: "Disabled the create dm button",
description: "Disables the create dm button",
default: true
}
});
@ -160,9 +160,9 @@ export default definePlugin({
}
},
{
find: ".createDMButtonContainer",
find: ".createDMButtonContainer,",
replacement: {
match: /"full-width"===\i/,
match: /"create-dm"\)/,
replace: "$&&&false"
},
predicate: () => settings.store.disableCreateDMButton

View file

@ -153,7 +153,11 @@ export default definePlugin({
const myId = useStateFromStores([AuthenticationStore], () => AuthenticationStore.getId());
return Object.keys(typingUsers)
.filter(id => id && id !== myId && !RelationshipStore.isBlockedOrIgnored(id))
.filter(id => {
if (!id || RelationshipStore.isBlockedOrIgnored(id)) return false;
if (id === myId) return Settings.plugins.AmITyping?.enabled;
return true;
})
.map(id => UserStore.getUser(id))
.filter(isNonNullish);
} catch (e) {

View file

@ -19,41 +19,16 @@
import * as DataStore from "@api/DataStore";
import { showNotification } from "@api/Notifications";
import { Settings } from "@api/Settings";
import { Alerts, OAuth2AuthorizeModal, UserStore } from "@webpack/common";
import { OAuth2AuthorizeModal, UserStore } from "@webpack/common";
import { Logger } from "./Logger";
import { openModal } from "./modal";
import { relaunch } from "./native";
export const cloudLogger = new Logger("Cloud", "#39b7e0");
export const getCloudUrl = () => new URL(Settings.cloud.url);
const getCloudUrlOrigin = () => getCloudUrl().origin;
export async function checkCloudUrlCsp() {
if (IS_WEB) return true;
const { host } = getCloudUrl();
if (host === "api.vencord.dev") return true;
if (host === "cloud.equicord.org") return true;
if (await VencordNative.csp.isDomainAllowed(Settings.cloud.url, ["connect-src"])) {
return true;
}
const res = await VencordNative.csp.requestAddOverride(Settings.cloud.url, ["connect-src"], "Cloud Sync");
if (res === "ok") {
Alerts.show({
title: "Cloud Integration enabled",
body: `${host} has been added to the whitelist. Please restart the app for the changes to take effect.`,
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
}
return false;
}
const getUserId = () => {
const id = UserStore.getCurrentUser()?.id;
if (!id) throw new Error("User not yet logged in");
@ -104,8 +79,6 @@ export async function authorizeCloud() {
return;
}
if (!await checkCloudUrlCsp()) return;
try {
const oauthConfiguration = await fetch(new URL("/v1/oauth/settings", getCloudUrl()));
var { clientId, redirectUri } = await oauthConfiguration.json();

View file

@ -1145,6 +1145,10 @@ export const EquicordDevs = Object.freeze({
name: "Mocha",
id: 808802000224518264n
},
justjxke: {
name: "justjxke",
id: 852558183087472640n
}
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly

View file

@ -22,7 +22,7 @@ import { PlainSettings, Settings } from "@api/Settings";
import { moment, SettingsRouter, Toasts } from "@webpack/common";
import { deflateSync, inflateSync } from "fflate";
import { checkCloudUrlCsp, getCloudAuth, getCloudUrl } from "./cloud";
import { getCloudAuth, getCloudUrl } from "./cloud";
import { Logger } from "./Logger";
import { relaunch } from "./native";
import { chooseFile, saveFile } from "./web";
@ -118,8 +118,6 @@ const cloudSettingsLogger = new Logger("Cloud:Settings", "#39b7e0");
export async function putCloudSettings(manual?: boolean) {
const settings = await exportSettings({ minify: true });
if (!await checkCloudUrlCsp()) return;
try {
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
method: "PUT",
@ -164,8 +162,6 @@ export async function putCloudSettings(manual?: boolean) {
}
export async function getCloudSettings(shouldNotify = true, force = false) {
if (!await checkCloudUrlCsp()) return;
try {
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
method: "GET",
@ -268,8 +264,6 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
}
export async function deleteCloudSettings() {
if (!await checkCloudUrlCsp()) return;
try {
const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
method: "DELETE",