Compare commits

..

47 commits

Author SHA1 Message Date
826e774af1
Merge branch 'hive-up-2' into hive
upstream fixes
2025-07-03 14:36:03 -04:00
Vendicated
93f28fe984
TypingTweaks: fix typo 2025-07-01 23:30:29 +02:00
Vendicated
4b0ff3ee5f
bump to v1.12.5 2025-07-01 23:27:54 +02:00
Vendicated
43ba1a4a5e
TypingIndicator: fix TypingTweaks compatibility 2025-07-01 23:18:12 +02:00
Vendicated
23eb85e898
AppleMusicRichPresence: fix broken album covers 2025-07-01 23:13:38 +02:00
Vendicated
9e22ab305c
Fix plugins using Message Popover buttons 2025-07-01 23:06:06 +02:00
Vendicated
1b2bc07592
TypingTweaks: fix several users are typing display 2025-07-01 22:57:28 +02:00
Vendicated
18274e4f0e
Fix TypingTweaks 2025-07-01 22:36:25 +02:00
Damian
468b290d28
FakeNitro: add 96 to available emoji sizes (#3526) 2025-06-29 20:56:20 +02:00
Vendicated
f6d92e5024
Update more Discord css variables 2025-06-28 03:56:49 +02:00
Vendicated
a25d26e921
Update to new Discord css variable names
Co-Authored-By: sadan <117494111+sadan4@users.noreply.github.com>
Co-Authored-By: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2025-06-28 03:47:58 +02:00
Vendicated
65f41cb7bd
Update GuildStore -> GuildRoleStore 2025-06-27 21:36:31 +02:00
sadan4
864ee7c7ad
Fix BlurNSFW and ShowMeYourName for latest version (#3517) 2025-06-27 13:45:22 -03:00
Vendicated
decb49fc0a
v1.12.4 2025-06-25 03:27:34 +02:00
sadan4
b6ffb33adc
Fix plugins for latest discord update (#3509)
Co-authored-by: Etorix <92535668+EtorixDev@users.noreply.github.com>
Co-authored-by: V <vendicated@riseup.net>
2025-06-25 03:25:36 +02:00
sadan4
9b24535d44
FakeNitro: fix crash when embed.url is undefined (#3510) 2025-06-25 02:44:11 +02:00
sadan4
658a62860e
BetterFolders: Fix sidebar filter patch (#3498) 2025-06-20 13:45:52 -03:00
Vendicated
f6bfd18816
ViewIcons: fix viewing animated icons/banners 2025-06-19 23:03:15 +02:00
Vendicated
ba76c43a26
ServerInfo: fix Blocked & Ignored tabs 2025-06-19 22:50:53 +02:00
Vendicated
96516f113a
ReplaceGoogleSearch: fix broken icons 2025-06-18 15:48:07 +02:00
Etorix
e4b1a196ae
fix Settings::onChange being fired twice (#3496) 2025-06-18 03:55:10 +02:00
Vendicated
7779e5a1ec
fix IrcColors 2025-06-17 22:37:56 +02:00
Vendicated
0444831073
RoleColorEverywhere: fix chat mentions 2025-06-17 22:15:11 +02:00
Vendicated
a6c1f97d12
Fix AnonymiseFileNames 2025-06-17 22:03:13 +02:00
Vendicated
8d97863db6
Fix ImplicitRelationships, RelationshipNotifier & ServerInfo 2025-06-17 21:58:12 +02:00
Vendicated
3a1e17e04d
remove redundant methods 2025-06-14 18:55:12 +02:00
Vendicated
78d3330ccf
make Open Themes/Settings folder properly open the folder 2025-06-14 18:51:40 +02:00
Vendicated
2a398985cf
fix: correctly allow resources from localhost 2025-06-14 00:55:27 +02:00
T1ckbase
b35b72c066
Translate: Make translation more readable (#3252) 2025-06-12 02:29:50 +02:00
Randomuser8219
a366693e96
ServerInfo: rename "Nitro Boosts" -> "Server Boosts" (#3364) 2025-06-12 02:24:24 +02:00
Vending Machine
ed5ed4b80a
Allow users to manually whitelist Domains for use in themes (#3476) 2025-06-12 02:19:45 +02:00
Nuckyz
7f2c4a3566
Delete Banger plugin ~ Ban modal no longer contains a gif
Discord removed the gifs displayed in the ban modal, so this plugin serves no purpose anymore.
2025-06-11 19:17:25 -03:00
Nuckyz
7112caaedd
Experiments: Fix toolbar help menu dev icon patch 2025-06-11 19:17:24 -03:00
Vendicated
18f2b49b67
fix loading themes with spaces in their name 2025-06-09 01:56:19 +02:00
Vending Machine
b19bb2b7af
Update README (#3472) 2025-06-08 20:10:30 +02:00
Vending Machine
6d47a340b1
QuickReply: correctly handle new & deleted messages (#3473) 2025-06-08 19:45:52 +02:00
Cookie
a386736dcc
WebScreenShareFixes: only apply stereo parameter to video audio (#3474) 2025-06-08 17:47:59 +02:00
Mia
bf68a8a3e8
MessageClickActions: make delete key detection consistent on lost focus (#3470)
Co-authored-by: Vending Machine <vendicated@riseup.net>
2025-06-08 16:55:12 +02:00
Vendicated
bb106b7c49
whitelist catbox for use in themes 2025-06-07 01:31:04 +02:00
Vendicated
3a2a16a09c
whitelist tenor and pinterest for use in themes 2025-06-07 01:18:14 +02:00
Vendicated
5f21eaabf8
bump to v1.12.3 2025-06-07 01:02:57 +02:00
Vanilla
4436e6d81d
Fix missing background on notifications (#3386)
Co-authored-by: Vending Machine <vendicated@riseup.net>
2025-06-07 00:53:58 +02:00
Vending Machine
47856a26f1
Updater: fix network errors triggering popups (#3436) 2025-06-07 00:46:49 +02:00
Nuckyz
9430803f36
TypingTweaks: Fix typing avatars and names disappearing 2025-06-06 19:11:06 -03:00
Vendicated
c19827a0e5
UserScript: disable theme ui, instead recommend Stylus 2025-06-07 00:06:01 +02:00
Vendicated
fae15dbdfe
avoid showing ugly red error cards to users 2025-06-06 18:51:05 +02:00
Vending Machine
e7076f5aee
Use much stricter, whitelist based CSP (#3162) 2025-06-06 18:30:19 +02:00
109 changed files with 1085 additions and 584 deletions

View file

@ -9,24 +9,22 @@ Installing is the same as the Vencord devs laid out: [(Install)](https://docs.ve
# Vencord
![](https://img.shields.io/github/package-json/v/Vendicated/Vencord?style=for-the-badge&logo=github&logoColor=d3869b&label=&color=1d2021&labelColor=282828)
[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Vee/cord&color=2185D0&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAKbUlEQVR4nNVae3AV5RX/nW/3Pva+b24e5HHzIICQKGoiYiW8NFBFgohaa6ctglpbFSujSGurzUinohWsOij/gGX6R2fqOK0d1FYTEZXaTrWCBbEikJCEyCvkeXNvkrunf+zdkJDkPnex/c3cmd29+53v/M6e73znnF2Cydj4Tntldzi6qrN/qKqzf2jy6b7BnL4B1dI7oMp9AyoRAIdVsNMqhlxWMZjtspzyK/Jhr036OMsm//bh2vzPzNSPzBD6xFutd7R0Dq758ky4orkjYuc05RCAkixbeEq2/UCJ1/LczxcX/c5IPfU5DMHmxpbCpu7o1k/b+xc1n43YjJI7EqV+W2RmvuPt0oDjB2vn5bQbITNjAzzdeKK8qTO0bU9T77zucNQUjzofHrvENWWu3aUBZfW6+ZOOZiIrbYXrmUXo9daX3v6i667O/iGRiRLpwqtIvKDc+0efJ3hb/UIaSkdGWgZ4sqGt9r2m3lc/P9HvSWe80ZiRp3TPL/UsX1+bvyvVsSkb4NE3WjbuPNj5SM8Fcvdk4bAKrqvwv7DxhuCPUxmXNIn6XSy3nWr6R8OhrqrU1btwqJ3m/bgwu/SqZJdEUgbYsuuka09b9/4Pm3tLMlPvwuAbpe6m+RcplfdcURBKdG9CA2zZddLV2Nx1+JO2vlxj1LswqCpynlxc6SxLZIS40bueWfy9vXvv/xt5APhXa1/u7v+EPqvfxXK8++IaoO2Vpn9+cLS33FjVLhw+bOotOX7q6N/i3TOhAX7y+rHN/+sBLxm8fah71k93tjw/0f/jGuDJxtZrdh7setA8tS4sdn7eef+v3mmfP95/Ywxw6x9Yev9I35/6Iubv83WVfl5a6Uu3VkoavZEo7TnS/Vo98xi+Yy6UKC3bDp7sd5ut1OWFDjyzNMib6oq5Oug0ezp8dqLfG3r92Nbzr48ywNONJ8obDnV/z2xlAk4ZW1aUqhaJIAvCb5YVqwFn3GBtCBoO9dz5TOPxUbnMKAM0dYa2d5lc2AgCNi8r5klui3aBgWynjE11QZbI3FV3NjQkjnYNbB+lj36wubGlcE9T71xTNQDw0Px8nlvmHl73GmfCrKCL19Tkmh4P9jT1LHz2vVP5+vmwAZq71a1m1/PXTPXwD68eS5KIEVUZd1yZwwumeEw1Qld/lJrPhF7Sz4cNsO+rUK2ZExd6rfj10iCPZ2GJCCoAZuCJxQUc9FvNVAX72kPX6ccC0Hp4zR0Ru1kT2mTCSzeXqn5l/EAniMAqoDLDYZWwqa5EVSzmhaKmsxHbLxvbbgdiBmjpHFxj2mwANlxXxBdPUib8nwgQgqAyEFUZxT4L1i/MN3UpHDsTWQvEDHDoTLjCrIluuyzAt8zMSkhGFhp5hrYUFk3z8IqZftOMcKRj4GIAEM80tFccM8n9Z+Qq+MXigqRIWCQCMzQvYIbKwH1X53FFnjkr88iZsLKpoXWa6BiIrjbDzF67hK23lKp2Obm1LAstPEZVjTwDkAio/2ZQ9dolw/VjAB0DfKfoCg9WGy2cADy1NMhBX2rR3CIRGICq8rAhAg4Jj9UWsDBhg+4MR6vF2VC0zGjB99fk8eJp3pQdyyrRMHF9KURVxswCB6+alWO4o3b2RyeLU32D2UYKnVPm5gfm5qWlrF0Wo4hzbCmoDNw0089XlboNNcLpvsFc0RtRDXuNle+x4Lkbi9PO6WWJIBFGEY+qjGjswtq5eVzosRilLnoiUavoH1INiTCyIDy/vETNcmRW1dl0L4gRVxmx3YFhlwnrry1QrZIxASE0yJIIDaiGSHt8UQFXF2Ve1zusYgzxkXGhyGvFvePUE+mgfyAqhGqAqKWVPv5udbYhSjmtkpYWq6OJqzFjqCpjTpmbl1Rk3klSGRBWmTISNC3Hjo1LgoYFJ0GA1aIVR+cTVxlQoS2Pb18a4PLszMKXzSJYuCySmq4Al03CiytKVYfBhYvLKk1IXE+XLRLhwZp81WlNf26HTFHhd0jhdAYTgKduCPLkgPHfQjitYkLiAIEZBDBlu2R6aF7euCV2Mgg45bDw2qWOdAavnp3D109PPdlJBvpTnYg4kVY3MDMuylVw62WJi63x4LHLZ0TAIR9OdWBVodPUclUQwWmT4hLXfgCIUDfDi6oiR8rzBJzyl8LnkD9KZVCOU8aLN5eoshnJ+Qh4bFJC4gztmEjgrtk5anaKnWWfXfpIuBTLjmSpSILw/E0laq7LuGxsIngVCYmIa96hLRG3TaZ1C/KTfjAEQLFIO8TPFk7aH/RZI8kMWrdgEs8udqXLKSUoMkEW4ETEQTRsoHyPlVZfmVw+Uuy3hR9bVHBQAMD0XPu/Ew24dqqH777K/La1DiKCxyYlRRzQymgG4+oyDxZOTdxZnp5r3wvEWmJ5btuL8W4uzbJh87LitLebdOFVpKSJx4IlwIzbL81CcYLO8iSX/IImGQCYae6Wg/2tXQNjNnW7LPDKyilqZd7ETU2zEBlifNTSS4i9PNFIx44x4jh2nZlBsUr0dN8QP/6XVhEaHJvnlfhtkXd/NF0BUextKRFXFznfGk+JDdcX8tdBHtDa6YpFsB4I9ac88omf8wbEgqa2XAIOme6bM35foqrQ+QZIKwGG80ifVbrXZZNGDfhOVYBvviS9JMMoaP3AEcQpPnHdOxiMGXkKbrx4dGfZY5c4T8H9+vmwAeqXFLXOKXW9r59fWuDA44sKv1byAOBzyCkTH+kdS2f4MLPgXJI0p9T17vrFxcf181GVxEUB+0qfIqt+RcKWFSWGNR4ygd4RTpW4HiCJgFWzstmnSPA7ZLU827pypPwxDB/687GXl1X6Vs6bbGz/LRN80hZCT+yLFZ0cgHED4egACeiXm89GsP9EePuzy4rvGil7jAGYmQDsBjDHUBYZ4GhHBMfORigd4rpnyIS9u6d4rqgnGrUtjCmmSYuOqwB0GcwjbWh9xviurpNnxnDA1IspMPe6bOL755MHJvhKjIgOA7jbJD4pw22Thj+kSIW47h2KRaydVezeP57sCdspRPQqgGeNJJIuBAE+ReJUiOv32mXaXjPZs21C2QnmXgdghyEsMoRfkVMiDgCywF/by9z3xJMb1wCxeHAPgDczZpAh/Iq+HSYmDjCsstgThmf5t4ii8eQm7CgS0SCA5QBezoRApnBaBSyCEhIHCLJEb4ZUd+2SqZSwzE+qpUpEQ9CC4qb01M8cRIQsh8zxiKsMtsn08nvlnrpkyAPj5AGJwMw3AtgGwJ/q2ExxvHsQB74KxfKBMblAyGmTHq4pc4/5GjQeUm6qE9FrAK4E8H6ie41GlkN/jTk6F5Ak2ueUpNmpkgfSMAAAENERAAsB3AHgZDoy0oFdFnBYpXPEBfU4beLRD6Z4qmumug+kIzPjaoeZfQDWAHgAQFam8hLh4MkwWjsHemyS2OF08IYrCjynzZ4zKTCzi5nXMvOnzBw16bevIxR95JOj7DNKb1PqXWa+HMDtAGoBXII0lxq0N2OfAmgA8Hsi2muMhudgesHPzNkA5gKoADADwFRoS8UHQO+x9wLoBNAB4AsAnwM4AOADIjLVxf8L9kdXUOE0IskAAAAASUVORK5CYII=)](https://codeberg.org/Vee/cord)
The cutest Discord client mod
| ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) |
| :--------------------------------------------------------------------------------------------------: |
| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) |
![](https://github.com/user-attachments/assets/3fac98c0-c411-4d2a-97a3-13b7da8687a2)
## Features
- Super easy to install (Download Installer, open, click install button, done)
- 100+ plugins built in: [See a list](https://vencord.dev/plugins)
- Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, PermissionsViewer, CustomCommands, ShowHiddenChannels, PronounDB
- Easy to install
- [100+ built in plugins](https://vencord.dev/plugins)
- Fairly lightweight despite the many inbuilt plugins
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
- Works on any Discord branch: Stable, Canary or PTB all work
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
- Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
- Privacy friendly: blocks Discord analytics & crash reporting out of the box and has no telemetry
- Maintained very actively, broken plugins are usually fixed within 12 hours
- Settings sync: Keep your plugins and their settings synchronised between devices / apps (optional)

View file

@ -20,16 +20,13 @@
/// <reference path="../src/globals.d.ts" />
import monacoHtmlLocal from "file://monacoWin.html?minify";
import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
import * as DataStore from "../src/api/DataStore";
import { debounce } from "../src/utils";
import { debounce, localStorage } from "../src/utils";
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
import { getTheme, Theme } from "../src/utils/discord";
import { getThemeInfo } from "../src/main/themes";
import { Settings } from "../src/Vencord";
// Discord deletes this so need to store in variable
const { localStorage } = window;
import { getStylusWebStoreUrl } from "@utils/web";
// listeners for ipc.on
const cssListeners = new Set<(css: string) => void>();
@ -45,12 +42,13 @@ window.VencordNative = {
themes: {
uploadTheme: (fileName: string, fileData: string) => DataStore.set(fileName, fileData, themeStore),
deleteTheme: (fileName: string) => DataStore.del(fileName, themeStore),
getThemesDir: async () => "",
getThemesList: () => DataStore.entries(themeStore).then(entries =>
entries.map(([name, css]) => getThemeInfo(css, name.toString()))
),
getThemeData: (fileName: string) => DataStore.get(fileName, themeStore),
getSystemValues: async () => ({}),
openFolder: async () => Promise.reject("themes:openFolder is not supported on web"),
},
native: {
@ -77,6 +75,14 @@ window.VencordNative = {
addThemeChangeListener: NOOP,
openFile: NOOP_ASYNC,
async openEditor() {
if (IS_USERSCRIPT) {
const shouldOpenWebStore = confirm("QuickCSS is not supported on the Userscript. You can instead use the Stylus extension.\n\nDo you want to open the Stylus web store page?");
if (shouldOpenWebStore) {
window.open(getStylusWebStoreUrl(), "_blank");
}
return;
}
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
const win = open("about:blank", "VencordQuickCss", features);
if (!win) {
@ -92,7 +98,7 @@ window.VencordNative = {
? "vs-light"
: "vs-dark";
win.document.write(IS_EXTENSION ? monacoHtmlLocal : monacoHtmlCdn);
win.document.write(monacoHtmlLocal);
},
},
@ -106,8 +112,9 @@ window.VencordNative = {
}
},
set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)),
getSettingsDir: async () => "LocalStorage"
openFolder: async () => Promise.reject("settings:openFolder is not supported on web"),
},
pluginHelpers: {} as any,
csp: {} as any,
};

View file

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

View file

@ -31,6 +31,7 @@ const defines = stringifyValues({
IS_UPDATER_DISABLED,
IS_WEB: false,
IS_EXTENSION: false,
IS_USERSCRIPT: false,
VERSION,
BUILD_TIMESTAMP
});

View file

@ -43,6 +43,7 @@ const commonOptions = {
define: stringifyValues({
IS_WEB: true,
IS_EXTENSION: false,
IS_USERSCRIPT: false,
IS_STANDALONE: true,
IS_DEV,
IS_REPORTER,
@ -98,6 +99,7 @@ const buildConfigs = [
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: {
...commonOptions.define,
IS_USERSCRIPT: "true",
window: "unsafeWindow",
},
outfile: "dist/Vencord.user.js",

View file

@ -134,8 +134,12 @@ async function init() {
if (!IS_WEB && !IS_UPDATER_DISABLED) {
runUpdateCheck();
// this tends to get really annoying, so only do this if the user has auto-update without notification enabled
if (Settings.autoUpdate && !Settings.autoUpdateNotification) {
setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes
}
}
if (IS_DEV) {
const pendingPatches = patches.filter(p => !p.all && p.predicate?.() !== false);

View file

@ -5,6 +5,7 @@
*/
import type { Settings } from "@api/Settings";
import { CspRequestResult } from "@main/csp/manager";
import { PluginIpcMappings } from "@main/ipcPlugins";
import type { UserThemeHeader } from "@main/themes";
import { IpcEvents } from "@shared/IpcEvents";
@ -33,10 +34,11 @@ export default {
themes: {
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
getThemesDir: () => invoke<string>(IpcEvents.GET_THEMES_DIR),
getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName),
getSystemValues: () => invoke<Record<string, string>>(IpcEvents.GET_THEME_SYSTEM_VALUES),
openFolder: () => invoke<void>(IpcEvents.OPEN_THEMES_FOLDER),
},
updater: {
@ -49,7 +51,8 @@ export default {
settings: {
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify),
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
openFolder: () => invoke<void>(IpcEvents.OPEN_SETTINGS_FOLDER),
},
quickCss: {
@ -73,5 +76,17 @@ export default {
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
},
csp: {
/**
* Note: Only supports full explicit matches, not wildcards.
*
* If `*.example.com` is allowed, `isDomainAllowed("https://sub.example.com")` will return false.
*/
isDomainAllowed: (url: string, directives: string[]) => invoke<boolean>(IpcEvents.CSP_IS_DOMAIN_ALLOWED, url, directives),
removeOverride: (url: string) => invoke<boolean>(IpcEvents.CSP_REMOVE_OVERRIDE, url),
requestAddOverride: (url: string, directives: string[], callerName: string) =>
invoke<CspRequestResult>(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, url, directives, callerName),
},
pluginHelpers: PluginHelpers
};

View file

@ -48,7 +48,7 @@ export function _modifyAccessories(
) {
for (const [key, accessory] of accessories.entries()) {
const res = (
<ErrorBoundary message={`Failed to render ${key} Message Accessory`} key={key}>
<ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}>
<accessory.render {...props} />
</ErrorBoundary>
);

View file

@ -3,8 +3,8 @@
all: unset;
display: flex;
flex-direction: column;
color: var(--text-normal);
background-color: var(--background-secondary-alt);
color: var(--text-default);
background-color: var(--background-base-lower-alt);
border-radius: 6px;
overflow: hidden;
cursor: pointer;
@ -12,7 +12,7 @@
}
.visual-refresh .vc-notification-root {
background-color: var(--bg-overlay-floating, var(--background-base-low));
background-color: var(--background-base-low);
}
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {

View file

@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => {
logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
}
get isNoop() {
if (IS_DEV) return false;
return this.props.noop;
}
render() {
if (this.state.error === NO_ERROR) return this.props.children;
if (this.props.noop) return null;
if (this.isNoop) return null;
if (this.props.fallback)
return (

View file

@ -3,5 +3,9 @@
background-color: #e7828430;
border: 1px solid #e78284;
border-radius: 5px;
color: var(--text-normal, white);
color: var(--text-default, white);
& a:hover {
text-decoration: underline;
}
}

View file

@ -28,6 +28,9 @@ export function Link(props: React.PropsWithChildren<Props>) {
props.style.pointerEvents = "none";
props["aria-disabled"] = true;
}
props.rel ??= "noreferrer";
return (
<a role="link" target="_blank" {...props}>
{props.children}

View file

@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
{!!plugin.settingsAboutComponent && (
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
<Forms.FormSection>
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
<plugin.settingsAboutComponent tempSettings={tempSettings} />
</ErrorBoundary>
</Forms.FormSection>

View file

@ -70,7 +70,7 @@
padding: 1em;
background: var(--info-warning-background);
border: 1px solid var(--info-warning-foreground);
color: var(--info-warning-text);
color: var(--info-warning-foreground);
}
.vc-plugins-restart-button {

View file

@ -21,7 +21,7 @@ import { Settings, useSettings } from "@api/Settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Grid } from "@components/Grid";
import { Link } from "@components/Link";
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
import { authorizeCloud, checkCloudUrlCsp, 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";
@ -38,6 +38,8 @@ 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() }

View file

@ -148,7 +148,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
)}
{compileResult &&
<Forms.FormText style={{ color: compileResult[0] ? "var(--text-positive)" : "var(--text-danger)" }}>
<Forms.FormText style={{ color: compileResult[0] ? "var(--status-positive)" : "var(--text-danger)" }}>
{compileResult[1]}
</Forms.FormText>
}

View file

@ -18,17 +18,21 @@
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { openPluginModal } from "@components/PluginSettings/PluginModal";
import type { UserThemeHeader } from "@main/themes";
import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
import { useAwaiter, useForceUpdater } from "@utils/react";
import { getStylusWebStoreUrl } from "@utils/web";
import { findLazy } from "@webpack";
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import { Alerts, Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react";
import Plugins from "~plugins";
@ -65,7 +69,7 @@ function Validator({ link }: { link: string; }) {
: "Valid!";
return <Forms.FormText style={{
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--text-positive)"
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--status-positive)"
}}>{text}</Forms.FormText>;
}
@ -159,7 +163,6 @@ function ThemesTab() {
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
useEffect(() => {
refreshLocalThemes();
@ -219,6 +222,12 @@ function ThemesTab() {
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
</Card>
<Card className="vc-settings-card">
<Forms.FormTitle tag="h5">External Resources</Forms.FormTitle>
<Forms.FormText>For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked.</Forms.FormText>
<Forms.FormText>Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts.</Forms.FormText>
</Card>
<Forms.FormSection title="Local Themes">
<QuickActionCard>
<>
@ -241,8 +250,7 @@ function ThemesTab() {
) : (
<QuickAction
text="Open Themes Folder"
action={() => showItemInFolder(themeDir!)}
disabled={themeDirPending}
action={() => VencordNative.themes.openFolder()}
Icon={FolderIcon}
/>
)}
@ -347,10 +355,99 @@ function ThemesTab() {
</TabBar.Item>
</TabBar>
<CspErrorCard />
{currentTab === ThemeTab.LOCAL && renderLocalThemes()}
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
</SettingsTab>
);
}
export default wrapTab(ThemesTab, "Themes");
export function CspErrorCard() {
if (IS_WEB) return null;
const errors = useCspErrors();
const forceUpdate = useForceUpdater();
if (!errors.length) return null;
const isImgurHtmlDomain = (url: string) => url.startsWith("https://imgur.com/");
const allowUrl = async (url: string) => {
const { origin: baseUrl, host } = new URL(url);
const result = await VencordNative.csp.requestAddOverride(baseUrl, ["connect-src", "img-src", "style-src", "font-src"], "Vencord Themes");
if (result !== "ok") return;
CspBlockedUrls.forEach(url => {
if (new URL(url).host === host) {
CspBlockedUrls.delete(url);
}
});
forceUpdate();
Alerts.show({
title: "Restart Required",
body: "A restart is required to apply this change",
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
};
const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);
return (
<ErrorCard className="vc-settings-card">
<Forms.FormTitle tag="h5">Blocked Resources</Forms.FormTitle>
<Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>
<Forms.FormText>It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.</Forms.FormText>
<Forms.FormText>
After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} to apply the change.
</Forms.FormText>
<Forms.FormTitle tag="h5" className={classes(Margins.top16, Margins.bottom8)}>Blocked URLs</Forms.FormTitle>
<div className="vc-settings-csp-list">
{errors.map((url, i) => (
<div key={url}>
{i !== 0 && <Forms.FormDivider className={Margins.bottom8} />}
<div className="vc-settings-csp-row">
<Link href={url}>{url}</Link>
<Button color={Button.Colors.PRIMARY} onClick={() => allowUrl(url)} disabled={isImgurHtmlDomain(url)}>
Allow
</Button>
</div>
</div>
))}
</div>
{hasImgurHtmlDomain && (
<>
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom16)} />
<Forms.FormText>
Imgur links should be direct links in the form of <code>https://i.imgur.com/...</code>
</Forms.FormText>
<Forms.FormText>To obtain a direct link, right-click the image and select "Copy image address".</Forms.FormText>
</>
)}
</ErrorCard>
);
}
function UserscriptThemesTab() {
return (
<SettingsTab title="Themes">
<Card className="vc-settings-card">
<Forms.FormTitle tag="h5">Themes are not supported on the Userscript!</Forms.FormTitle>
<Forms.FormText>
You can instead install themes with the <Link href={getStylusWebStoreUrl()}>Stylus extension</Link>!
</Forms.FormText>
</Card>
</SettingsTab>
);
}
export default IS_USERSCRIPT
? wrapTab(UserscriptThemesTab, "Themes")
: wrapTab(ThemesTab, "Themes");

View file

@ -94,7 +94,7 @@ function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
<span style={{
marginLeft: "0.5em",
color: "var(--text-normal)"
color: "var(--text-default)"
}}>{message} - {author}</span>
</div>
))}

View file

@ -26,8 +26,7 @@ import { gitRemote } from "@shared/vencordUserAgent";
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
import { Margins } from "@utils/margins";
import { identity, isPluginDev } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { relaunch } from "@utils/native";
import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
import BadgeAPI from "../../plugins/_api/badges";
@ -53,9 +52,6 @@ type KeysOfType<Object, Type> = {
}[keyof Object];
function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
fallbackValue: "Loading..."
});
const settings = useSettings();
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
@ -171,7 +167,7 @@ function VencordSettings() {
<QuickAction
Icon={FolderIcon}
text="Open Settings Folder"
action={() => showItemInFolder(settingsDir)}
action={() => VencordNative.settings.openFolder()}
/>
)}
<QuickAction

View file

@ -1,5 +1,5 @@
.vc-addon-card {
background-color: var(--background-secondary-alt);
background-color: var(--background-base-lower-alt);
color: var(--interactive-active);
border-radius: 8px;
display: block;

View file

@ -14,7 +14,7 @@
.vc-settings-quickActions-pill {
all: unset;
background: var(--background-secondary);
background: var(--background-base-lower);
color: var(--header-secondary);
display: flex;
align-items: center;
@ -26,7 +26,7 @@
}
.vc-settings-quickActions-pill:hover {
background: var(--background-secondary-alt);
background: var(--background-base-lower-alt);
transform: translateY(-1px);
box-shadow: var(--elevation-high);
}

View file

@ -23,13 +23,13 @@
.vc-backup-restore-card {
background-color: var(--info-warning-background);
border-color: var(--info-warning-foreground);
color: var(--info-warning-text);
color: var(--info-warning-foreground);
}
.vc-settings-theme-links {
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
display: inline-block !important;
color: var(--text-normal) !important;
color: var(--text-default) !important;
padding: 0.5em;
border: 1px solid var(--background-modifier-accent);
max-height: unset;

View file

@ -7,7 +7,7 @@
.vc-settings-theme-card {
display: flex;
flex-direction: column;
background-color: var(--background-secondary-alt);
background-color: var(--background-base-lower-alt);
color: var(--interactive-active);
border-radius: 8px;
padding: 1em;
@ -27,3 +27,26 @@
.vc-settings-theme-author::before {
content: "by ";
}
.vc-settings-csp-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.vc-settings-csp-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
& a {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
line-height: 1.2em;
}
--custom-button-button-md-height: 26px;
}

View file

@ -3,7 +3,7 @@
}
.vc-owner-crown-icon {
color: var(--text-warning);
color: var(--status-warning);
}
.vc-heart-icon {

3
src/globals.d.ts vendored
View file

@ -29,11 +29,12 @@ declare global {
* replace: "IS_WEB?foo:bar"
* // GOOD
* replace: IS_WEB ? "foo" : "bar"
* // also good
* // also okay
* replace: `${IS_WEB}?foo:bar`
*/
export var IS_WEB: boolean;
export var IS_EXTENSION: boolean;
export var IS_USERSCRIPT: boolean;
export var IS_STANDALONE: boolean;
export var IS_UPDATER_DISABLED: boolean;
export var IS_DEV: boolean;

154
src/main/csp/index.ts Normal file
View file

@ -0,0 +1,154 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NativeSettings } from "@main/settings";
import { session } from "electron";
type PolicyMap = Record<string, string[]>;
export const ConnectSrc = ["connect-src"];
export const ImageSrc = [...ConnectSrc, "img-src"];
export const CssSrc = ["style-src", "font-src"];
export const ImageAndCssSrc = [...ImageSrc, ...CssSrc];
export const ImageScriptsAndCssSrc = [...ImageAndCssSrc, "script-src", "worker-src"];
// Plugins can whitelist their own domains by importing this object in their native.ts
// script and just adding to it. But generally, you should just edit this file instead
export const CspPolicies: PolicyMap = {
"http://localhost:*": ImageAndCssSrc,
"http://127.0.0.1:*": ImageAndCssSrc,
"localhost:*": ImageAndCssSrc,
"127.0.0.1:*": ImageAndCssSrc,
"*.github.io": ImageAndCssSrc, // GitHub pages, used by most themes
"github.com": ImageAndCssSrc, // GitHub content (stuff uploaded to markdown forms), used by most themes
"raw.githubusercontent.com": ImageAndCssSrc, // GitHub raw, used by some themes
"*.gitlab.io": ImageAndCssSrc, // GitLab pages, used by some themes
"gitlab.com": ImageAndCssSrc, // GitLab raw, used by some themes
"*.codeberg.page": ImageAndCssSrc, // Codeberg pages, used by some themes
"codeberg.org": ImageAndCssSrc, // Codeberg raw, used by some themes
"*.githack.com": ImageAndCssSrc, // githack (namely raw.githack.com), used by some themes
"jsdelivr.net": ImageAndCssSrc, // jsDelivr, used by very few themes
"fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes
"i.imgur.com": ImageSrc, // Imgur, used by some themes
"i.ibb.co": ImageSrc, // ImgBB, used by some themes
"i.pinimg.com": ImageSrc, // Pinterest, used by some themes
"*.tenor.com": ImageSrc, // Tenor, used by some themes
"files.catbox.moe": ImageAndCssSrc, // Catbox, used by some themes
"cdn.discordapp.com": ImageAndCssSrc, // Discord CDN, used by Vencord and some themes to load media
"media.discordapp.net": ImageSrc, // Discord media CDN, possible alternative to Discord CDN
// CDNs used for some things by Vencord.
// FIXME: we really should not be using CDNs anymore
"cdnjs.cloudflare.com": ImageScriptsAndCssSrc,
"cdn.jsdelivr.net": ImageScriptsAndCssSrc,
// Function Specific
"api.github.com": ConnectSrc, // used for updating Vencord itself
"ws.audioscrobbler.com": ConnectSrc, // Last.fm API
"translate-pa.googleapis.com": ConnectSrc, // Google Translate API
"*.vencord.dev": ImageSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)
"manti.vendicated.dev": ImageSrc, // ReviewDB API
"decor.fieryflames.dev": ConnectSrc, // Decor API
"ugc.decor.fieryflames.dev": ImageSrc, // Decor CDN
"sponsor.ajay.app": ConnectSrc, // Dearrow API
"dearrow-thumb.ajay.app": ImageSrc, // Dearrow Thumbnail CDN
"usrbg.is-hardly.online": ImageSrc, // USRBG API
"icons.duckduckgo.com": ImageSrc, // DuckDuckGo Favicon API (Reverse Image Search)
};
const findHeader = (headers: PolicyMap, headerName: Lowercase<string>) => {
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
};
const parsePolicy = (policy: string): PolicyMap => {
const result: PolicyMap = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyMap): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
const patchCsp = (headers: PolicyMap) => {
const reportOnlyHeader = findHeader(headers, "content-security-policy-report-only");
if (reportOnlyHeader)
delete headers[reportOnlyHeader];
const header = findHeader(headers, "content-security-policy");
if (header) {
const csp = parsePolicy(headers[header][0]);
const pushDirective = (directive: string, ...values: string[]) => {
csp[directive] ??= [...(csp["default-src"] ?? [])];
csp[directive].push(...values);
};
pushDirective("style-src", "'unsafe-inline'");
// we could make unsafe-inline safe by using strict-dynamic with a random nonce on our Vencord loader script https://content-security-policy.com/strict-dynamic/
// HOWEVER, at the time of writing (24 Jan 2025), Discord is INSANE and also uses unsafe-inline
// Once they stop using it, we also should
pushDirective("script-src", "'unsafe-inline'", "'unsafe-eval'");
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
pushDirective(directive, "blob:", "data:", "vencord:");
}
for (const [host, directives] of Object.entries(NativeSettings.store.customCspRules)) {
for (const directive of directives) {
pushDirective(directive, host);
}
}
for (const [host, directives] of Object.entries(CspPolicies)) {
for (const directive of directives) {
pushDirective(directive, host);
}
}
headers[header] = [stringifyPolicy(csp)];
}
};
export function initCsp() {
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders);
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet") {
const header = findHeader(responseHeaders, "content-type");
if (header)
responseHeaders[header] = ["text/css"];
}
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
}

125
src/main/csp/manager.ts Normal file
View file

@ -0,0 +1,125 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NativeSettings } from "@main/settings";
import { IpcEvents } from "@shared/IpcEvents";
import { dialog, ipcMain, IpcMainInvokeEvent } from "electron";
import { CspPolicies, ImageAndCssSrc } from ".";
export type CspRequestResult = "invalid" | "cancelled" | "unchecked" | "ok" | "conflict";
export function registerCspIpcHandlers() {
ipcMain.handle(IpcEvents.CSP_REMOVE_OVERRIDE, removeCspRule);
ipcMain.handle(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, addCspRule);
ipcMain.handle(IpcEvents.CSP_IS_DOMAIN_ALLOWED, isDomainAllowed);
}
function validate(url: string, directives: string[]) {
try {
const { host } = new URL(url);
if (/[;'"\\]/.test(host)) return false;
} catch {
return false;
}
if (directives.length === 0) return false;
if (directives.some(d => !ImageAndCssSrc.includes(d))) return false;
return true;
}
function getMessage(url: string, directives: string[], callerName: string) {
const domain = new URL(url).host;
const message = `${callerName} wants to allow connections to ${domain}`;
let detail =
`Unless you recognise and fully trust ${domain}, you should cancel this request!\n\n` +
`You will have to fully close and restart ${IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} for the changes to take effect.`;
if (directives.length === 1 && directives[0] === "connect-src") {
return { message, detail };
}
const contentTypes = directives
.filter(type => type !== "connect-src")
.map(type => {
switch (type) {
case "img-src":
return "Images";
case "style-src":
return "CSS & Themes";
case "font-src":
return "Fonts";
default:
throw new Error(`Illegal CSP directive: ${type}`);
}
})
.sort()
.join(", ");
detail = `The following types of content will be allowed to load from ${domain}:\n${contentTypes}\n\n${detail}`;
return { message, detail };
}
async function addCspRule(_: IpcMainInvokeEvent, url: string, directives: string[], callerName: string): Promise<CspRequestResult> {
if (!validate(url, directives)) {
return "invalid";
}
const domain = new URL(url).host;
if (domain in NativeSettings.store.customCspRules) {
return "conflict";
}
const { checkboxChecked, response } = await dialog.showMessageBox({
...getMessage(url, directives, callerName),
type: callerName ? "info" : "warning",
title: "Vencord Host Permissions",
buttons: ["Cancel", "Allow"],
defaultId: 0,
cancelId: 0,
checkboxLabel: `I fully trust ${domain} and understand the risks of allowing connections to it.`,
checkboxChecked: false,
});
if (response !== 1) {
return "cancelled";
}
if (!checkboxChecked) {
return "unchecked";
}
NativeSettings.store.customCspRules[domain] = directives;
return "ok";
}
function removeCspRule(_: IpcMainInvokeEvent, domain: string) {
if (domain in NativeSettings.store.customCspRules) {
delete NativeSettings.store.customCspRules[domain];
return true;
}
return false;
}
function isDomainAllowed(_: IpcMainInvokeEvent, url: string, directives: string[]) {
try {
const domain = new URL(url).host;
const ruleForDomain = CspPolicies[domain] ?? NativeSettings.store.customCspRules[domain];
if (!ruleForDomain) return false;
return directives.every(d => ruleForDomain.includes(d));
} catch (e) {
return false;
}
}

View file

@ -16,9 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { app, protocol, session } from "electron";
import { app, net, protocol } from "electron";
import { join } from "path";
import { pathToFileURL } from "url";
import { initCsp } from "./csp";
import { ensureSafePath } from "./ipcMain";
import { RendererSettings } from "./settings";
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
@ -26,21 +28,27 @@ import { installExt } from "./utils/extensions";
if (IS_VESKTOP || !IS_VANILLA) {
app.whenReady().then(() => {
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
protocol.handle("vencord", ({ url: unsafeUrl }) => {
let url = decodeURI(unsafeUrl).slice("vencord://".length).replace(/\?v=\d+$/, "");
if (url.endsWith("/")) url = url.slice(0, -1);
if (url.startsWith("/themes/")) {
const theme = url.slice("/themes/".length);
const safeUrl = ensureSafePath(THEMES_DIR, theme);
if (!safeUrl) {
cb({ statusCode: 403 });
return;
return new Response(null, {
status: 404
});
}
cb(safeUrl.replace(/\?v=\d+$/, ""));
return;
return net.fetch(pathToFileURL(safeUrl).toString());
}
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
switch (url) {
case "renderer.js.map":
case "vencordDesktopRenderer.js.map":
@ -48,10 +56,11 @@ if (IS_VESKTOP || !IS_VANILLA) {
case "vencordDesktopPreload.js.map":
case "patcher.js.map":
case "vencordDesktopMain.js.map":
cb(join(__dirname, url));
break;
return net.fetch(pathToFileURL(join(__dirname, url)).toString());
default:
cb({ statusCode: 403 });
return new Response(null, {
status: 404
});
}
});
@ -63,70 +72,7 @@ if (IS_VESKTOP || !IS_VANILLA) {
} catch { }
const findHeader = (headers: Record<string, string[]>, headerName: Lowercase<string>) => {
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
};
// Remove CSP
type PolicyResult = Record<string, string[]>;
const parsePolicy = (policy: string): PolicyResult => {
const result: PolicyResult = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
const patchCsp = (headers: Record<string, string[]>) => {
const header = findHeader(headers, "content-security-policy");
if (header) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] ??= [];
csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'");
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
};
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders);
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet") {
const header = findHeader(responseHeaders, "content-type");
if (header)
responseHeaders[header] = ["text/css"];
}
}
cb({ cancel: false, responseHeaders });
});
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
// impossible to load css from github raw despite our fix above
session.defaultSession.webRequest.onHeadersReceived = () => { };
initCsp();
});
}

View file

@ -28,14 +28,17 @@ import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
import { open, readdir, readFile } from "fs/promises";
import { join, normalize } from "path";
import { registerCspIpcHandlers } from "./csp/manager";
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants";
import { makeLinksOpenExternally } from "./utils/externalLinks";
mkdirSync(THEMES_DIR, { recursive: true });
registerCspIpcHandlers();
export function ensureSafePath(basePath: string, path: string) {
const normalizedBasePath = normalize(basePath);
const normalizedBasePath = normalize(basePath + "/");
const newPath = join(basePath, path);
const normalizedPath = normalize(newPath);
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
@ -89,7 +92,6 @@ ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
writeFileSync(QUICKCSS_PATH, css)
);
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
ipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes());
ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName));
ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
@ -97,6 +99,8 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}`
}));
ipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR));
ipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR));
export function initIpc(mainWindow: BrowserWindow) {
let quickCssWatcher: FSWatcher | undefined;

View file

@ -36,7 +36,6 @@ RendererSettings.addGlobalChangeListener(() => {
}
});
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain);
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => {
@ -49,16 +48,18 @@ export interface NativeSettings {
[setting: string]: any;
};
};
customCspRules: Record<string, string[]>;
}
const DefaultNativeSettings: NativeSettings = {
plugins: {}
plugins: {},
customCspRules: {}
};
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
mergeDefaults(nativeSettings, DefaultNativeSettings);
export const NativeSettings = new SettingsStore(nativeSettings);
export const NativeSettings = new SettingsStore(nativeSettings as NativeSettings);
NativeSettings.addGlobalChangeListener(() => {
try {

View file

@ -35,7 +35,10 @@ export function serializeErrors(func: (...args: any[]) => any) {
ok: false,
error: e instanceof Error ? {
// prototypes get lost, so turn error into plain object
...e
...e,
message: e.message,
name: e.name,
stack: e.stack
} : e
};
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { get } from "@main/utils/simpleGet";
import { fetchBuffer, fetchJson } from "@main/utils/http";
import { IpcEvents } from "@shared/IpcEvents";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { ipcMain } from "electron";
@ -31,8 +31,8 @@ import { serializeErrors, VENCORD_FILES } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
let PendingUpdates = [] as [string, string][];
async function githubGet(endpoint: string) {
return get(API_BASE + endpoint, {
async function githubGet<T = any>(endpoint: string) {
return fetchJson<T>(API_BASE + endpoint, {
headers: {
Accept: "application/vnd.github+json",
// "All API requests MUST include a valid User-Agent header.
@ -46,9 +46,8 @@ async function calculateGitChanges() {
const isOutdated = await fetchUpdates();
if (!isOutdated) return [];
const res = await githubGet(`/compare/${gitHash}...HEAD`);
const data = await githubGet(`/compare/${gitHash}...HEAD`);
const data = JSON.parse(res.toString("utf-8"));
return data.commits.map((c: any) => ({
// github api only sends the long sha
hash: c.sha.slice(0, 7),
@ -58,9 +57,8 @@ async function calculateGitChanges() {
}
async function fetchUpdates() {
const release = await githubGet("/releases/latest");
const data = await githubGet("/releases/latest");
const data = JSON.parse(release.toString());
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
if (hash === gitHash)
return false;
@ -70,16 +68,20 @@ async function fetchUpdates() {
PendingUpdates.push([name, browser_download_url]);
}
});
return true;
}
async function applyUpdates() {
await Promise.all(PendingUpdates.map(
async ([name, data]) => writeFile(
join(__dirname, name),
await get(data)
)
));
const fileContents = await Promise.all(PendingUpdates.map(async ([name, url]) => {
const contents = await fetchBuffer(url);
return [join(__dirname, name), contents] as const;
}));
await Promise.all(fileContents.map(async ([filename, contents]) =>
writeFile(filename, contents))
);
PendingUpdates = [];
return true;
}

View file

@ -24,7 +24,7 @@ import { join } from "path";
import { DATA_DIR } from "./constants";
import { crxToZip } from "./crxToZip";
import { get } from "./simpleGet";
import { fetchBuffer } from "./http";
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
@ -69,13 +69,14 @@ export async function installExt(id: string) {
} catch (err) {
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;
const buf = await get(url, {
const buf = await fetchBuffer(url, {
headers: {
"User-Agent": `Electron ${process.versions.electron} ~ Vencord (https://github.com/Vendicated/Vencord)`
}
});
await extract(crxToZip(buf), extDir).catch(console.error);
await extract(crxToZip(buf), extDir)
.catch(err => console.error(`Failed to extract extension ${id}`, err));
}
session.defaultSession.loadExtension(extDir);

70
src/main/utils/http.ts Normal file
View file

@ -0,0 +1,70 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { createWriteStream } from "original-fs";
import { Readable } from "stream";
import { finished } from "stream/promises";
type Url = string | URL;
export async function checkedFetch(url: Url, options?: RequestInit) {
try {
var res = await fetch(url, options);
} catch (err) {
if (err instanceof Error && err.cause) {
err = err.cause;
}
throw new Error(`${options?.method ?? "GET"} ${url} failed: ${err}`);
}
if (res.ok) {
return res;
}
let message = `${options?.method ?? "GET"} ${url}: ${res.status} ${res.statusText}`;
try {
const reason = await res.text();
message += `\n${reason}`;
} catch { }
throw new Error(message);
}
export async function fetchJson<T = any>(url: Url, options?: RequestInit) {
const res = await checkedFetch(url, options);
return res.json() as Promise<T>;
}
export async function fetchBuffer(url: Url, options?: RequestInit) {
const res = await checkedFetch(url, options);
const buf = await res.arrayBuffer();
return Buffer.from(buf);
}
export async function downloadToFile(url: Url, path: string, options?: RequestInit) {
const res = await checkedFetch(url, options);
if (!res.body) {
throw new Error(`Download ${url}: response body is empty`);
}
// @ts-expect-error weird type conflict
const body = Readable.fromWeb(res.body);
await finished(body.pipe(createWriteStream(path)));
}

View file

@ -1,37 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import https from "https";
export function get(url: string, options: https.RequestOptions = {}) {
return new Promise<Buffer>((resolve, reject) => {
https.get(url, options, res => {
const { statusCode, statusMessage, headers } = res;
if (statusCode! >= 400)
return void reject(`${statusCode}: ${statusMessage} - ${url}`);
if (statusCode! >= 300)
return void resolve(get(headers.location!, options));
const chunks = [] as Buffer[];
res.on("error", reject);
res.on("data", chunk => chunks.push(chunk));
res.once("end", () => resolve(Buffer.concat(chunks)));
});
});
}

View file

@ -33,7 +33,7 @@ import definePlugin from "@utils/types";
import { Forms, Toasts, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64";
const ContributorBadge: ProfileBadge = {
description: "Vencord Contributor",

View file

@ -27,7 +27,7 @@ export default definePlugin({
{
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
replacement: {
match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?children:\[(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
replace: (_, ReactButton, PotionButton, ButtonComponent, showReactButton, message) => "" +
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,${showReactButton}&&${PotionButton},`
}

View file

@ -75,11 +75,7 @@ export default definePlugin({
find: "async uploadFiles(",
replacement: [
{
match: /async uploadFiles\((\i),\i\){/,
replace: "$&$1.forEach($self.anonymise);"
},
{
match: /async uploadFilesSimple\((\i)\){/,
match: /async uploadFiles\((\i)\){/,
replace: "$&$1.forEach($self.anonymise);"
}
],

View file

@ -27,7 +27,7 @@ interface RemoteData {
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
const APPLE_MUSIC_BUNDLE_REGEX = /<script type="module" crossorigin src="([a-zA-Z0-9.\-/]+)"><\/script>/;
const APPLE_MUSIC_TOKEN_REGEX = canonicalizeMatch(/Promise.allSettled\(\i\)\}const \i="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)"/);
const APPLE_MUSIC_TOKEN_REGEX = canonicalizeMatch(/\b(\i)="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)"(?=.+?Bearer \$\{\1\})/);
let cachedToken: string | undefined = undefined;
@ -38,7 +38,7 @@ const getToken = async () => {
const bundleUrl = new URL(html.match(APPLE_MUSIC_BUNDLE_REGEX)![1], "https://music.apple.com/");
const bundle = await fetch(bundleUrl).then(r => r.text());
const token = bundle.match(APPLE_MUSIC_TOKEN_REGEX)![1];
const token = bundle.match(APPLE_MUSIC_TOKEN_REGEX)![2];
cachedToken = token;
return token;

View file

@ -1,49 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
source: {
description: "Source to replace ban GIF with (Video or Gif)",
type: OptionType.STRING,
default: "https://i.imgur.com/wp5q52C.mp4",
restartNeeded: true,
}
});
export default definePlugin({
name: "BANger",
description: "Replaces the GIF in the ban dialogue with a custom one.",
authors: [Devs.Xinto, Devs.Glitch],
settings,
patches: [
{
find: "#{intl::jeKpoq::raw}", // BAN_CONFIRM_TITLE
replacement: {
match: /src:\i\("?\d+"?\)/g,
replace: "src:$self.source"
}
}
],
get source() {
return settings.store.source;
}
});

View file

@ -179,7 +179,7 @@ export default definePlugin({
},
// If we are rendering the Better Folders sidebar, we filter out everything but the Guild List from the Sidebar children
{
match: /unreadMentionsFixedFooter\].+?\]/,
match: /unreadMentionsFixedFooter\].+?\}\)\]/,
replace: "$&.filter($self.makeGuildsBarSidebarFilter(!!arguments[0]?.isBetterFolders))"
}
]

View file

@ -12,7 +12,7 @@ import { Devs } from "@utils/constants";
import { getCurrentGuild, openImageModal } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { GuildStore, Menu, PermissionStore } from "@webpack/common";
import { GuildRoleStore, Menu, PermissionStore } from "@webpack/common";
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
@ -80,7 +80,7 @@ export default definePlugin({
const guild = getCurrentGuild();
if (!guild) return;
const role = GuildStore.getRole(guild.id, id);
const role = GuildRoleStore.getRole(guild.id, id);
if (!role) return;
if (role.colorString) {

View file

@ -138,7 +138,7 @@ export default definePlugin({
borderRadius: "50%",
backgroundColor: "var(--interactive-normal)",
color: "var(--background-secondary)",
color: "var(--background-base-lower)",
}}
>
<PlatformIcon width={14} height={14} />

View file

@ -43,7 +43,7 @@ export default definePlugin({
patches: [
{
find: ".embedWrapper,embed",
find: "}renderEmbeds(",
replacement: [{
match: /\.container/,
replace: "$&+(this.props.channel.nsfw? ' vc-nsfw-img': '')"

View file

@ -75,7 +75,8 @@ export default definePlugin({
patches: [{
find: "renderConnectionStatus(){",
replacement: {
match: /(renderConnectionStatus\(\){.+\.channel,children:)(.+?}\):\i)(?=}\))/,
// in renderConnectionStatus()
match: /(lineClamp:1,children:)(\i)(?=,|}\))/,
replace: "$1[$2,$self.renderTimer(this.props.channel.id)]"
}
}],

View file

@ -7,14 +7,13 @@
import { classNameFactory } from "@api/Styles";
import { ErrorCard } from "@components/ErrorCard";
import { Margins } from "@utils/margins";
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
import { findByCodeLazy, findStoreLazy } from "@webpack";
import { Button, ColorPicker, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
import { settings } from "..";
import { relativeLuminance } from "../utils/colorUtils";
import { createOrUpdateThemeColorVars } from "../utils/styleUtils";
const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"===');
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");

View file

@ -456,7 +456,7 @@ export default definePlugin({
<Forms.FormDivider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
<div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--background-mod-faint)" }}>
{activity[0] && <ActivityView
activity={activity[0]}
user={UserStore.getCurrentUser()}

View file

@ -150,5 +150,5 @@ export default definePlugin({
}
},
DecorSection: ErrorBoundary.wrap(DecorSection)
DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })
});

View file

@ -91,7 +91,7 @@ function parseNode(node: Node) {
function initWs(isManual = false) {
let wasConnected = isManual;
let hasErrored = false;
const ws = socket = new WebSocket(`ws://localhost:${PORT}`);
const ws = socket = new WebSocket(`ws://127.0.0.1:${PORT}`);
ws.addEventListener("open", () => {
wasConnected = true;

View file

@ -81,7 +81,7 @@ export default definePlugin({
},
// Change top right chat toolbar button from the help one to the dev one
{
find: ".CONTEXTLESS,isActivityPanelMode:",
find: '"M9 3v18"',
replacement: {
match: /hasBugReporterAccess:(\i)/,
replace: "_hasBugReporterAccess:$1=true"

View file

@ -225,7 +225,7 @@ function CloneModal({ data }: { data: Sticker | Emoji; }) {
aria-disabled={isCloning}
style={{
borderRadius: "50%",
backgroundColor: "var(--background-secondary)",
backgroundColor: "var(--background-base-lower)",
display: "inline-flex",
justifyContent: "center",
alignItems: "center",

View file

@ -127,7 +127,7 @@ const settings = definePluginSettings({
description: "Size of the emojis when sending",
type: OptionType.SLIDER,
default: 48,
markers: [32, 48, 64, 128, 160, 256, 512]
markers: [32, 48, 64, 96, 128, 160, 256, 512]
},
transformEmojis: {
description: "Whether to transform fake emojis into real ones",
@ -394,7 +394,7 @@ export default definePlugin({
},
// Separate patch for allowing using custom app icons
{
find: "?24:30,",
find: "getCurrentDesktopIcon(),",
replacement: {
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
replace: "true"
@ -671,24 +671,27 @@ export default definePlugin({
},
shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) {
try {
const contentItems = message.content.split(/\s/);
if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false;
switch (embed.type) {
case "image": {
const url = embed.url ?? embed.image?.url;
if (!url) return false;
if (
!settings.store.transformCompoundSentence
&& !contentItems.some(item => item === embed.url! || item.match(hyperLinkRegex)?.[1] === embed.url!)
&& !contentItems.some(item => item === url || item.match(hyperLinkRegex)?.[1] === url)
) return false;
if (settings.store.transformEmojis) {
if (fakeNitroEmojiRegex.test(embed.url!)) return true;
if (fakeNitroEmojiRegex.test(url)) return true;
}
if (settings.store.transformStickers) {
if (fakeNitroStickerRegex.test(embed.url!)) return true;
if (fakeNitroStickerRegex.test(url)) return true;
const gifMatch = embed.url!.match(fakeNitroGifStickerRegex);
const gifMatch = url.match(fakeNitroGifStickerRegex);
if (gifMatch) {
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
if (StickerStore.getStickerById(gifMatch[1])) return true;
@ -698,6 +701,9 @@ export default definePlugin({
break;
}
}
} catch (e) {
new Logger("FakeNitro").error("Error in shouldIgnoreEmbed:", e);
}
return false;
},

View file

@ -24,10 +24,9 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import { classes, copyWithToast } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
import { findComponentByCodeLazy } from "@webpack";
import { Button, ColorPicker, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
import { User } from "discord-types/general";
import { ReactElement } from "react";
import virtualMerge from "virtual-merge";
@ -109,10 +108,8 @@ interface ProfileModalProps {
isTryItOutFlow: boolean;
}
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>("isTryItOutFlow:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER");
const requireColorPicker = extractAndLoadChunksLazy(["#{intl::USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON}"], /createPromise:\(\)=>\i\.\i(\("?.+?"?\)).then\(\i\.bind\(\i,"?(.+?)"?\)\)/);
export default definePlugin({
name: "FakeProfileThemes",
@ -141,8 +138,6 @@ export default definePlugin({
const [color1, setColor1] = useState(existingColors[0]);
const [color2, setColor2] = useState(existingColors[1]);
const [, , loadingColorPickerChunk] = useAwaiter(requireColorPicker);
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
@ -162,7 +157,6 @@ export default definePlugin({
className={classes(Margins.top8, Margins.bottom8)}
/>
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
{!loadingColorPickerChunk && (
<Flex
direction={Flex.Direction.HORIZONTAL}
style={{ gap: "1rem" }}
@ -206,7 +200,6 @@ export default definePlugin({
Copy 3y3
</Button>
</Flex>
)}
<Forms.FormDivider
className={classes(Margins.top8, Margins.bottom8)}
/>

View file

@ -118,7 +118,7 @@ export default definePlugin({
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
this.instance = instance;
return (
<ErrorBoundary noop={true}>
<ErrorBoundary noop>
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
</ErrorBoundary>
);

View file

@ -20,10 +20,10 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
import { Constants, FluxDispatcher, GuildStore, RelationshipStore, RestAPI, SnowflakeUtils, UserStore } from "@webpack/common";
import { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
import { Settings } from "Vencord";
const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore");
const UserAffinitiesStore = findStoreLazy("UserAffinitiesV2Store");
export default definePlugin({
name: "ImplicitRelationships",
@ -117,39 +117,24 @@ export default definePlugin({
wrapSort(comparator: Function, row: any) {
return row.type === 5
? -(UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0)
? (UserAffinitiesStore.getUserAffinity(row.user.id)?.communicationRank ?? 0)
: comparator(row);
},
async refreshUserAffinities() {
try {
await RestAPI.get({ url: "/users/@me/affinities/users", retries: 3 }).then(({ body }) => {
FluxDispatcher.dispatch({
type: "LOAD_USER_AFFINITIES_SUCCESS",
affinities: body,
});
});
} catch (e) {
// Not a critical error if this fails for some reason
}
},
async fetchImplicitRelationships() {
// Implicit relationships are defined as users that you:
// 1. Have an affinity for
// 2. Do not have a relationship with
await this.refreshUserAffinities();
const userAffinities: Set<string> = UserAffinitiesStore.getUserAffinitiesUserIds();
const relationships = RelationshipStore.getRelationships();
const nonFriendAffinities = Array.from(userAffinities).filter(
id => !RelationshipStore.getRelationshipType(id)
);
nonFriendAffinities.forEach(id => {
relationships[id] = 5;
const userAffinities: Record<string, any>[] = UserAffinitiesStore.getUserAffinities();
const relationships = RelationshipStore.getMutableRelationships();
const nonFriendAffinities = userAffinities.filter(a => !RelationshipStore.getRelationshipType(a.otherUserId));
nonFriendAffinities.forEach(a => {
relationships[a.otherUserId] = 5;
});
RelationshipStore.emitChange();
const toRequest = nonFriendAffinities.filter(id => !UserStore.getUser(id));
const toRequest = nonFriendAffinities.filter(a => !UserStore.getUser(a.otherUserId));
const allGuildIds = Object.keys(GuildStore.getGuilds());
const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
let count = allGuildIds.length * Math.ceil(toRequest.length / 100);

View file

@ -67,9 +67,8 @@ export default definePlugin({
find: '="SYSTEM_TAG"',
replacement: {
// Override colorString with our custom color and disable gradients if applying the custom color.
match: /&&null!=\i\.secondaryColor,(?<=colorString:(\i).+?(\i)=.+?)/,
replace: (m, colorString, hasGradientColors) => `${m}` +
`vcIrcColorsDummy=[${colorString},${hasGradientColors}]=$self.getMessageColorsVariables(arguments[0],${hasGradientColors}),`
match: /(?<=colorString:\i,colorStrings:\i,colorRoleName:\i}=)(\i),/,
replace: "$self.wrapMessageColorProps($1, arguments[0]),"
}
},
{
@ -82,11 +81,26 @@ export default definePlugin({
}
],
getMessageColorsVariables(context: any, hasGradientColors: boolean) {
wrapMessageColorProps(colorProps: { colorString: string, colorStrings?: Record<"primaryColor" | "secondaryColor" | "tertiaryColor", string>; }, context: any) {
try {
const colorString = this.calculateNameColorForMessageContext(context);
const originalColorString = context?.author?.colorString;
if (colorString === colorProps.colorString) {
return colorProps;
}
return [colorString, hasGradientColors && colorString === originalColorString];
return {
...colorProps,
colorString,
colorStrings: colorProps.colorStrings && {
primaryColor: colorString,
secondaryColor: undefined,
tertiaryColor: undefined
}
};
} catch (e) {
console.error("Failed to calculate message color strings:", e);
return colorProps;
}
},
calculateNameColorForMessageContext(context: any) {
@ -108,6 +122,7 @@ export default definePlugin({
},
calculateNameColorForListContext(context: any) {
try {
const id = context?.user?.id;
const colorString = context?.colorString;
const color = calculateNameColorForUser(id);
@ -119,5 +134,8 @@ export default definePlugin({
return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString)
? color
: colorString;
} catch (e) {
console.error("Failed to calculate name color for list context:", e);
}
}
});

View file

@ -10,7 +10,7 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { GuildStore, SelectedGuildStore, useState } from "@webpack/common";
import { GuildRoleStore, SelectedGuildStore, useState } from "@webpack/common";
import { User } from "discord-types/general";
const settings = definePluginSettings({
@ -89,7 +89,7 @@ export default definePlugin({
// Discord uses Role Mentions for uncached users because .... idk
if (!roleId) return null;
const role = GuildStore.getRole(guildId, roleId);
const role = GuildRoleStore.getRole(guildId, roleId);
if (!role?.icon) return <DefaultRoleIcon />;
@ -99,7 +99,7 @@ export default definePlugin({
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
/>
);
}),
}, { noop: true }),
});
function getUsernameString(username: string) {

View file

@ -20,7 +20,8 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common";
import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore, WindowStore } from "@webpack/common";
import NoReplyMentionPlugin from "plugins/noReplyMention";
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
@ -28,6 +29,7 @@ const EditStore = findByPropsLazy("isEditing", "isEditingAny");
let isDeletePressed = false;
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
const focusChanged = () => !WindowStore.isFocused() && (isDeletePressed = false);
const settings = definePluginSettings({
enableDeleteOnClick: {
@ -62,11 +64,13 @@ export default definePlugin({
start() {
document.addEventListener("keydown", keydown);
document.addEventListener("keyup", keyup);
WindowStore.addChangeListener(focusChanged);
},
stop() {
document.removeEventListener("keydown", keydown);
document.removeEventListener("keyup", keyup);
WindowStore.removeChangeListener(focusChanged);
},
onMessageClick(msg: any, channel, event) {
@ -89,9 +93,8 @@ export default definePlugin({
if (msg.hasFlag(EPHEMERAL)) return;
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default;
const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention")
? NoReplyMention.shouldMention(msg, isShiftPress)
const shouldMention = Vencord.Plugins.isPluginEnabled(NoReplyMentionPlugin.name)
? NoReplyMentionPlugin.shouldMention(msg, isShiftPress)
: !isShiftPress;
FluxDispatcher.dispatch({

View file

@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js";
import { classes } from "@utils/misc";
import { Queue } from "@utils/Queue";
@ -295,7 +294,7 @@ function ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps):
<Embed
embed={{
rawDescription: "",
color: "var(--background-secondary)",
color: "var(--background-base-lower)",
author: {
name: <Text variant="text-xs/medium" tag="span">
<span>{channelLabel} - </span>
@ -373,7 +372,7 @@ export default definePlugin({
settings,
start() {
addMessageAccessory("messageLinkEmbed", props => {
addMessageAccessory("MessageLinkEmbeds", props => {
if (!messageLinkRegex.test(props.message.content))
return null;
@ -381,16 +380,14 @@ export default definePlugin({
messageLinkRegex.lastIndex = 0;
return (
<ErrorBoundary>
<MessageEmbedAccessory
message={props.message}
/>
</ErrorBoundary>
);
}, 4 /* just above rich embeds */);
},
stop() {
removeMessageAccessory("messageLinkEmbed");
removeMessageAccessory("MessageLinkEmbeds");
}
});

View file

@ -1,5 +1,5 @@
.messagelogger-deleted {
--text-normal: var(--status-danger, #f04747);
--text-default: var(--status-danger, #f04747);
--interactive-normal: var(--status-danger, #f04747);
--text-muted: var(--status-danger, #f04747);
--embed-title: var(--red-460, #be3535);

View file

@ -204,5 +204,5 @@ export default definePlugin({
/>
</>
);
})
}, { noop: true })
});

View file

@ -21,12 +21,9 @@ import { Devs } from "@utils/constants";
import { runtimeHashMessageKey } from "@utils/intlHash";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { i18n } from "@webpack/common";
import { i18n, RelationshipStore } from "@webpack/common";
import { Message } from "discord-types/general";
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
interface MessageDeleteProps {
// Internal intl message for BLOCKED_MESSAGE_COUNT
collapsedReason: () => any;

View file

@ -75,5 +75,5 @@ export default definePlugin({
}}> Pause Indefinitely.</a>}
</div>
);
})
}, { noop: true })
});

View file

@ -23,7 +23,7 @@ import { copyToClipboard } from "@utils/clipboard";
import { getIntlMessage, getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findByCodeLazy } from "@webpack";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildRoleStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
import { UnicodeEmoji } from "@webpack/types";
import type { Guild, Role, User } from "discord-types/general";
@ -85,7 +85,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
const [selectedItemIndex, selectItem] = useState(0);
const selectedItem = permissions[selectedItemIndex];
const roles = GuildStore.getRoles(guild.id);
const roles = GuildRoleStore.getRoles(guild.id);
return (
<ModalRoot
@ -238,7 +238,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
id={cl("view-as-role")}
label={getIntlMessage("VIEW_AS_ROLE")}
action={() => {
const role = GuildStore.getRole(guild.id, roleId);
const role = GuildRoleStore.getRole(guild.id, roleId);
if (!role) return;
onClose();

View file

@ -159,7 +159,7 @@ function UserPermissionsComponent({ guild, guildMember, closePopout }: { guild:
viewBox="0 96 960 960"
transform={permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
>
<path fill="var(--text-normal)" d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" />
<path fill="var(--text-default)" d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" />
</svg>
</div>
)}
@ -181,7 +181,7 @@ function UserPermissionsComponent({ guild, guildMember, closePopout }: { guild:
height="24"
viewBox="0 0 24 24"
>
<path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
<path fill="var(--text-default)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
</svg>
</div>
)}

View file

@ -37,7 +37,7 @@ export function PermissionAllowedIcon() {
viewBox="0 0 24 24"
>
<title>Allowed</title>
<path fill="var(--text-positive)" d="M8.99991 16.17L4.82991 12L3.40991 13.41L8.99991 19L20.9999 7.00003L19.5899 5.59003L8.99991 16.17ZZ" />
<path fill="var(--status-positive)" d="M8.99991 16.17L4.82991 12L3.40991 13.41L8.99991 19L20.9999 7.00003L19.5899 5.59003L8.99991 16.17ZZ" />
</svg>
);
}
@ -51,7 +51,7 @@ export function PermissionDefaultIcon() {
>
<g>
<title>Not overwritten</title>
<polygon fill="var(--text-normal)" points="12 2.32 10.513 2 4 13.68 5.487 14" />
<polygon fill="var(--text-default)" points="12 2.32 10.513 2 4 13.68 5.487 14" />
</g>
</svg>
);

View file

@ -26,7 +26,7 @@ import { Devs } from "@utils/constants";
import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, match, Menu, PermissionsBits, Popout, TooltipContainer, useRef, UserStore } from "@webpack/common";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildRoleStore, GuildStore, match, Menu, PermissionsBits, Popout, TooltipContainer, useRef, UserStore } from "@webpack/common";
import type { Guild, GuildMember } from "discord-types/general";
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions";
@ -107,7 +107,7 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
};
})
.otherwise(() => {
const permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
const permissions = Object.values(GuildRoleStore.getRoles(guild.id)).map(role => ({
type: PermissionType.Role,
...role
}));

View file

@ -140,10 +140,9 @@
/* copy pasted from discord cause impossible to webpack find */
.vc-permviewer-role-button {
border-radius: var(--radius-xs);
background: var(--bg-mod-faint);
border-radius: var(--radius-sm);
color: var(--interactive-normal);
border: 1px solid var(--border-faint);
border: 1px solid var(--user-profile-border);
/* stylelint-disable-next-line value-no-vendor-prefix */
width: -moz-fit-content;
width: fit-content;
@ -151,9 +150,8 @@
padding: 4px
}
.custom-profile-theme .vc-permviewer-role-button {
background: rgb(var(--bg-overlay-color)/var(--bg-overlay-opacity-6));
border-color: var(--profile-body-border-color)
.vc-permviewer-role-button:hover {
background-color: var(--user-profile-background-hover);
}
.vc-permviewer-granted-by-container {

View file

@ -18,7 +18,7 @@
import { classNameFactory } from "@api/Styles";
import { findByPropsLazy } from "@webpack";
import { GuildStore } from "@webpack/common";
import { GuildRoleStore } from "@webpack/common";
import { Guild, GuildMember, Role } from "discord-types/general";
import { PermissionsSortOrder, settings } from ".";
@ -29,7 +29,7 @@ export const { getGuildPermissionSpecMap } = findByPropsLazy("getGuildPermission
export const cl = classNameFactory("vc-permviewer-");
export function getSortedRoles({ id }: Guild, member: GuildMember) {
const roles = GuildStore.getRoles(id);
const roles = GuildRoleStore.getRoles(id);
return [...member.roles, id]
.map(id => roles[id])
@ -48,7 +48,7 @@ export function sortUserRoles(roles: Role[]) {
}
export function sortPermissionOverwrites<T extends { id: string; type: number; }>(overwrites: T[], guildId: string) {
const roles = GuildStore.getRoles(guildId);
const roles = GuildRoleStore.getRoles(guildId);
return overwrites.sort((a, b) => {
if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0;

View file

@ -7,18 +7,11 @@
import { classNameFactory } from "@api/Styles";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
import { Button, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common";
import { Button, ColorPicker, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common";
import { DEFAULT_COLOR, SWATCHES } from "../constants";
import { categoryLen, createCategory, getCategory } from "../data";
interface ColorPickerProps {
color: number | null;
showEyeDropper?: boolean;
suggestedColors?: string[];
onChange(value: number | null): void;
}
interface ColorPickerWithSwatchesProps {
defaultColor: number;
colors: number[];
@ -29,7 +22,6 @@ interface ColorPickerWithSwatchesProps {
renderCustomButton?: () => React.ReactNode;
}
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
const ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>('id:"color-picker"');
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/);

View file

@ -16,20 +16,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings, Settings } from "@api/Settings";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, ComponentDispatch, FluxDispatcher as Dispatcher, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
import { ChannelStore, ComponentDispatch, FluxDispatcher as Dispatcher, MessageActions, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
import { Message } from "discord-types/general";
const Kangaroo = findByPropsLazy("jumpToMessage");
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
import NoBlockedMessagesPlugin from "plugins/noBlockedMessages";
import NoReplyMentionPlugin from "plugins/noReplyMention";
const isMac = navigator.platform.includes("Mac"); // bruh
let replyIdx = -1;
let editIdx = -1;
let currentlyReplyingId: string | null = null;
let currentlyEditingId: string | null = null;
const enum MentionOptions {
DISABLED,
@ -69,36 +66,29 @@ export default definePlugin({
flux: {
DELETE_PENDING_REPLY() {
replyIdx = -1;
currentlyReplyingId = null;
},
MESSAGE_END_EDIT() {
editIdx = -1;
currentlyEditingId = null;
},
CHANNEL_SELECT() {
currentlyReplyingId = null;
currentlyEditingId = null;
},
MESSAGE_START_EDIT: onStartEdit,
CREATE_PENDING_REPLY: onCreatePendingReply
}
});
function calculateIdx(messages: Message[], id: string) {
const idx = messages.findIndex(m => m.id === id);
return idx === -1
? idx
: messages.length - idx - 1;
}
function onStartEdit({ channelId, messageId, _isQuickEdit }: any) {
function onStartEdit({ messageId, _isQuickEdit }: any) {
if (_isQuickEdit) return;
const meId = UserStore.getCurrentUser().id;
const messages = MessageStore.getMessages(channelId)._array.filter(m => m.author.id === meId);
editIdx = calculateIdx(messages, messageId);
currentlyEditingId = messageId;
}
function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) {
if (_isQuickReply) return;
replyIdx = calculateIdx(MessageStore.getMessages(message.channel_id)._array, message.id);
currentlyReplyingId = message.id;
}
const isCtrl = (e: KeyboardEvent) => isMac ? e.metaKey : e.ctrlKey;
@ -123,10 +113,10 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
const vh = Math.max(document.documentElement.clientHeight, window.innerHeight);
const rect = element.getBoundingClientRect();
const isOffscreen = rect.bottom < 200 || rect.top - vh >= -200;
const isOffscreen = rect.bottom < 150 || rect.top - vh >= -150;
if (isOffscreen) {
Kangaroo.jumpToMessage({
MessageActions.jumpToMessage({
channelId,
messageId,
flash: false,
@ -137,43 +127,48 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
function getNextMessage(isUp: boolean, isReply: boolean) {
let messages: Array<Message & { deleted?: boolean; }> = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
if (!isReply) { // we are editing so only include own
const meId = UserStore.getCurrentUser().id;
messages = messages.filter(m => m.author.id === meId);
}
const hasNoBlockedMessages = Vencord.Plugins.isPluginEnabled(NoBlockedMessagesPlugin.name);
if (Vencord.Plugins.isPluginEnabled("NoBlockedMessages")) {
messages = messages.filter(m => !RelationshipStore.isBlocked(m.author.id));
}
messages = messages.filter(m => {
if (m.deleted) return false;
if (!isReply && m.author.id !== meId) return false; // editing only own messages
if (hasNoBlockedMessages && NoBlockedMessagesPlugin.shouldIgnoreMessage(m)) return false;
const mutate = (i: number) => isUp
? Math.min(messages.length - 1, i + 1)
: Math.max(-1, i - 1);
return true;
});
const findNextNonDeleted = (i: number) => {
do {
i = mutate(i);
} while (i !== -1 && messages[messages.length - i - 1]?.deleted === true);
return i;
const findNextNonDeleted = (id: string | null) => {
if (id === null) return messages[messages.length - 1];
const idx = messages.findIndex(m => m.id === id);
if (idx === -1) return messages[messages.length - 1];
const i = isUp ? idx - 1 : idx + 1;
return messages[i] ?? null;
};
let i: number;
if (isReply)
replyIdx = i = findNextNonDeleted(replyIdx);
else
editIdx = i = findNextNonDeleted(editIdx);
return i === - 1 ? undefined : messages[messages.length - i - 1];
if (isReply) {
const msg = findNextNonDeleted(currentlyReplyingId);
currentlyReplyingId = msg?.id ?? null;
return msg;
} else {
const msg = findNextNonDeleted(currentlyEditingId);
currentlyEditingId = msg?.id ?? null;
return msg;
}
}
function shouldMention(message) {
const { enabled, userList, shouldPingListed } = Settings.plugins.NoReplyMention;
const shouldPing = !enabled || (shouldPingListed === userList.includes(message.author.id));
function shouldMention(message: Message) {
switch (settings.store.shouldMention) {
case MentionOptions.NO_REPLY_MENTION_PLUGIN: return shouldPing;
case MentionOptions.DISABLED: return false;
default: return true;
case MentionOptions.NO_REPLY_MENTION_PLUGIN:
if (!Vencord.Plugins.isPluginEnabled(NoReplyMentionPlugin.name)) return true;
return NoReplyMentionPlugin.shouldMention(message, false);
case MentionOptions.DISABLED:
return false;
default:
return true;
}
}
@ -181,13 +176,16 @@ function shouldMention(message) {
function nextReply(isUp: boolean) {
const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
const message = getNextMessage(isUp, true);
if (!message)
if (!message) {
return void Dispatcher.dispatch({
type: "DELETE_PENDING_REPLY",
channelId: SelectedChannelStore.getChannelId(),
});
}
const channel = ChannelStore.getChannel(message.channel_id);
const meId = UserStore.getCurrentUser().id;
@ -199,6 +197,7 @@ function nextReply(isUp: boolean) {
showMentionToggle: !channel.isPrivate() && message.author.id !== meId,
_isQuickReply: true
});
ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS");
jumpIfOffScreen(channel.id, message.id);
}
@ -209,11 +208,13 @@ function nextEdit(isUp: boolean) {
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
const message = getNextMessage(isUp, false);
if (!message)
if (!message) {
return Dispatcher.dispatch({
type: "MESSAGE_END_EDIT",
channelId: SelectedChannelStore.getChannelId()
});
}
Dispatcher.dispatch({
type: "MESSAGE_START_EDIT",
channelId: message.channel_id,
@ -221,5 +222,6 @@ function nextEdit(isUp: boolean) {
content: message.content,
_isQuickEdit: true
});
jumpIfOffScreen(message.channel_id, message.id);
}

View file

@ -172,7 +172,7 @@ export async function syncFriends() {
friends.friends = [];
friends.requests = [];
const relationShips = RelationshipStore.getRelationships();
const relationShips = RelationshipStore.getMutableRelationships();
for (const id in relationShips) {
switch (relationShips[id]) {
case RelationshipType.FRIEND:

View file

@ -69,7 +69,7 @@ function makeSearchItem(src: string) {
aria-hidden="true"
height={16}
width={16}
src={`https://www.google.com/s2/favicons?domain=${Engines[engine]}&sz=64`}
src={`https://icons.duckduckgo.com/ip3/${new URL(Engines[engine]).hostname}.ico`}
/>
{engine}
</Flex>

View file

@ -53,14 +53,12 @@ function makeSearchItem(src: string) {
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
<img
style={{
borderRadius: i >= 3 // Do not round Google, Yandex & SauceNAO
? "50%"
: void 0
borderRadius: "50%",
}}
aria-hidden="true"
height={16}
width={16}
src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")}
src={`https://icons.duckduckgo.com/ip3/${new URL(Engines[engine]).host}.ico`}
/>
{engine}
</Flex>

View file

@ -72,7 +72,7 @@
overflow-y: hidden;
margin-top: 1px;
margin-bottom: 8px;
color: var(--text-normal);
color: var(--text-default);
font-size: 15px;
}

View file

@ -23,7 +23,7 @@ import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common";
import { ChannelStore, GuildMemberStore, GuildRoleStore, GuildStore } from "@webpack/common";
const useMessageAuthor = findByCodeLazy('"Result cannot be null because the message is not null"');
@ -84,8 +84,8 @@ export default definePlugin({
find: ".USER_MENTION)",
replacement: [
{
match: /(?<=onContextMenu:\i,color:)\i(?<=\.getNickname\((\i),\i,(\i).+?)/,
replace: "$self.getColorInt($2?.id,$1)",
match: /(?<=user:(\i),guildId:([^,]+?),.{0,100}?children:\i=>\i)\((\i)\)/,
replace: "({...$3,color:$self.getColorInt($1?.id,$2)})",
}
],
predicate: () => settings.store.chatMentions
@ -197,7 +197,7 @@ export default definePlugin({
const value = `color-mix(in oklab, ${author.colorString} ${messageSaturation}%, var({DEFAULT}))`;
return {
color: value.replace("{DEFAULT}", "--text-normal"),
color: value.replace("{DEFAULT}", "--text-default"),
"--header-primary": value.replace("{DEFAULT}", "--header-primary"),
"--text-muted": value.replace("{DEFAULT}", "--text-muted")
};
@ -210,7 +210,7 @@ export default definePlugin({
},
RoleGroupColor: ErrorBoundary.wrap(({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) => {
const role = GuildStore.getRole(guildId, id);
const role = GuildRoleStore.getRole(guildId, id);
return (
<span style={{

View file

@ -1,6 +1,6 @@
.vc-st-date-picker {
background-color: var(--input-background);
color: var(--text-normal);
color: var(--text-default);
width: 95%;
padding: 8px 8px 8px 12px;
margin: 1em 0;

View file

@ -12,7 +12,7 @@ import { classes } from "@utils/misc";
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildRoleStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
import { Guild, User } from "discord-types/general";
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
@ -198,9 +198,9 @@ function ServerInfoTab({ guild }: GuildProps) {
"Vanity Link": guild.vanityURLCode ? (<a>{`discord.gg/${guild.vanityURLCode}`}</a>) : "-", // Making the anchor href valid would cause Discord to reload
"Preferred Locale": guild.preferredLocale || "-",
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
"Server Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category
"Roles": Object.keys(GuildStore.getRoles(guild.id)).length - 1, // - @everyone
"Roles": Object.keys(GuildRoleStore.getRoles(guild.id)).length - 1, // - @everyone
};
return (
@ -220,12 +220,12 @@ function FriendsTab({ guild, setCount }: RelationshipProps) {
}
function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
const blockedIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isBlocked(id));
const blockedIds = RelationshipStore.getBlockedIDs();
return UserList("blocked", guild, blockedIds, setCount);
}
function IgnoredUserTab({ guild, setCount }: RelationshipProps) {
const ignoredIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isIgnored(id));
const ignoredIds = RelationshipStore.getIgnoredIDs();
return UserList("ignored", guild, ignoredIds, setCount);
}

View file

@ -68,7 +68,7 @@
}
.vc-gp-server-info-pair {
color: var(--text-normal);
color: var(--text-default);
}
.vc-gp-server-info-pair [class^="timestamp"] {

View file

@ -78,12 +78,12 @@ export const Highlighter = ({
});
const themeBase: ThemeBase = {
plainColor: currentTheme?.fg || "var(--text-normal)",
plainColor: currentTheme?.fg || "var(--text-default)",
accentBgColor:
currentTheme?.colors?.["statusBar.background"] || (useHljs ? "#7289da" : "#007BC8"),
accentFgColor: currentTheme?.colors?.["statusBar.foreground"] || "#FFF",
backgroundColor:
currentTheme?.colors?.["editor.background"] || "var(--background-secondary)",
currentTheme?.colors?.["editor.background"] || "var(--background-base-lower)",
};
let langName;

View file

@ -1,6 +1,6 @@
.vc-shiki-container {
border: 4px;
background-color: var(--background-secondary);
background-color: var(--background-base-lower);
}
.vc-shiki-root {

View file

@ -22,12 +22,12 @@
}
.vc-shc-heading-nsfw-icon {
color: var(--text-normal);
color: var(--text-default);
}
.vc-shc-topic-container {
color: var(--text-normal);
background: var(--bg-overlay-3, var(--background-secondary));
color: var(--text-default);
background: var(--bg-overlay-5, var(--background-base-lower));
border-radius: 5px;
padding: 10px;
max-width: 70vw;
@ -37,7 +37,7 @@
display: flex;
flex-direction: row;
align-items: center;
background: var(--bg-overlay-3, var(--background-secondary));
background: var(--bg-overlay-5, var(--background-base-lower));
border-radius: 8px;
padding: 0.75em;
margin-left: 0.75em;
@ -46,7 +46,7 @@
.vc-shc-tags-container {
display: flex;
flex-direction: column;
background: var(--bg-overlay-3, var(--background-secondary));
background: var(--bg-overlay-5, var(--background-base-lower));
border-radius: 5px;
padding: 0.75em;
gap: 0.75em;
@ -65,7 +65,7 @@
display: flex;
flex-direction: column;
align-items: center;
background: var(--bg-overlay-3, var(--background-secondary));
background: var(--bg-overlay-5, var(--background-base-lower));
border-radius: 5px;
padding: 0.75em;
max-width: 70vw;
@ -83,7 +83,7 @@
cursor: pointer;
display: flex;
align-items: center;
color: var(--text-normal);
color: var(--text-default);
}
.vc-shc-allowed-users-and-roles-container-permdetails-btn {
@ -91,7 +91,7 @@
cursor: pointer;
display: flex;
align-items: center;
color: var(--text-normal);
color: var(--text-default);
}
.vc-shc-allowed-users-and-roles-container > [class^="members"] {

View file

@ -48,10 +48,9 @@ export default definePlugin({
authors: [Devs.Rini, Devs.TheKodeToad],
patches: [
{
find: '"BaseUsername"',
find: '="SYSTEM_TAG"',
replacement: {
/* TODO: remove \i+\i once change makes it to stable */
match: /(?<=onContextMenu:\i,children:)(?:\i\+\i|\i)/,
match: /(?<=onContextMenu:\i,children:)\i/,
replace: "$self.renderUsername(arguments[0])"
}
},

View file

@ -86,5 +86,5 @@ export default definePlugin({
</TooltipContainer>
)}
</div>;
})
}, { noop: true })
});

View file

@ -8,11 +8,11 @@
}
.theme-light #vc-spotify-player {
background: var(--bg-overlay-3, var(--background-secondary-alt));
background: var(--bg-overlay-3, var(--background-base-lower-alt));
}
.theme-dark #vc-spotify-player {
background: var(--bg-overlay-1, var(--background-secondary-alt));
background: var(--bg-overlay-1, var(--background-base-lower-alt));
}
.vc-spotify-button {
@ -143,7 +143,7 @@
#vc-spotify-progress-bar {
position: relative;
color: var(--text-normal);
color: var(--text-default);
width: 100%;
margin: 0.5em 0;
margin-bottom: 5px;
@ -195,5 +195,5 @@
.vc-spotify-fallback {
padding: 0.5em;
color: var(--text-normal);
color: var(--text-default);
}

View file

@ -2,7 +2,7 @@
.visual-refresh {
#vc-spotify-player {
padding: 12px;
background: var(--bg-overlay-floating, var(--background-base-low, var(--background-secondary-alt)));
background: var(--bg-overlay-floating, var(--background-base-low, var(--background-base-lower-alt)));
margin: 0;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
@ -22,7 +22,7 @@
#vc-spotify-progress-bar {
position: relative;
color: var(--text-normal);
color: var(--text-default);
width: 100%;
}

View file

@ -57,7 +57,7 @@ export function TranslationAccessory({ message }: { message: Message; }) {
<span className={cl("accessory")}>
<TranslateIcon width={16} height={16} className={cl("accessory-icon")} />
{Parser.parse(translation.text)}
{" "}
<br />
(translated from {translation.sourceLanguage} - <Dismiss onDismiss={() => setTranslation(undefined)} />)
</span>
);

View file

@ -15,6 +15,8 @@
margin-top: 0.5em;
font-style: italic;
font-weight: 400;
line-height: 1.2rem;
white-space: break-spaces;
}
.vc-trans-accessory-icon {

View file

@ -69,26 +69,29 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s
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)
);
const [a, b, c] = typingUsersArray;
let tooltipText: string;
switch (typingUsersArray.length) {
case 0: break;
case 1: {
tooltipText = getIntlMessage("ONE_USER_TYPING", { a: getDisplayName(guildId, typingUsersArray[0]) });
tooltipText = getIntlMessage("ONE_USER_TYPING", { a: getDisplayName(guildId, a) });
break;
}
case 2: {
tooltipText = getIntlMessage("TWO_USERS_TYPING", { a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) });
tooltipText = getIntlMessage("TWO_USERS_TYPING", { a: getDisplayName(guildId, a), b: getDisplayName(guildId, b) });
break;
}
case 3: {
tooltipText = getIntlMessage("THREE_USERS_TYPING", { a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) });
tooltipText = getIntlMessage("THREE_USERS_TYPING", { a: getDisplayName(guildId, a), b: getDisplayName(guildId, b), c: getDisplayName(guildId, c) });
break;
}
default: {
tooltipText = Settings.plugins.TypingTweaks.enabled
? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), count: typingUsersArray.length - 2 })
? buildSeveralUsers({ a: UserStore.getUser(a), b: UserStore.getUser(b), count: typingUsersArray.length - 2, guildId })
: getIntlMessage("SEVERAL_USERS_TYPING");
break;
}
@ -178,7 +181,7 @@ export default definePlugin({
// Theads
{
// This is the thread "spine" that shows in the left
find: "M11 9H4C2.89543 9 2 8.10457 2 7V1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1V7C0 9.20914 1.79086 11 4 11H11C11.5523 11 12 10.5523 12 10C12 9.44771 11.5523 9 11 9Z",
find: "M0 15H2c0 1.6569",
replacement: {
match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/,
replace: "$&,$self.TypingIndicator($1.id,$1.getGuildId())"

View file

@ -45,14 +45,17 @@ const settings = definePluginSettings({
}
});
export function buildSeveralUsers({ a, b, count }: { a: string, b: string, count: number; }) {
return [
<strong key="0">{a}</strong>,
", ",
<strong key="1">{b}</strong>,
`, and ${count} others are typing...`
];
}
export const buildSeveralUsers = ErrorBoundary.wrap(({ a, b, count, guildId }: { a: User, b: User, count: number; guildId: string; }) => {
return (
<>
<TypingUser user={a} guildId={guildId} />
{", "}
<TypingUser user={b} guildId={guildId} />
{", "}
and {count} others are typing...
</>
);
}, { noop: true });
interface Props {
user: User;
@ -96,21 +99,23 @@ export default definePlugin({
patches: [
{
find: "#{intl::THREE_USERS_TYPING}",
group: true,
replacement: [
{
// Style the indicator and add function call to modify the children before rendering
match: /(?<=children:\[(\i)\.length>0.{0,200}?"aria-atomic":!0,children:)\i(?<=guildId:(\i).+?)/,
replace: "$self.renderTypingUsers({ users: $1, guildId: $2, children: $& })"
match: /(?<=children:\[(\i)\.length>0.{0,300}?"aria-atomic":!0,children:)\i/,
replace: "$self.renderTypingUsers({ users: $1, guildId: arguments[0]?.channel?.guild_id, children: $& })"
},
{
// Changes the indicator to keep the user object when creating the list of typing users
match: /\.map\((\i)=>\i\.\i\.getName\(\i,\i\.id,\1\)\)/,
match: /\.map\((\i)=>\i\.\i\.getName\(\i(?:\.guild_id)?,\i\.id,\1\)\)/,
replace: ""
},
{
// Adds the alternative formatting for several users typing
match: /(,{a:(\i),b:(\i),c:\i}\):\i\.length>3&&\(\i=)\i\.\i\.string\(\i\.\i#{intl::SEVERAL_USERS_TYPING}\)(?<=(\i)\.length.+?)/,
replace: (_, rest, a, b, users) => `${rest}$self.buildSeveralUsers({ a: ${a}, b: ${b}, count: ${users}.length - 2 })`,
replace: (_, rest, a, b, users) =>
`${rest}$self.buildSeveralUsers({ a: ${a}, b: ${b}, count: ${users}.length - 2, guildId: arguments[0]?.channel?.guild_id })`,
predicate: () => settings.store.alternativeFormatting
}
]

View file

@ -125,7 +125,7 @@ function VencordPopoutButton() {
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
children.splice(
children.length - 1, 0,
<ErrorBoundary noop={true}>
<ErrorBoundary noop>
<VencordPopoutButton />
</ErrorBoundary>
);

View file

@ -71,9 +71,14 @@ const openAvatar = (url: string) => openImage(url, 512, 512);
const openBanner = (url: string) => openImage(url, 1024);
function openImage(url: string, width: number, height?: number) {
const format = url.startsWith("/") ? "png" : settings.store.format;
const u = new URL(url, window.location.href);
const format = url.startsWith("/")
? "png"
: u.searchParams.get("animated") === "true"
? "gif"
: settings.store.format;
u.searchParams.set("size", settings.store.imgSize);
u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`);
url = u.toString();

View file

@ -27,7 +27,7 @@ import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, GuildStore, Menu, Text } from "@webpack/common";
import { Button, ChannelStore, Forms, GuildRoleStore, Menu, Text } from "@webpack/common";
import { Message } from "discord-types/general";
@ -148,7 +148,7 @@ const devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id:
const guild = getCurrentGuild();
if (!guild) return;
const role = GuildStore.getRole(guild.id, id);
const role = GuildRoleStore.getRole(guild.id, id);
if (!role) return;
children.push(

View file

@ -10,9 +10,9 @@
}
.vc-vmsg-preview {
color: var(--text-normal);
color: var(--text-default);
border-radius: 24px;
background-color: var(--background-secondary);
background-color: var(--background-base-lower);
position: relative;
display: flex;
align-items: center;

View file

@ -25,8 +25,8 @@ export default definePlugin({
replace: ";b=AS:800000;level-asymmetry-allowed=1"
},
{
match: "useinbandfec=1",
replace: "useinbandfec=1;stereo=1;sprop-stereo=1"
match: /;usedtx=".concat\((\i)\?"0":"1"\)/,
replace: '$&.concat($1?";stereo=1;sprop-stereo=1":"")'
}
]
}

View file

@ -10,7 +10,7 @@ import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
import { findByCodeLazy, findLazy } from "@webpack";
import { Button, ChannelStore, GuildStore, UserStore } from "@webpack/common";
import { Button, ChannelStore, GuildRoleStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
const ChannelTypes = findLazy(m => m.ANNOUNCEMENT_THREAD === 10);
@ -267,7 +267,7 @@ export default definePlugin({
// color role mentions (unity styling btw lol)
if (message.mention_roles.length > 0) {
for (const roleId of message.mention_roles) {
const role = GuildStore.getRole(channel.guild_id, roleId);
const role = GuildRoleStore.getRole(channel.guild_id, roleId);
if (!role) continue;
const roleColor = role.colorString ?? `#${pingColor}`;
finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`);

View file

@ -17,29 +17,38 @@
*/
export const enum IpcEvents {
QUICK_CSS_UPDATE = "VencordQuickCssUpdate",
THEME_UPDATE = "VencordThemeUpdate",
OPEN_QUICKCSS = "VencordOpenQuickCss",
GET_QUICK_CSS = "VencordGetQuickCss",
SET_QUICK_CSS = "VencordSetQuickCss",
UPLOAD_THEME = "VencordUploadTheme",
DELETE_THEME = "VencordDeleteTheme",
GET_THEMES_DIR = "VencordGetThemesDir",
QUICK_CSS_UPDATE = "VencordQuickCssUpdate",
GET_SETTINGS = "VencordGetSettings",
SET_SETTINGS = "VencordSetSettings",
GET_THEMES_LIST = "VencordGetThemesList",
GET_THEME_DATA = "VencordGetThemeData",
GET_THEME_SYSTEM_VALUES = "VencordGetThemeSystemValues",
GET_SETTINGS_DIR = "VencordGetSettingsDir",
GET_SETTINGS = "VencordGetSettings",
SET_SETTINGS = "VencordSetSettings",
UPLOAD_THEME = "VencordUploadTheme",
DELETE_THEME = "VencordDeleteTheme",
THEME_UPDATE = "VencordThemeUpdate",
OPEN_EXTERNAL = "VencordOpenExternal",
OPEN_QUICKCSS = "VencordOpenQuickCss",
OPEN_THEMES_FOLDER = "VencordOpenThemesFolder",
OPEN_SETTINGS_FOLDER = "VencordOpenSettingsFolder",
GET_UPDATES = "VencordGetUpdates",
GET_REPO = "VencordGetRepo",
UPDATE = "VencordUpdate",
BUILD = "VencordBuild",
OPEN_MONACO_EDITOR = "VencordOpenMonacoEditor",
GET_PLUGIN_IPC_METHOD_MAP = "VencordGetPluginIpcMethodMap",
OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect",
VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording",
CSP_IS_DOMAIN_ALLOWED = "VencordCspIsDomainAllowed",
CSP_REMOVE_OVERRIDE = "VencordCspRemoveOverride",
CSP_REQUEST_ADD_OVERRIDE = "VencordCspRequestAddOverride",
}

View file

@ -160,7 +160,7 @@ export class SettingsStore<T extends object> {
// So, we need to extract the top-level setting path (plugins.pluginName.settingName),
// to be able to notify globalListeners and top-level setting name listeners (let { settingName } = settings.use(["settingName"]),
// with the new value
if (paths.length > 2 && paths[0] === "plugins") {
if (paths.length > 3 && paths[0] === "plugins") {
const settingPath = paths.slice(0, 3);
const settingPathStr = settingPath.join(".");
const settingValue = settingPath.reduce((acc, curr) => acc[curr], root);

View file

@ -19,15 +19,40 @@
import * as DataStore from "@api/DataStore";
import { showNotification } from "@api/Notifications";
import { Settings } from "@api/Settings";
import { OAuth2AuthorizeModal, UserStore } from "@webpack/common";
import { Alerts, 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 cloudUrlOrigin = () => getCloudUrl().origin;
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 (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");
@ -37,7 +62,7 @@ const getUserId = () => {
export async function getAuthorization() {
const secrets = await DataStore.get<Record<string, string>>("Vencord_cloudSecret") ?? {};
const origin = cloudUrlOrigin();
const origin = getCloudUrlOrigin();
// we need to migrate from the old format here
if (secrets[origin]) {
@ -59,7 +84,7 @@ export async function getAuthorization() {
async function setAuthorization(secret: string) {
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
secrets ??= {};
secrets[`${cloudUrlOrigin()}:${getUserId()}`] = secret;
secrets[`${getCloudUrlOrigin()}:${getUserId()}`] = secret;
return secrets;
});
}
@ -67,7 +92,7 @@ async function setAuthorization(secret: string) {
export async function deauthorizeCloud() {
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
secrets ??= {};
delete secrets[`${cloudUrlOrigin()}:${getUserId()}`];
delete secrets[`${getCloudUrlOrigin()}:${getUserId()}`];
return secrets;
});
}
@ -78,6 +103,8 @@ 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

@ -40,7 +40,7 @@ export interface Dev {
*/
export const Devs = /* #__PURE__*/ Object.freeze({
Ven: {
name: "Vee",
name: "V",
id: 343383572805058560n
},
Arjix: {
@ -194,7 +194,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
},
axyie: {
name: "'ax",
id: 273562710745284628n,
id: 929877747151548487n,
},
pointy: {
name: "pointy",
@ -587,7 +587,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
},
samsam: {
name: "samsam",
id: 836452332387565589n,
id: 400482410279469056n,
},
Cootshk: {
name: "Cootshk",

Some files were not shown because too many files have changed in this diff Show more