Compare commits
47 commits
9051269657
...
826e774af1
| Author | SHA1 | Date | |
|---|---|---|---|
| 826e774af1 | |||
|
|
93f28fe984 | ||
|
|
4b0ff3ee5f | ||
|
|
43ba1a4a5e | ||
|
|
23eb85e898 | ||
|
|
9e22ab305c | ||
|
|
1b2bc07592 | ||
|
|
18274e4f0e | ||
|
|
468b290d28 | ||
|
|
f6d92e5024 | ||
|
|
a25d26e921 | ||
|
|
65f41cb7bd | ||
|
|
864ee7c7ad | ||
|
|
decb49fc0a | ||
|
|
b6ffb33adc | ||
|
|
9b24535d44 | ||
|
|
658a62860e | ||
|
|
f6bfd18816 | ||
|
|
ba76c43a26 | ||
|
|
96516f113a | ||
|
|
e4b1a196ae | ||
|
|
7779e5a1ec | ||
|
|
0444831073 | ||
|
|
a6c1f97d12 | ||
|
|
8d97863db6 | ||
|
|
3a1e17e04d | ||
|
|
78d3330ccf | ||
|
|
2a398985cf | ||
|
|
b35b72c066 | ||
|
|
a366693e96 | ||
|
|
ed5ed4b80a | ||
|
|
7f2c4a3566 | ||
|
|
7112caaedd | ||
|
|
18f2b49b67 | ||
|
|
b19bb2b7af | ||
|
|
6d47a340b1 | ||
|
|
a386736dcc | ||
|
|
bf68a8a3e8 | ||
|
|
bb106b7c49 | ||
|
|
3a2a16a09c | ||
|
|
5f21eaabf8 | ||
|
|
4436e6d81d | ||
|
|
47856a26f1 | ||
|
|
9430803f36 | ||
|
|
c19827a0e5 | ||
|
|
fae15dbdfe | ||
|
|
e7076f5aee |
109 changed files with 1085 additions and 584 deletions
14
README.md
14
README.md
|
|
@ -9,24 +9,22 @@ Installing is the same as the Vencord devs laid out: [(Install)](https://docs.ve
|
||||||
|
|
||||||
# Vencord
|
# Vencord
|
||||||
|
|
||||||
|

|
||||||
[](https://codeberg.org/Vee/cord)
|
[](https://codeberg.org/Vee/cord)
|
||||||
|
|
||||||
The cutest Discord client mod
|
The cutest Discord client mod
|
||||||
|
|
||||||
|  |
|

|
||||||
| :--------------------------------------------------------------------------------------------------: |
|
|
||||||
| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) |
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Super easy to install (Download Installer, open, click install button, done)
|
- Easy to install
|
||||||
- 100+ plugins built in: [See a list](https://vencord.dev/plugins)
|
- [100+ built in plugins](https://vencord.dev/plugins)
|
||||||
- Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, PermissionsViewer, CustomCommands, ShowHiddenChannels, PronounDB
|
|
||||||
- Fairly lightweight despite the many inbuilt plugins
|
- Fairly lightweight despite the many inbuilt plugins
|
||||||
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
- 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)
|
- 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
|
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
||||||
- Settings sync: Keep your plugins and their settings synchronised between devices / apps (optional)
|
- Settings sync: Keep your plugins and their settings synchronised between devices / apps (optional)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,13 @@
|
||||||
/// <reference path="../src/globals.d.ts" />
|
/// <reference path="../src/globals.d.ts" />
|
||||||
|
|
||||||
import monacoHtmlLocal from "file://monacoWin.html?minify";
|
import monacoHtmlLocal from "file://monacoWin.html?minify";
|
||||||
import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
|
|
||||||
import * as DataStore from "../src/api/DataStore";
|
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 { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
||||||
import { getTheme, Theme } from "../src/utils/discord";
|
import { getTheme, Theme } from "../src/utils/discord";
|
||||||
import { getThemeInfo } from "../src/main/themes";
|
import { getThemeInfo } from "../src/main/themes";
|
||||||
import { Settings } from "../src/Vencord";
|
import { Settings } from "../src/Vencord";
|
||||||
|
import { getStylusWebStoreUrl } from "@utils/web";
|
||||||
// Discord deletes this so need to store in variable
|
|
||||||
const { localStorage } = window;
|
|
||||||
|
|
||||||
// listeners for ipc.on
|
// listeners for ipc.on
|
||||||
const cssListeners = new Set<(css: string) => void>();
|
const cssListeners = new Set<(css: string) => void>();
|
||||||
|
|
@ -45,12 +42,13 @@ window.VencordNative = {
|
||||||
themes: {
|
themes: {
|
||||||
uploadTheme: (fileName: string, fileData: string) => DataStore.set(fileName, fileData, themeStore),
|
uploadTheme: (fileName: string, fileData: string) => DataStore.set(fileName, fileData, themeStore),
|
||||||
deleteTheme: (fileName: string) => DataStore.del(fileName, themeStore),
|
deleteTheme: (fileName: string) => DataStore.del(fileName, themeStore),
|
||||||
getThemesDir: async () => "",
|
|
||||||
getThemesList: () => DataStore.entries(themeStore).then(entries =>
|
getThemesList: () => DataStore.entries(themeStore).then(entries =>
|
||||||
entries.map(([name, css]) => getThemeInfo(css, name.toString()))
|
entries.map(([name, css]) => getThemeInfo(css, name.toString()))
|
||||||
),
|
),
|
||||||
getThemeData: (fileName: string) => DataStore.get(fileName, themeStore),
|
getThemeData: (fileName: string) => DataStore.get(fileName, themeStore),
|
||||||
getSystemValues: async () => ({}),
|
getSystemValues: async () => ({}),
|
||||||
|
|
||||||
|
openFolder: async () => Promise.reject("themes:openFolder is not supported on web"),
|
||||||
},
|
},
|
||||||
|
|
||||||
native: {
|
native: {
|
||||||
|
|
@ -77,6 +75,14 @@ window.VencordNative = {
|
||||||
addThemeChangeListener: NOOP,
|
addThemeChangeListener: NOOP,
|
||||||
openFile: NOOP_ASYNC,
|
openFile: NOOP_ASYNC,
|
||||||
async openEditor() {
|
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 features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
|
||||||
const win = open("about:blank", "VencordQuickCss", features);
|
const win = open("about:blank", "VencordQuickCss", features);
|
||||||
if (!win) {
|
if (!win) {
|
||||||
|
|
@ -92,7 +98,7 @@ window.VencordNative = {
|
||||||
? "vs-light"
|
? "vs-light"
|
||||||
: "vs-dark";
|
: "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)),
|
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,
|
pluginHelpers: {} as any,
|
||||||
|
csp: {} as any,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "vencord",
|
"name": "vencord",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"version": "1.12.2",
|
"version": "1.12.5",
|
||||||
"description": "The cutest Discord client mod",
|
"description": "The cutest Discord client mod",
|
||||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const defines = stringifyValues({
|
||||||
IS_UPDATER_DISABLED,
|
IS_UPDATER_DISABLED,
|
||||||
IS_WEB: false,
|
IS_WEB: false,
|
||||||
IS_EXTENSION: false,
|
IS_EXTENSION: false,
|
||||||
|
IS_USERSCRIPT: false,
|
||||||
VERSION,
|
VERSION,
|
||||||
BUILD_TIMESTAMP
|
BUILD_TIMESTAMP
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ const commonOptions = {
|
||||||
define: stringifyValues({
|
define: stringifyValues({
|
||||||
IS_WEB: true,
|
IS_WEB: true,
|
||||||
IS_EXTENSION: false,
|
IS_EXTENSION: false,
|
||||||
|
IS_USERSCRIPT: false,
|
||||||
IS_STANDALONE: true,
|
IS_STANDALONE: true,
|
||||||
IS_DEV,
|
IS_DEV,
|
||||||
IS_REPORTER,
|
IS_REPORTER,
|
||||||
|
|
@ -98,6 +99,7 @@ const buildConfigs = [
|
||||||
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
||||||
define: {
|
define: {
|
||||||
...commonOptions.define,
|
...commonOptions.define,
|
||||||
|
IS_USERSCRIPT: "true",
|
||||||
window: "unsafeWindow",
|
window: "unsafeWindow",
|
||||||
},
|
},
|
||||||
outfile: "dist/Vencord.user.js",
|
outfile: "dist/Vencord.user.js",
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,12 @@ async function init() {
|
||||||
|
|
||||||
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
||||||
runUpdateCheck();
|
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
|
setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (IS_DEV) {
|
if (IS_DEV) {
|
||||||
const pendingPatches = patches.filter(p => !p.all && p.predicate?.() !== false);
|
const pendingPatches = patches.filter(p => !p.all && p.predicate?.() !== false);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Settings } from "@api/Settings";
|
import type { Settings } from "@api/Settings";
|
||||||
|
import { CspRequestResult } from "@main/csp/manager";
|
||||||
import { PluginIpcMappings } from "@main/ipcPlugins";
|
import { PluginIpcMappings } from "@main/ipcPlugins";
|
||||||
import type { UserThemeHeader } from "@main/themes";
|
import type { UserThemeHeader } from "@main/themes";
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
|
|
@ -33,10 +34,11 @@ export default {
|
||||||
themes: {
|
themes: {
|
||||||
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
|
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
|
||||||
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
|
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
|
||||||
getThemesDir: () => invoke<string>(IpcEvents.GET_THEMES_DIR),
|
|
||||||
getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),
|
getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),
|
||||||
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName),
|
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName),
|
||||||
getSystemValues: () => invoke<Record<string, string>>(IpcEvents.GET_THEME_SYSTEM_VALUES),
|
getSystemValues: () => invoke<Record<string, string>>(IpcEvents.GET_THEME_SYSTEM_VALUES),
|
||||||
|
|
||||||
|
openFolder: () => invoke<void>(IpcEvents.OPEN_THEMES_FOLDER),
|
||||||
},
|
},
|
||||||
|
|
||||||
updater: {
|
updater: {
|
||||||
|
|
@ -49,7 +51,8 @@ export default {
|
||||||
settings: {
|
settings: {
|
||||||
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
|
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
|
||||||
set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify),
|
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: {
|
quickCss: {
|
||||||
|
|
@ -73,5 +76,17 @@ export default {
|
||||||
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
|
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
|
pluginHelpers: PluginHelpers
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export function _modifyAccessories(
|
||||||
) {
|
) {
|
||||||
for (const [key, accessory] of accessories.entries()) {
|
for (const [key, accessory] of accessories.entries()) {
|
||||||
const res = (
|
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} />
|
<accessory.render {...props} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
all: unset;
|
all: unset;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-base-lower-alt);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.visual-refresh .vc-notification-root {
|
.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) {
|
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => {
|
||||||
logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
|
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() {
|
render() {
|
||||||
if (this.state.error === NO_ERROR) return this.props.children;
|
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)
|
if (this.props.fallback)
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,9 @@
|
||||||
background-color: #e7828430;
|
background-color: #e7828430;
|
||||||
border: 1px solid #e78284;
|
border: 1px solid #e78284;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
color: var(--text-normal, white);
|
color: var(--text-default, white);
|
||||||
|
|
||||||
|
& a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ export function Link(props: React.PropsWithChildren<Props>) {
|
||||||
props.style.pointerEvents = "none";
|
props.style.pointerEvents = "none";
|
||||||
props["aria-disabled"] = true;
|
props["aria-disabled"] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
props.rel ??= "noreferrer";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a role="link" target="_blank" {...props}>
|
<a role="link" target="_blank" {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
||||||
{!!plugin.settingsAboutComponent && (
|
{!!plugin.settingsAboutComponent && (
|
||||||
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
|
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
|
||||||
<Forms.FormSection>
|
<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} />
|
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Forms.FormSection>
|
</Forms.FormSection>
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background: var(--info-warning-background);
|
background: var(--info-warning-background);
|
||||||
border: 1px solid var(--info-warning-foreground);
|
border: 1px solid var(--info-warning-foreground);
|
||||||
color: var(--info-warning-text);
|
color: var(--info-warning-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-plugins-restart-button {
|
.vc-plugins-restart-button {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { Settings, useSettings } from "@api/Settings";
|
||||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||||
import { Grid } from "@components/Grid";
|
import { Grid } from "@components/Grid";
|
||||||
import { Link } from "@components/Link";
|
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 { Margins } from "@utils/margins";
|
||||||
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
||||||
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
||||||
|
|
@ -38,6 +38,8 @@ function validateUrl(url: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function eraseAllData() {
|
async function eraseAllData() {
|
||||||
|
if (!await checkCloudUrlCsp()) return;
|
||||||
|
|
||||||
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { Authorization: await getCloudAuth() }
|
headers: { Authorization: await getCloudAuth() }
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{compileResult &&
|
{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]}
|
{compileResult[1]}
|
||||||
</Forms.FormText>
|
</Forms.FormText>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,17 +18,21 @@
|
||||||
|
|
||||||
import { Settings, useSettings } from "@api/Settings";
|
import { Settings, useSettings } from "@api/Settings";
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
import { Flex } from "@components/Flex";
|
import { Flex } from "@components/Flex";
|
||||||
import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
|
import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
|
||||||
import { Link } from "@components/Link";
|
import { Link } from "@components/Link";
|
||||||
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||||
import type { UserThemeHeader } from "@main/themes";
|
import type { UserThemeHeader } from "@main/themes";
|
||||||
|
import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations";
|
||||||
import { openInviteModal } from "@utils/discord";
|
import { openInviteModal } from "@utils/discord";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { showItemInFolder } from "@utils/native";
|
import { classes } from "@utils/misc";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { relaunch } from "@utils/native";
|
||||||
|
import { useAwaiter, useForceUpdater } from "@utils/react";
|
||||||
|
import { getStylusWebStoreUrl } from "@utils/web";
|
||||||
import { findLazy } from "@webpack";
|
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 type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||||
|
|
||||||
import Plugins from "~plugins";
|
import Plugins from "~plugins";
|
||||||
|
|
@ -65,7 +69,7 @@ function Validator({ link }: { link: string; }) {
|
||||||
: "Valid!";
|
: "Valid!";
|
||||||
|
|
||||||
return <Forms.FormText style={{
|
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>;
|
}}>{text}</Forms.FormText>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,7 +163,6 @@ function ThemesTab() {
|
||||||
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
|
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
|
||||||
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
|
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
|
||||||
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
|
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
|
||||||
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshLocalThemes();
|
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>
|
<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>
|
||||||
|
|
||||||
|
<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">
|
<Forms.FormSection title="Local Themes">
|
||||||
<QuickActionCard>
|
<QuickActionCard>
|
||||||
<>
|
<>
|
||||||
|
|
@ -241,8 +250,7 @@ function ThemesTab() {
|
||||||
) : (
|
) : (
|
||||||
<QuickAction
|
<QuickAction
|
||||||
text="Open Themes Folder"
|
text="Open Themes Folder"
|
||||||
action={() => showItemInFolder(themeDir!)}
|
action={() => VencordNative.themes.openFolder()}
|
||||||
disabled={themeDirPending}
|
|
||||||
Icon={FolderIcon}
|
Icon={FolderIcon}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -347,10 +355,99 @@ function ThemesTab() {
|
||||||
</TabBar.Item>
|
</TabBar.Item>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
|
|
||||||
|
<CspErrorCard />
|
||||||
{currentTab === ThemeTab.LOCAL && renderLocalThemes()}
|
{currentTab === ThemeTab.LOCAL && renderLocalThemes()}
|
||||||
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
|
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
|
||||||
</SettingsTab>
|
</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");
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof
|
||||||
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
||||||
<span style={{
|
<span style={{
|
||||||
marginLeft: "0.5em",
|
marginLeft: "0.5em",
|
||||||
color: "var(--text-normal)"
|
color: "var(--text-default)"
|
||||||
}}>{message} - {author}</span>
|
}}>{message} - {author}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,7 @@ import { gitRemote } from "@shared/vencordUserAgent";
|
||||||
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
|
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { identity, isPluginDev } from "@utils/misc";
|
import { identity, isPluginDev } from "@utils/misc";
|
||||||
import { relaunch, showItemInFolder } from "@utils/native";
|
import { relaunch } from "@utils/native";
|
||||||
import { useAwaiter } from "@utils/react";
|
|
||||||
import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
|
import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import BadgeAPI from "../../plugins/_api/badges";
|
import BadgeAPI from "../../plugins/_api/badges";
|
||||||
|
|
@ -53,9 +52,6 @@ type KeysOfType<Object, Type> = {
|
||||||
}[keyof Object];
|
}[keyof Object];
|
||||||
|
|
||||||
function VencordSettings() {
|
function VencordSettings() {
|
||||||
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
|
|
||||||
fallbackValue: "Loading..."
|
|
||||||
});
|
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
|
||||||
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
|
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
|
||||||
|
|
@ -171,7 +167,7 @@ function VencordSettings() {
|
||||||
<QuickAction
|
<QuickAction
|
||||||
Icon={FolderIcon}
|
Icon={FolderIcon}
|
||||||
text="Open Settings Folder"
|
text="Open Settings Folder"
|
||||||
action={() => showItemInFolder(settingsDir)}
|
action={() => VencordNative.settings.openFolder()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<QuickAction
|
<QuickAction
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
.vc-addon-card {
|
.vc-addon-card {
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-base-lower-alt);
|
||||||
color: var(--interactive-active);
|
color: var(--interactive-active);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
.vc-settings-quickActions-pill {
|
.vc-settings-quickActions-pill {
|
||||||
all: unset;
|
all: unset;
|
||||||
background: var(--background-secondary);
|
background: var(--background-base-lower);
|
||||||
color: var(--header-secondary);
|
color: var(--header-secondary);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-settings-quickActions-pill:hover {
|
.vc-settings-quickActions-pill:hover {
|
||||||
background: var(--background-secondary-alt);
|
background: var(--background-base-lower-alt);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: var(--elevation-high);
|
box-shadow: var(--elevation-high);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,13 @@
|
||||||
.vc-backup-restore-card {
|
.vc-backup-restore-card {
|
||||||
background-color: var(--info-warning-background);
|
background-color: var(--info-warning-background);
|
||||||
border-color: var(--info-warning-foreground);
|
border-color: var(--info-warning-foreground);
|
||||||
color: var(--info-warning-text);
|
color: var(--info-warning-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-settings-theme-links {
|
.vc-settings-theme-links {
|
||||||
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
|
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
|
||||||
display: inline-block !important;
|
display: inline-block !important;
|
||||||
color: var(--text-normal) !important;
|
color: var(--text-default) !important;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border: 1px solid var(--background-modifier-accent);
|
border: 1px solid var(--background-modifier-accent);
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
.vc-settings-theme-card {
|
.vc-settings-theme-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-base-lower-alt);
|
||||||
color: var(--interactive-active);
|
color: var(--interactive-active);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|
@ -27,3 +27,26 @@
|
||||||
.vc-settings-theme-author::before {
|
.vc-settings-theme-author::before {
|
||||||
content: "by ";
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-owner-crown-icon {
|
.vc-owner-crown-icon {
|
||||||
color: var(--text-warning);
|
color: var(--status-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-heart-icon {
|
.vc-heart-icon {
|
||||||
|
|
|
||||||
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
|
|
@ -29,11 +29,12 @@ declare global {
|
||||||
* replace: "IS_WEB?foo:bar"
|
* replace: "IS_WEB?foo:bar"
|
||||||
* // GOOD
|
* // GOOD
|
||||||
* replace: IS_WEB ? "foo" : "bar"
|
* replace: IS_WEB ? "foo" : "bar"
|
||||||
* // also good
|
* // also okay
|
||||||
* replace: `${IS_WEB}?foo:bar`
|
* replace: `${IS_WEB}?foo:bar`
|
||||||
*/
|
*/
|
||||||
export var IS_WEB: boolean;
|
export var IS_WEB: boolean;
|
||||||
export var IS_EXTENSION: boolean;
|
export var IS_EXTENSION: boolean;
|
||||||
|
export var IS_USERSCRIPT: boolean;
|
||||||
export var IS_STANDALONE: boolean;
|
export var IS_STANDALONE: boolean;
|
||||||
export var IS_UPDATER_DISABLED: boolean;
|
export var IS_UPDATER_DISABLED: boolean;
|
||||||
export var IS_DEV: boolean;
|
export var IS_DEV: boolean;
|
||||||
|
|
|
||||||
154
src/main/csp/index.ts
Normal file
154
src/main/csp/index.ts
Normal 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
125
src/main/csp/manager.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,9 +16,11 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, protocol, session } from "electron";
|
import { app, net, protocol } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { pathToFileURL } from "url";
|
||||||
|
|
||||||
|
import { initCsp } from "./csp";
|
||||||
import { ensureSafePath } from "./ipcMain";
|
import { ensureSafePath } from "./ipcMain";
|
||||||
import { RendererSettings } from "./settings";
|
import { RendererSettings } from "./settings";
|
||||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||||
|
|
@ -26,21 +28,27 @@ import { installExt } from "./utils/extensions";
|
||||||
|
|
||||||
if (IS_VESKTOP || !IS_VANILLA) {
|
if (IS_VESKTOP || !IS_VANILLA) {
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
protocol.handle("vencord", ({ url: unsafeUrl }) => {
|
||||||
// from a string I don't think any other form of sourcemaps would work
|
let url = decodeURI(unsafeUrl).slice("vencord://".length).replace(/\?v=\d+$/, "");
|
||||||
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
|
||||||
let url = unsafeUrl.slice("vencord://".length);
|
|
||||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||||
|
|
||||||
if (url.startsWith("/themes/")) {
|
if (url.startsWith("/themes/")) {
|
||||||
const theme = url.slice("/themes/".length);
|
const theme = url.slice("/themes/".length);
|
||||||
|
|
||||||
const safeUrl = ensureSafePath(THEMES_DIR, theme);
|
const safeUrl = ensureSafePath(THEMES_DIR, theme);
|
||||||
if (!safeUrl) {
|
if (!safeUrl) {
|
||||||
cb({ statusCode: 403 });
|
return new Response(null, {
|
||||||
return;
|
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) {
|
switch (url) {
|
||||||
case "renderer.js.map":
|
case "renderer.js.map":
|
||||||
case "vencordDesktopRenderer.js.map":
|
case "vencordDesktopRenderer.js.map":
|
||||||
|
|
@ -48,10 +56,11 @@ if (IS_VESKTOP || !IS_VANILLA) {
|
||||||
case "vencordDesktopPreload.js.map":
|
case "vencordDesktopPreload.js.map":
|
||||||
case "patcher.js.map":
|
case "patcher.js.map":
|
||||||
case "vencordDesktopMain.js.map":
|
case "vencordDesktopMain.js.map":
|
||||||
cb(join(__dirname, url));
|
return net.fetch(pathToFileURL(join(__dirname, url)).toString());
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
cb({ statusCode: 403 });
|
return new Response(null, {
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -63,70 +72,7 @@ if (IS_VESKTOP || !IS_VANILLA) {
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
|
|
||||||
const findHeader = (headers: Record<string, string[]>, headerName: Lowercase<string>) => {
|
initCsp();
|
||||||
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 = () => { };
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,17 @@ import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
|
||||||
import { open, readdir, readFile } from "fs/promises";
|
import { open, readdir, readFile } from "fs/promises";
|
||||||
import { join, normalize } from "path";
|
import { join, normalize } from "path";
|
||||||
|
|
||||||
|
import { registerCspIpcHandlers } from "./csp/manager";
|
||||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
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";
|
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||||
|
|
||||||
mkdirSync(THEMES_DIR, { recursive: true });
|
mkdirSync(THEMES_DIR, { recursive: true });
|
||||||
|
|
||||||
|
registerCspIpcHandlers();
|
||||||
|
|
||||||
export function ensureSafePath(basePath: string, path: string) {
|
export function ensureSafePath(basePath: string, path: string) {
|
||||||
const normalizedBasePath = normalize(basePath);
|
const normalizedBasePath = normalize(basePath + "/");
|
||||||
const newPath = join(basePath, path);
|
const newPath = join(basePath, path);
|
||||||
const normalizedPath = normalize(newPath);
|
const normalizedPath = normalize(newPath);
|
||||||
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
|
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
|
||||||
|
|
@ -89,7 +92,6 @@ ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
||||||
writeFileSync(QUICKCSS_PATH, css)
|
writeFileSync(QUICKCSS_PATH, css)
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
|
|
||||||
ipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes());
|
ipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes());
|
||||||
ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName));
|
ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName));
|
||||||
ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
|
ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
|
||||||
|
|
@ -97,6 +99,8 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
|
||||||
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}`
|
"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) {
|
export function initIpc(mainWindow: BrowserWindow) {
|
||||||
let quickCssWatcher: FSWatcher | undefined;
|
let quickCssWatcher: FSWatcher | undefined;
|
||||||
|
|
|
||||||
|
|
@ -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.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain);
|
||||||
|
|
||||||
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => {
|
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => {
|
||||||
|
|
@ -49,16 +48,18 @@ export interface NativeSettings {
|
||||||
[setting: string]: any;
|
[setting: string]: any;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
customCspRules: Record<string, string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultNativeSettings: NativeSettings = {
|
const DefaultNativeSettings: NativeSettings = {
|
||||||
plugins: {}
|
plugins: {},
|
||||||
|
customCspRules: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
|
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
|
||||||
mergeDefaults(nativeSettings, DefaultNativeSettings);
|
mergeDefaults(nativeSettings, DefaultNativeSettings);
|
||||||
|
|
||||||
export const NativeSettings = new SettingsStore(nativeSettings);
|
export const NativeSettings = new SettingsStore(nativeSettings as NativeSettings);
|
||||||
|
|
||||||
NativeSettings.addGlobalChangeListener(() => {
|
NativeSettings.addGlobalChangeListener(() => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,10 @@ export function serializeErrors(func: (...args: any[]) => any) {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: e instanceof Error ? {
|
error: e instanceof Error ? {
|
||||||
// prototypes get lost, so turn error into plain object
|
// prototypes get lost, so turn error into plain object
|
||||||
...e
|
...e,
|
||||||
|
message: e.message,
|
||||||
|
name: e.name,
|
||||||
|
stack: e.stack
|
||||||
} : e
|
} : e
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { get } from "@main/utils/simpleGet";
|
import { fetchBuffer, fetchJson } from "@main/utils/http";
|
||||||
import { IpcEvents } from "@shared/IpcEvents";
|
import { IpcEvents } from "@shared/IpcEvents";
|
||||||
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
|
|
@ -31,8 +31,8 @@ import { serializeErrors, VENCORD_FILES } from "./common";
|
||||||
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
||||||
let PendingUpdates = [] as [string, string][];
|
let PendingUpdates = [] as [string, string][];
|
||||||
|
|
||||||
async function githubGet(endpoint: string) {
|
async function githubGet<T = any>(endpoint: string) {
|
||||||
return get(API_BASE + endpoint, {
|
return fetchJson<T>(API_BASE + endpoint, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
// "All API requests MUST include a valid User-Agent header.
|
// "All API requests MUST include a valid User-Agent header.
|
||||||
|
|
@ -46,9 +46,8 @@ async function calculateGitChanges() {
|
||||||
const isOutdated = await fetchUpdates();
|
const isOutdated = await fetchUpdates();
|
||||||
if (!isOutdated) return [];
|
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) => ({
|
return data.commits.map((c: any) => ({
|
||||||
// github api only sends the long sha
|
// github api only sends the long sha
|
||||||
hash: c.sha.slice(0, 7),
|
hash: c.sha.slice(0, 7),
|
||||||
|
|
@ -58,9 +57,8 @@ async function calculateGitChanges() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUpdates() {
|
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);
|
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
|
||||||
if (hash === gitHash)
|
if (hash === gitHash)
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -70,16 +68,20 @@ async function fetchUpdates() {
|
||||||
PendingUpdates.push([name, browser_download_url]);
|
PendingUpdates.push([name, browser_download_url]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyUpdates() {
|
async function applyUpdates() {
|
||||||
await Promise.all(PendingUpdates.map(
|
const fileContents = await Promise.all(PendingUpdates.map(async ([name, url]) => {
|
||||||
async ([name, data]) => writeFile(
|
const contents = await fetchBuffer(url);
|
||||||
join(__dirname, name),
|
return [join(__dirname, name), contents] as const;
|
||||||
await get(data)
|
}));
|
||||||
)
|
|
||||||
));
|
await Promise.all(fileContents.map(async ([filename, contents]) =>
|
||||||
|
writeFile(filename, contents))
|
||||||
|
);
|
||||||
|
|
||||||
PendingUpdates = [];
|
PendingUpdates = [];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { join } from "path";
|
||||||
|
|
||||||
import { DATA_DIR } from "./constants";
|
import { DATA_DIR } from "./constants";
|
||||||
import { crxToZip } from "./crxToZip";
|
import { crxToZip } from "./crxToZip";
|
||||||
import { get } from "./simpleGet";
|
import { fetchBuffer } from "./http";
|
||||||
|
|
||||||
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
||||||
|
|
||||||
|
|
@ -69,13 +69,14 @@ export async function installExt(id: string) {
|
||||||
} catch (err) {
|
} 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 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: {
|
headers: {
|
||||||
"User-Agent": `Electron ${process.versions.electron} ~ Vencord (https://github.com/Vendicated/Vencord)`
|
"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);
|
session.defaultSession.loadExtension(extDir);
|
||||||
|
|
|
||||||
70
src/main/utils/http.ts
Normal file
70
src/main/utils/http.ts
Normal 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)));
|
||||||
|
}
|
||||||
|
|
@ -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)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -33,7 +33,7 @@ import definePlugin from "@utils/types";
|
||||||
import { Forms, Toasts, UserStore } from "@webpack/common";
|
import { Forms, Toasts, UserStore } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
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 = {
|
const ContributorBadge: ProfileBadge = {
|
||||||
description: "Vencord Contributor",
|
description: "Vencord Contributor",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
||||||
{
|
{
|
||||||
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
|
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
|
||||||
replacement: {
|
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) => "" +
|
replace: (_, ReactButton, PotionButton, ButtonComponent, showReactButton, message) => "" +
|
||||||
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,${showReactButton}&&${PotionButton},`
|
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,${showReactButton}&&${PotionButton},`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,7 @@ export default definePlugin({
|
||||||
find: "async uploadFiles(",
|
find: "async uploadFiles(",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /async uploadFiles\((\i),\i\){/,
|
match: /async uploadFiles\((\i)\){/,
|
||||||
replace: "$&$1.forEach($self.anonymise);"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
match: /async uploadFilesSimple\((\i)\){/,
|
|
||||||
replace: "$&$1.forEach($self.anonymise);"
|
replace: "$&$1.forEach($self.anonymise);"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ interface RemoteData {
|
||||||
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
|
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_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;
|
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 bundleUrl = new URL(html.match(APPLE_MUSIC_BUNDLE_REGEX)![1], "https://music.apple.com/");
|
||||||
|
|
||||||
const bundle = await fetch(bundleUrl).then(r => r.text());
|
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;
|
cachedToken = token;
|
||||||
return token;
|
return token;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -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
|
// 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))"
|
replace: "$&.filter($self.makeGuildsBarSidebarFilter(!!arguments[0]?.isBetterFolders))"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { Devs } from "@utils/constants";
|
||||||
import { getCurrentGuild, openImageModal } from "@utils/discord";
|
import { getCurrentGuild, openImageModal } from "@utils/discord";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { GuildStore, Menu, PermissionStore } from "@webpack/common";
|
import { GuildRoleStore, Menu, PermissionStore } from "@webpack/common";
|
||||||
|
|
||||||
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ export default definePlugin({
|
||||||
const guild = getCurrentGuild();
|
const guild = getCurrentGuild();
|
||||||
if (!guild) return;
|
if (!guild) return;
|
||||||
|
|
||||||
const role = GuildStore.getRole(guild.id, id);
|
const role = GuildRoleStore.getRole(guild.id, id);
|
||||||
if (!role) return;
|
if (!role) return;
|
||||||
|
|
||||||
if (role.colorString) {
|
if (role.colorString) {
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export default definePlugin({
|
||||||
|
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
backgroundColor: "var(--interactive-normal)",
|
backgroundColor: "var(--interactive-normal)",
|
||||||
color: "var(--background-secondary)",
|
color: "var(--background-base-lower)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlatformIcon width={14} height={14} />
|
<PlatformIcon width={14} height={14} />
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export default definePlugin({
|
||||||
|
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: ".embedWrapper,embed",
|
find: "}renderEmbeds(",
|
||||||
replacement: [{
|
replacement: [{
|
||||||
match: /\.container/,
|
match: /\.container/,
|
||||||
replace: "$&+(this.props.channel.nsfw? ' vc-nsfw-img': '')"
|
replace: "$&+(this.props.channel.nsfw? ' vc-nsfw-img': '')"
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@ export default definePlugin({
|
||||||
patches: [{
|
patches: [{
|
||||||
find: "renderConnectionStatus(){",
|
find: "renderConnectionStatus(){",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /(renderConnectionStatus\(\){.+\.channel,children:)(.+?}\):\i)(?=}\))/,
|
// in renderConnectionStatus()
|
||||||
|
match: /(lineClamp:1,children:)(\i)(?=,|}\))/,
|
||||||
replace: "$1[$2,$self.renderTimer(this.props.channel.id)]"
|
replace: "$1[$2,$self.renderTimer(this.props.channel.id)]"
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,13 @@
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { ErrorCard } from "@components/ErrorCard";
|
import { ErrorCard } from "@components/ErrorCard";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
import { findByCodeLazy, findStoreLazy } from "@webpack";
|
||||||
import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
|
import { Button, ColorPicker, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
|
||||||
|
|
||||||
import { settings } from "..";
|
import { settings } from "..";
|
||||||
import { relativeLuminance } from "../utils/colorUtils";
|
import { relativeLuminance } from "../utils/colorUtils";
|
||||||
import { createOrUpdateThemeColorVars } from "../utils/styleUtils";
|
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 saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"===');
|
||||||
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
|
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -456,7 +456,7 @@ export default definePlugin({
|
||||||
|
|
||||||
<Forms.FormDivider className={Margins.top8} />
|
<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[0] && <ActivityView
|
||||||
activity={activity[0]}
|
activity={activity[0]}
|
||||||
user={UserStore.getCurrentUser()}
|
user={UserStore.getCurrentUser()}
|
||||||
|
|
|
||||||
|
|
@ -150,5 +150,5 @@ export default definePlugin({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
DecorSection: ErrorBoundary.wrap(DecorSection)
|
DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ function parseNode(node: Node) {
|
||||||
function initWs(isManual = false) {
|
function initWs(isManual = false) {
|
||||||
let wasConnected = isManual;
|
let wasConnected = isManual;
|
||||||
let hasErrored = false;
|
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", () => {
|
ws.addEventListener("open", () => {
|
||||||
wasConnected = true;
|
wasConnected = true;
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// Change top right chat toolbar button from the help one to the dev one
|
// Change top right chat toolbar button from the help one to the dev one
|
||||||
{
|
{
|
||||||
find: ".CONTEXTLESS,isActivityPanelMode:",
|
find: '"M9 3v18"',
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /hasBugReporterAccess:(\i)/,
|
match: /hasBugReporterAccess:(\i)/,
|
||||||
replace: "_hasBugReporterAccess:$1=true"
|
replace: "_hasBugReporterAccess:$1=true"
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ function CloneModal({ data }: { data: Sticker | Emoji; }) {
|
||||||
aria-disabled={isCloning}
|
aria-disabled={isCloning}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
backgroundColor: "var(--background-secondary)",
|
backgroundColor: "var(--background-base-lower)",
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ const settings = definePluginSettings({
|
||||||
description: "Size of the emojis when sending",
|
description: "Size of the emojis when sending",
|
||||||
type: OptionType.SLIDER,
|
type: OptionType.SLIDER,
|
||||||
default: 48,
|
default: 48,
|
||||||
markers: [32, 48, 64, 128, 160, 256, 512]
|
markers: [32, 48, 64, 96, 128, 160, 256, 512]
|
||||||
},
|
},
|
||||||
transformEmojis: {
|
transformEmojis: {
|
||||||
description: "Whether to transform fake emojis into real ones",
|
description: "Whether to transform fake emojis into real ones",
|
||||||
|
|
@ -394,7 +394,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
// Separate patch for allowing using custom app icons
|
// Separate patch for allowing using custom app icons
|
||||||
{
|
{
|
||||||
find: "?24:30,",
|
find: "getCurrentDesktopIcon(),",
|
||||||
replacement: {
|
replacement: {
|
||||||
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
|
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
|
||||||
replace: "true"
|
replace: "true"
|
||||||
|
|
@ -671,24 +671,27 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) {
|
shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) {
|
||||||
|
try {
|
||||||
const contentItems = message.content.split(/\s/);
|
const contentItems = message.content.split(/\s/);
|
||||||
if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false;
|
if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false;
|
||||||
|
|
||||||
switch (embed.type) {
|
switch (embed.type) {
|
||||||
case "image": {
|
case "image": {
|
||||||
|
const url = embed.url ?? embed.image?.url;
|
||||||
|
if (!url) return false;
|
||||||
if (
|
if (
|
||||||
!settings.store.transformCompoundSentence
|
!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;
|
) return false;
|
||||||
|
|
||||||
if (settings.store.transformEmojis) {
|
if (settings.store.transformEmojis) {
|
||||||
if (fakeNitroEmojiRegex.test(embed.url!)) return true;
|
if (fakeNitroEmojiRegex.test(url)) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.store.transformStickers) {
|
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) {
|
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
|
// 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;
|
if (StickerStore.getStickerById(gifMatch[1])) return true;
|
||||||
|
|
@ -698,6 +701,9 @@ export default definePlugin({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
new Logger("FakeNitro").error("Error in shouldIgnoreEmbed:", e);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,9 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import { Margins } from "@utils/margins";
|
import { Margins } from "@utils/margins";
|
||||||
import { classes, copyWithToast } from "@utils/misc";
|
import { classes, copyWithToast } from "@utils/misc";
|
||||||
import { useAwaiter } from "@utils/react";
|
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
|
import { findComponentByCodeLazy } from "@webpack";
|
||||||
import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
|
import { Button, ColorPicker, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
|
||||||
import { User } from "discord-types/general";
|
import { User } from "discord-types/general";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import virtualMerge from "virtual-merge";
|
import virtualMerge from "virtual-merge";
|
||||||
|
|
@ -109,10 +108,8 @@ interface ProfileModalProps {
|
||||||
isTryItOutFlow: boolean;
|
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 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({
|
export default definePlugin({
|
||||||
name: "FakeProfileThemes",
|
name: "FakeProfileThemes",
|
||||||
|
|
@ -141,8 +138,6 @@ export default definePlugin({
|
||||||
const [color1, setColor1] = useState(existingColors[0]);
|
const [color1, setColor1] = useState(existingColors[0]);
|
||||||
const [color2, setColor2] = useState(existingColors[1]);
|
const [color2, setColor2] = useState(existingColors[1]);
|
||||||
|
|
||||||
const [, , loadingColorPickerChunk] = useAwaiter(requireColorPicker);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Forms.FormSection>
|
<Forms.FormSection>
|
||||||
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
||||||
|
|
@ -162,7 +157,6 @@ export default definePlugin({
|
||||||
className={classes(Margins.top8, Margins.bottom8)}
|
className={classes(Margins.top8, Margins.bottom8)}
|
||||||
/>
|
/>
|
||||||
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
|
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
|
||||||
{!loadingColorPickerChunk && (
|
|
||||||
<Flex
|
<Flex
|
||||||
direction={Flex.Direction.HORIZONTAL}
|
direction={Flex.Direction.HORIZONTAL}
|
||||||
style={{ gap: "1rem" }}
|
style={{ gap: "1rem" }}
|
||||||
|
|
@ -206,7 +200,6 @@ export default definePlugin({
|
||||||
Copy 3y3
|
Copy 3y3
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
|
||||||
<Forms.FormDivider
|
<Forms.FormDivider
|
||||||
className={classes(Margins.top8, Margins.bottom8)}
|
className={classes(Margins.top8, Margins.bottom8)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ export default definePlugin({
|
||||||
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
|
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary noop={true}>
|
<ErrorBoundary noop>
|
||||||
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
|
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findStoreLazy } from "@webpack";
|
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";
|
import { Settings } from "Vencord";
|
||||||
|
|
||||||
const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore");
|
const UserAffinitiesStore = findStoreLazy("UserAffinitiesV2Store");
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "ImplicitRelationships",
|
name: "ImplicitRelationships",
|
||||||
|
|
@ -117,39 +117,24 @@ export default definePlugin({
|
||||||
|
|
||||||
wrapSort(comparator: Function, row: any) {
|
wrapSort(comparator: Function, row: any) {
|
||||||
return row.type === 5
|
return row.type === 5
|
||||||
? -(UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0)
|
? (UserAffinitiesStore.getUserAffinity(row.user.id)?.communicationRank ?? 0)
|
||||||
: comparator(row);
|
: 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() {
|
async fetchImplicitRelationships() {
|
||||||
// Implicit relationships are defined as users that you:
|
// Implicit relationships are defined as users that you:
|
||||||
// 1. Have an affinity for
|
// 1. Have an affinity for
|
||||||
// 2. Do not have a relationship with
|
// 2. Do not have a relationship with
|
||||||
await this.refreshUserAffinities();
|
await this.refreshUserAffinities();
|
||||||
const userAffinities: Set<string> = UserAffinitiesStore.getUserAffinitiesUserIds();
|
const userAffinities: Record<string, any>[] = UserAffinitiesStore.getUserAffinities();
|
||||||
const relationships = RelationshipStore.getRelationships();
|
const relationships = RelationshipStore.getMutableRelationships();
|
||||||
const nonFriendAffinities = Array.from(userAffinities).filter(
|
const nonFriendAffinities = userAffinities.filter(a => !RelationshipStore.getRelationshipType(a.otherUserId));
|
||||||
id => !RelationshipStore.getRelationshipType(id)
|
nonFriendAffinities.forEach(a => {
|
||||||
);
|
relationships[a.otherUserId] = 5;
|
||||||
nonFriendAffinities.forEach(id => {
|
|
||||||
relationships[id] = 5;
|
|
||||||
});
|
});
|
||||||
RelationshipStore.emitChange();
|
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 allGuildIds = Object.keys(GuildStore.getGuilds());
|
||||||
const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
|
const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
|
||||||
let count = allGuildIds.length * Math.ceil(toRequest.length / 100);
|
let count = allGuildIds.length * Math.ceil(toRequest.length / 100);
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,8 @@ export default definePlugin({
|
||||||
find: '="SYSTEM_TAG"',
|
find: '="SYSTEM_TAG"',
|
||||||
replacement: {
|
replacement: {
|
||||||
// Override colorString with our custom color and disable gradients if applying the custom color.
|
// Override colorString with our custom color and disable gradients if applying the custom color.
|
||||||
match: /&&null!=\i\.secondaryColor,(?<=colorString:(\i).+?(\i)=.+?)/,
|
match: /(?<=colorString:\i,colorStrings:\i,colorRoleName:\i}=)(\i),/,
|
||||||
replace: (m, colorString, hasGradientColors) => `${m}` +
|
replace: "$self.wrapMessageColorProps($1, arguments[0]),"
|
||||||
`vcIrcColorsDummy=[${colorString},${hasGradientColors}]=$self.getMessageColorsVariables(arguments[0],${hasGradientColors}),`
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -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 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) {
|
calculateNameColorForMessageContext(context: any) {
|
||||||
|
|
@ -108,6 +122,7 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
|
|
||||||
calculateNameColorForListContext(context: any) {
|
calculateNameColorForListContext(context: any) {
|
||||||
|
try {
|
||||||
const id = context?.user?.id;
|
const id = context?.user?.id;
|
||||||
const colorString = context?.colorString;
|
const colorString = context?.colorString;
|
||||||
const color = calculateNameColorForUser(id);
|
const color = calculateNameColorForUser(id);
|
||||||
|
|
@ -119,5 +134,8 @@ export default definePlugin({
|
||||||
return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString)
|
return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString)
|
||||||
? color
|
? color
|
||||||
: colorString;
|
: colorString;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to calculate name color for list context:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { definePluginSettings } from "@api/Settings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
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";
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
|
|
@ -89,7 +89,7 @@ export default definePlugin({
|
||||||
// Discord uses Role Mentions for uncached users because .... idk
|
// Discord uses Role Mentions for uncached users because .... idk
|
||||||
if (!roleId) return null;
|
if (!roleId) return null;
|
||||||
|
|
||||||
const role = GuildStore.getRole(guildId, roleId);
|
const role = GuildRoleStore.getRole(guildId, roleId);
|
||||||
|
|
||||||
if (!role?.icon) return <DefaultRoleIcon />;
|
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`}
|
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}),
|
}, { noop: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
function getUsernameString(username: string) {
|
function getUsernameString(username: string) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
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 MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
||||||
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||||
|
|
@ -28,6 +29,7 @@ const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||||
let isDeletePressed = false;
|
let isDeletePressed = false;
|
||||||
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
||||||
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
||||||
|
const focusChanged = () => !WindowStore.isFocused() && (isDeletePressed = false);
|
||||||
|
|
||||||
const settings = definePluginSettings({
|
const settings = definePluginSettings({
|
||||||
enableDeleteOnClick: {
|
enableDeleteOnClick: {
|
||||||
|
|
@ -62,11 +64,13 @@ export default definePlugin({
|
||||||
start() {
|
start() {
|
||||||
document.addEventListener("keydown", keydown);
|
document.addEventListener("keydown", keydown);
|
||||||
document.addEventListener("keyup", keyup);
|
document.addEventListener("keyup", keyup);
|
||||||
|
WindowStore.addChangeListener(focusChanged);
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
document.removeEventListener("keydown", keydown);
|
document.removeEventListener("keydown", keydown);
|
||||||
document.removeEventListener("keyup", keyup);
|
document.removeEventListener("keyup", keyup);
|
||||||
|
WindowStore.removeChangeListener(focusChanged);
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessageClick(msg: any, channel, event) {
|
onMessageClick(msg: any, channel, event) {
|
||||||
|
|
@ -89,9 +93,8 @@ export default definePlugin({
|
||||||
if (msg.hasFlag(EPHEMERAL)) return;
|
if (msg.hasFlag(EPHEMERAL)) return;
|
||||||
|
|
||||||
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
|
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(NoReplyMentionPlugin.name)
|
||||||
const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention")
|
? NoReplyMentionPlugin.shouldMention(msg, isShiftPress)
|
||||||
? NoReplyMention.shouldMention(msg, isShiftPress)
|
|
||||||
: !isShiftPress;
|
: !isShiftPress;
|
||||||
|
|
||||||
FluxDispatcher.dispatch({
|
FluxDispatcher.dispatch({
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
|
||||||
import { updateMessage } from "@api/MessageUpdater";
|
import { updateMessage } from "@api/MessageUpdater";
|
||||||
import { definePluginSettings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { getUserSettingLazy } from "@api/UserSettings";
|
import { getUserSettingLazy } from "@api/UserSettings";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
|
||||||
import { Devs } from "@utils/constants.js";
|
import { Devs } from "@utils/constants.js";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import { Queue } from "@utils/Queue";
|
import { Queue } from "@utils/Queue";
|
||||||
|
|
@ -295,7 +294,7 @@ function ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps):
|
||||||
<Embed
|
<Embed
|
||||||
embed={{
|
embed={{
|
||||||
rawDescription: "",
|
rawDescription: "",
|
||||||
color: "var(--background-secondary)",
|
color: "var(--background-base-lower)",
|
||||||
author: {
|
author: {
|
||||||
name: <Text variant="text-xs/medium" tag="span">
|
name: <Text variant="text-xs/medium" tag="span">
|
||||||
<span>{channelLabel} - </span>
|
<span>{channelLabel} - </span>
|
||||||
|
|
@ -373,7 +372,7 @@ export default definePlugin({
|
||||||
settings,
|
settings,
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
addMessageAccessory("messageLinkEmbed", props => {
|
addMessageAccessory("MessageLinkEmbeds", props => {
|
||||||
if (!messageLinkRegex.test(props.message.content))
|
if (!messageLinkRegex.test(props.message.content))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
|
@ -381,16 +380,14 @@ export default definePlugin({
|
||||||
messageLinkRegex.lastIndex = 0;
|
messageLinkRegex.lastIndex = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
|
||||||
<MessageEmbedAccessory
|
<MessageEmbedAccessory
|
||||||
message={props.message}
|
message={props.message}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
);
|
||||||
}, 4 /* just above rich embeds */);
|
}, 4 /* just above rich embeds */);
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
removeMessageAccessory("messageLinkEmbed");
|
removeMessageAccessory("MessageLinkEmbeds");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
.messagelogger-deleted {
|
.messagelogger-deleted {
|
||||||
--text-normal: var(--status-danger, #f04747);
|
--text-default: var(--status-danger, #f04747);
|
||||||
--interactive-normal: var(--status-danger, #f04747);
|
--interactive-normal: var(--status-danger, #f04747);
|
||||||
--text-muted: var(--status-danger, #f04747);
|
--text-muted: var(--status-danger, #f04747);
|
||||||
--embed-title: var(--red-460, #be3535);
|
--embed-title: var(--red-460, #be3535);
|
||||||
|
|
|
||||||
|
|
@ -204,5 +204,5 @@ export default definePlugin({
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})
|
}, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,9 @@ import { Devs } from "@utils/constants";
|
||||||
import { runtimeHashMessageKey } from "@utils/intlHash";
|
import { runtimeHashMessageKey } from "@utils/intlHash";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { i18n, RelationshipStore } from "@webpack/common";
|
||||||
import { i18n } from "@webpack/common";
|
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
|
|
||||||
|
|
||||||
interface MessageDeleteProps {
|
interface MessageDeleteProps {
|
||||||
// Internal intl message for BLOCKED_MESSAGE_COUNT
|
// Internal intl message for BLOCKED_MESSAGE_COUNT
|
||||||
collapsedReason: () => any;
|
collapsedReason: () => any;
|
||||||
|
|
|
||||||
|
|
@ -75,5 +75,5 @@ export default definePlugin({
|
||||||
}}> Pause Indefinitely.</a>}
|
}}> Pause Indefinitely.</a>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
}, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { copyToClipboard } from "@utils/clipboard";
|
||||||
import { getIntlMessage, getUniqueUsername } from "@utils/discord";
|
import { getIntlMessage, getUniqueUsername } from "@utils/discord";
|
||||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { findByCodeLazy } from "@webpack";
|
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 { UnicodeEmoji } from "@webpack/types";
|
||||||
import type { Guild, Role, User } from "discord-types/general";
|
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 [selectedItemIndex, selectItem] = useState(0);
|
||||||
const selectedItem = permissions[selectedItemIndex];
|
const selectedItem = permissions[selectedItemIndex];
|
||||||
|
|
||||||
const roles = GuildStore.getRoles(guild.id);
|
const roles = GuildRoleStore.getRoles(guild.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalRoot
|
<ModalRoot
|
||||||
|
|
@ -238,7 +238,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
||||||
id={cl("view-as-role")}
|
id={cl("view-as-role")}
|
||||||
label={getIntlMessage("VIEW_AS_ROLE")}
|
label={getIntlMessage("VIEW_AS_ROLE")}
|
||||||
action={() => {
|
action={() => {
|
||||||
const role = GuildStore.getRole(guild.id, roleId);
|
const role = GuildRoleStore.getRole(guild.id, roleId);
|
||||||
if (!role) return;
|
if (!role) return;
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ function UserPermissionsComponent({ guild, guildMember, closePopout }: { guild:
|
||||||
viewBox="0 96 960 960"
|
viewBox="0 96 960 960"
|
||||||
transform={permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -181,7 +181,7 @@ function UserPermissionsComponent({ guild, guildMember, closePopout }: { guild:
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export function PermissionAllowedIcon() {
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<title>Allowed</title>
|
<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>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +51,7 @@ export function PermissionDefaultIcon() {
|
||||||
>
|
>
|
||||||
<g>
|
<g>
|
||||||
<title>Not overwritten</title>
|
<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>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import { Devs } from "@utils/constants";
|
||||||
import { classes } from "@utils/misc";
|
import { classes } from "@utils/misc";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
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 type { Guild, GuildMember } from "discord-types/general";
|
||||||
|
|
||||||
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions";
|
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions";
|
||||||
|
|
@ -107,7 +107,7 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.otherwise(() => {
|
.otherwise(() => {
|
||||||
const permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
|
const permissions = Object.values(GuildRoleStore.getRoles(guild.id)).map(role => ({
|
||||||
type: PermissionType.Role,
|
type: PermissionType.Role,
|
||||||
...role
|
...role
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -140,10 +140,9 @@
|
||||||
|
|
||||||
/* copy pasted from discord cause impossible to webpack find */
|
/* copy pasted from discord cause impossible to webpack find */
|
||||||
.vc-permviewer-role-button {
|
.vc-permviewer-role-button {
|
||||||
border-radius: var(--radius-xs);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--bg-mod-faint);
|
|
||||||
color: var(--interactive-normal);
|
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 */
|
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||||
width: -moz-fit-content;
|
width: -moz-fit-content;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
|
@ -151,9 +150,8 @@
|
||||||
padding: 4px
|
padding: 4px
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-profile-theme .vc-permviewer-role-button {
|
.vc-permviewer-role-button:hover {
|
||||||
background: rgb(var(--bg-overlay-color)/var(--bg-overlay-opacity-6));
|
background-color: var(--user-profile-background-hover);
|
||||||
border-color: var(--profile-body-border-color)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-permviewer-granted-by-container {
|
.vc-permviewer-granted-by-container {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { findByPropsLazy } from "@webpack";
|
||||||
import { GuildStore } from "@webpack/common";
|
import { GuildRoleStore } from "@webpack/common";
|
||||||
import { Guild, GuildMember, Role } from "discord-types/general";
|
import { Guild, GuildMember, Role } from "discord-types/general";
|
||||||
|
|
||||||
import { PermissionsSortOrder, settings } from ".";
|
import { PermissionsSortOrder, settings } from ".";
|
||||||
|
|
@ -29,7 +29,7 @@ export const { getGuildPermissionSpecMap } = findByPropsLazy("getGuildPermission
|
||||||
export const cl = classNameFactory("vc-permviewer-");
|
export const cl = classNameFactory("vc-permviewer-");
|
||||||
|
|
||||||
export function getSortedRoles({ id }: Guild, member: GuildMember) {
|
export function getSortedRoles({ id }: Guild, member: GuildMember) {
|
||||||
const roles = GuildStore.getRoles(id);
|
const roles = GuildRoleStore.getRoles(id);
|
||||||
|
|
||||||
return [...member.roles, id]
|
return [...member.roles, id]
|
||||||
.map(id => 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) {
|
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) => {
|
return overwrites.sort((a, b) => {
|
||||||
if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0;
|
if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0;
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,11 @@
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
|
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
|
||||||
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
|
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 { DEFAULT_COLOR, SWATCHES } from "../constants";
|
||||||
import { categoryLen, createCategory, getCategory } from "../data";
|
import { categoryLen, createCategory, getCategory } from "../data";
|
||||||
|
|
||||||
interface ColorPickerProps {
|
|
||||||
color: number | null;
|
|
||||||
showEyeDropper?: boolean;
|
|
||||||
suggestedColors?: string[];
|
|
||||||
onChange(value: number | null): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ColorPickerWithSwatchesProps {
|
interface ColorPickerWithSwatchesProps {
|
||||||
defaultColor: number;
|
defaultColor: number;
|
||||||
colors: number[];
|
colors: number[];
|
||||||
|
|
@ -29,7 +22,6 @@ interface ColorPickerWithSwatchesProps {
|
||||||
renderCustomButton?: () => React.ReactNode;
|
renderCustomButton?: () => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
|
|
||||||
const ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>('id:"color-picker"');
|
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"/);
|
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/);
|
||||||
|
|
|
||||||
|
|
@ -16,20 +16,17 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePluginSettings, Settings } from "@api/Settings";
|
import { definePluginSettings } from "@api/Settings";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByPropsLazy } from "@webpack";
|
import { ChannelStore, ComponentDispatch, FluxDispatcher as Dispatcher, MessageActions, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||||
import { ChannelStore, ComponentDispatch, FluxDispatcher as Dispatcher, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
|
||||||
import { Message } from "discord-types/general";
|
import { Message } from "discord-types/general";
|
||||||
|
import NoBlockedMessagesPlugin from "plugins/noBlockedMessages";
|
||||||
const Kangaroo = findByPropsLazy("jumpToMessage");
|
import NoReplyMentionPlugin from "plugins/noReplyMention";
|
||||||
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
|
|
||||||
|
|
||||||
const isMac = navigator.platform.includes("Mac"); // bruh
|
const isMac = navigator.platform.includes("Mac"); // bruh
|
||||||
let replyIdx = -1;
|
let currentlyReplyingId: string | null = null;
|
||||||
let editIdx = -1;
|
let currentlyEditingId: string | null = null;
|
||||||
|
|
||||||
|
|
||||||
const enum MentionOptions {
|
const enum MentionOptions {
|
||||||
DISABLED,
|
DISABLED,
|
||||||
|
|
@ -69,36 +66,29 @@ export default definePlugin({
|
||||||
|
|
||||||
flux: {
|
flux: {
|
||||||
DELETE_PENDING_REPLY() {
|
DELETE_PENDING_REPLY() {
|
||||||
replyIdx = -1;
|
currentlyReplyingId = null;
|
||||||
},
|
},
|
||||||
MESSAGE_END_EDIT() {
|
MESSAGE_END_EDIT() {
|
||||||
editIdx = -1;
|
currentlyEditingId = null;
|
||||||
|
},
|
||||||
|
CHANNEL_SELECT() {
|
||||||
|
currentlyReplyingId = null;
|
||||||
|
currentlyEditingId = null;
|
||||||
},
|
},
|
||||||
MESSAGE_START_EDIT: onStartEdit,
|
MESSAGE_START_EDIT: onStartEdit,
|
||||||
CREATE_PENDING_REPLY: onCreatePendingReply
|
CREATE_PENDING_REPLY: onCreatePendingReply
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function calculateIdx(messages: Message[], id: string) {
|
function onStartEdit({ messageId, _isQuickEdit }: any) {
|
||||||
const idx = messages.findIndex(m => m.id === id);
|
|
||||||
return idx === -1
|
|
||||||
? idx
|
|
||||||
: messages.length - idx - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onStartEdit({ channelId, messageId, _isQuickEdit }: any) {
|
|
||||||
if (_isQuickEdit) return;
|
if (_isQuickEdit) return;
|
||||||
|
currentlyEditingId = messageId;
|
||||||
const meId = UserStore.getCurrentUser().id;
|
|
||||||
|
|
||||||
const messages = MessageStore.getMessages(channelId)._array.filter(m => m.author.id === meId);
|
|
||||||
editIdx = calculateIdx(messages, messageId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) {
|
function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) {
|
||||||
if (_isQuickReply) return;
|
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;
|
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 vh = Math.max(document.documentElement.clientHeight, window.innerHeight);
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
const isOffscreen = rect.bottom < 200 || rect.top - vh >= -200;
|
const isOffscreen = rect.bottom < 150 || rect.top - vh >= -150;
|
||||||
|
|
||||||
if (isOffscreen) {
|
if (isOffscreen) {
|
||||||
Kangaroo.jumpToMessage({
|
MessageActions.jumpToMessage({
|
||||||
channelId,
|
channelId,
|
||||||
messageId,
|
messageId,
|
||||||
flash: false,
|
flash: false,
|
||||||
|
|
@ -137,43 +127,48 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
|
||||||
|
|
||||||
function getNextMessage(isUp: boolean, isReply: boolean) {
|
function getNextMessage(isUp: boolean, isReply: boolean) {
|
||||||
let messages: Array<Message & { deleted?: boolean; }> = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
|
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;
|
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 => {
|
||||||
messages = messages.filter(m => !RelationshipStore.isBlocked(m.author.id));
|
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
|
return true;
|
||||||
? Math.min(messages.length - 1, i + 1)
|
});
|
||||||
: Math.max(-1, i - 1);
|
|
||||||
|
|
||||||
const findNextNonDeleted = (i: number) => {
|
const findNextNonDeleted = (id: string | null) => {
|
||||||
do {
|
if (id === null) return messages[messages.length - 1];
|
||||||
i = mutate(i);
|
|
||||||
} while (i !== -1 && messages[messages.length - i - 1]?.deleted === true);
|
const idx = messages.findIndex(m => m.id === id);
|
||||||
return i;
|
if (idx === -1) return messages[messages.length - 1];
|
||||||
|
|
||||||
|
const i = isUp ? idx - 1 : idx + 1;
|
||||||
|
return messages[i] ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
let i: number;
|
if (isReply) {
|
||||||
if (isReply)
|
const msg = findNextNonDeleted(currentlyReplyingId);
|
||||||
replyIdx = i = findNextNonDeleted(replyIdx);
|
currentlyReplyingId = msg?.id ?? null;
|
||||||
else
|
return msg;
|
||||||
editIdx = i = findNextNonDeleted(editIdx);
|
} else {
|
||||||
|
const msg = findNextNonDeleted(currentlyEditingId);
|
||||||
return i === - 1 ? undefined : messages[messages.length - i - 1];
|
currentlyEditingId = msg?.id ?? null;
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldMention(message) {
|
function shouldMention(message: Message) {
|
||||||
const { enabled, userList, shouldPingListed } = Settings.plugins.NoReplyMention;
|
|
||||||
const shouldPing = !enabled || (shouldPingListed === userList.includes(message.author.id));
|
|
||||||
|
|
||||||
switch (settings.store.shouldMention) {
|
switch (settings.store.shouldMention) {
|
||||||
case MentionOptions.NO_REPLY_MENTION_PLUGIN: return shouldPing;
|
case MentionOptions.NO_REPLY_MENTION_PLUGIN:
|
||||||
case MentionOptions.DISABLED: return false;
|
if (!Vencord.Plugins.isPluginEnabled(NoReplyMentionPlugin.name)) return true;
|
||||||
default: 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) {
|
function nextReply(isUp: boolean) {
|
||||||
const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
|
const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
|
||||||
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
||||||
|
|
||||||
const message = getNextMessage(isUp, true);
|
const message = getNextMessage(isUp, true);
|
||||||
|
|
||||||
if (!message)
|
if (!message) {
|
||||||
return void Dispatcher.dispatch({
|
return void Dispatcher.dispatch({
|
||||||
type: "DELETE_PENDING_REPLY",
|
type: "DELETE_PENDING_REPLY",
|
||||||
channelId: SelectedChannelStore.getChannelId(),
|
channelId: SelectedChannelStore.getChannelId(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const channel = ChannelStore.getChannel(message.channel_id);
|
const channel = ChannelStore.getChannel(message.channel_id);
|
||||||
const meId = UserStore.getCurrentUser().id;
|
const meId = UserStore.getCurrentUser().id;
|
||||||
|
|
||||||
|
|
@ -199,6 +197,7 @@ function nextReply(isUp: boolean) {
|
||||||
showMentionToggle: !channel.isPrivate() && message.author.id !== meId,
|
showMentionToggle: !channel.isPrivate() && message.author.id !== meId,
|
||||||
_isQuickReply: true
|
_isQuickReply: true
|
||||||
});
|
});
|
||||||
|
|
||||||
ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS");
|
ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS");
|
||||||
jumpIfOffScreen(channel.id, message.id);
|
jumpIfOffScreen(channel.id, message.id);
|
||||||
}
|
}
|
||||||
|
|
@ -209,11 +208,13 @@ function nextEdit(isUp: boolean) {
|
||||||
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
||||||
const message = getNextMessage(isUp, false);
|
const message = getNextMessage(isUp, false);
|
||||||
|
|
||||||
if (!message)
|
if (!message) {
|
||||||
return Dispatcher.dispatch({
|
return Dispatcher.dispatch({
|
||||||
type: "MESSAGE_END_EDIT",
|
type: "MESSAGE_END_EDIT",
|
||||||
channelId: SelectedChannelStore.getChannelId()
|
channelId: SelectedChannelStore.getChannelId()
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Dispatcher.dispatch({
|
Dispatcher.dispatch({
|
||||||
type: "MESSAGE_START_EDIT",
|
type: "MESSAGE_START_EDIT",
|
||||||
channelId: message.channel_id,
|
channelId: message.channel_id,
|
||||||
|
|
@ -221,5 +222,6 @@ function nextEdit(isUp: boolean) {
|
||||||
content: message.content,
|
content: message.content,
|
||||||
_isQuickEdit: true
|
_isQuickEdit: true
|
||||||
});
|
});
|
||||||
|
|
||||||
jumpIfOffScreen(message.channel_id, message.id);
|
jumpIfOffScreen(message.channel_id, message.id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ export async function syncFriends() {
|
||||||
friends.friends = [];
|
friends.friends = [];
|
||||||
friends.requests = [];
|
friends.requests = [];
|
||||||
|
|
||||||
const relationShips = RelationshipStore.getRelationships();
|
const relationShips = RelationshipStore.getMutableRelationships();
|
||||||
for (const id in relationShips) {
|
for (const id in relationShips) {
|
||||||
switch (relationShips[id]) {
|
switch (relationShips[id]) {
|
||||||
case RelationshipType.FRIEND:
|
case RelationshipType.FRIEND:
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ function makeSearchItem(src: string) {
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
height={16}
|
height={16}
|
||||||
width={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}
|
{engine}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
||||||
|
|
@ -53,14 +53,12 @@ function makeSearchItem(src: string) {
|
||||||
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
||||||
<img
|
<img
|
||||||
style={{
|
style={{
|
||||||
borderRadius: i >= 3 // Do not round Google, Yandex & SauceNAO
|
borderRadius: "50%",
|
||||||
? "50%"
|
|
||||||
: void 0
|
|
||||||
}}
|
}}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
height={16}
|
height={16}
|
||||||
width={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}
|
{engine}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { Devs } from "@utils/constants";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { findByCodeLazy } from "@webpack";
|
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"');
|
const useMessageAuthor = findByCodeLazy('"Result cannot be null because the message is not null"');
|
||||||
|
|
||||||
|
|
@ -84,8 +84,8 @@ export default definePlugin({
|
||||||
find: ".USER_MENTION)",
|
find: ".USER_MENTION)",
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
match: /(?<=onContextMenu:\i,color:)\i(?<=\.getNickname\((\i),\i,(\i).+?)/,
|
match: /(?<=user:(\i),guildId:([^,]+?),.{0,100}?children:\i=>\i)\((\i)\)/,
|
||||||
replace: "$self.getColorInt($2?.id,$1)",
|
replace: "({...$3,color:$self.getColorInt($1?.id,$2)})",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
predicate: () => settings.store.chatMentions
|
predicate: () => settings.store.chatMentions
|
||||||
|
|
@ -197,7 +197,7 @@ export default definePlugin({
|
||||||
const value = `color-mix(in oklab, ${author.colorString} ${messageSaturation}%, var({DEFAULT}))`;
|
const value = `color-mix(in oklab, ${author.colorString} ${messageSaturation}%, var({DEFAULT}))`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
color: value.replace("{DEFAULT}", "--text-normal"),
|
color: value.replace("{DEFAULT}", "--text-default"),
|
||||||
"--header-primary": value.replace("{DEFAULT}", "--header-primary"),
|
"--header-primary": value.replace("{DEFAULT}", "--header-primary"),
|
||||||
"--text-muted": value.replace("{DEFAULT}", "--text-muted")
|
"--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; }) => {
|
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 (
|
return (
|
||||||
<span style={{
|
<span style={{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
.vc-st-date-picker {
|
.vc-st-date-picker {
|
||||||
background-color: var(--input-background);
|
background-color: var(--input-background);
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
width: 95%;
|
width: 95%;
|
||||||
padding: 8px 8px 8px 12px;
|
padding: 8px 8px 8px 12px;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { classes } from "@utils/misc";
|
||||||
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import { useAwaiter } from "@utils/react";
|
import { useAwaiter } from "@utils/react";
|
||||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
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";
|
import { Guild, User } from "discord-types/general";
|
||||||
|
|
||||||
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
|
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
|
"Vanity Link": guild.vanityURLCode ? (<a>{`discord.gg/${guild.vanityURLCode}`}</a>) : "-", // Making the anchor href valid would cause Discord to reload
|
||||||
"Preferred Locale": guild.preferredLocale || "-",
|
"Preferred Locale": guild.preferredLocale || "-",
|
||||||
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
|
"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
|
"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 (
|
return (
|
||||||
|
|
@ -220,12 +220,12 @@ function FriendsTab({ guild, setCount }: RelationshipProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlockedUsersTab({ 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);
|
return UserList("blocked", guild, blockedIds, setCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function IgnoredUserTab({ guild, setCount }: RelationshipProps) {
|
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);
|
return UserList("ignored", guild, ignoredIds, setCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-gp-server-info-pair {
|
.vc-gp-server-info-pair {
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-gp-server-info-pair [class^="timestamp"] {
|
.vc-gp-server-info-pair [class^="timestamp"] {
|
||||||
|
|
|
||||||
|
|
@ -78,12 +78,12 @@ export const Highlighter = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const themeBase: ThemeBase = {
|
const themeBase: ThemeBase = {
|
||||||
plainColor: currentTheme?.fg || "var(--text-normal)",
|
plainColor: currentTheme?.fg || "var(--text-default)",
|
||||||
accentBgColor:
|
accentBgColor:
|
||||||
currentTheme?.colors?.["statusBar.background"] || (useHljs ? "#7289da" : "#007BC8"),
|
currentTheme?.colors?.["statusBar.background"] || (useHljs ? "#7289da" : "#007BC8"),
|
||||||
accentFgColor: currentTheme?.colors?.["statusBar.foreground"] || "#FFF",
|
accentFgColor: currentTheme?.colors?.["statusBar.foreground"] || "#FFF",
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
currentTheme?.colors?.["editor.background"] || "var(--background-secondary)",
|
currentTheme?.colors?.["editor.background"] || "var(--background-base-lower)",
|
||||||
};
|
};
|
||||||
|
|
||||||
let langName;
|
let langName;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
.vc-shiki-container {
|
.vc-shiki-container {
|
||||||
border: 4px;
|
border: 4px;
|
||||||
background-color: var(--background-secondary);
|
background-color: var(--background-base-lower);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-shiki-root {
|
.vc-shiki-root {
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-shc-heading-nsfw-icon {
|
.vc-shc-heading-nsfw-icon {
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-shc-topic-container {
|
.vc-shc-topic-container {
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
background: var(--bg-overlay-3, var(--background-secondary));
|
background: var(--bg-overlay-5, var(--background-base-lower));
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
max-width: 70vw;
|
max-width: 70vw;
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--bg-overlay-3, var(--background-secondary));
|
background: var(--bg-overlay-5, var(--background-base-lower));
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
margin-left: 0.75em;
|
margin-left: 0.75em;
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
.vc-shc-tags-container {
|
.vc-shc-tags-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg-overlay-3, var(--background-secondary));
|
background: var(--bg-overlay-5, var(--background-base-lower));
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
gap: 0.75em;
|
gap: 0.75em;
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--bg-overlay-3, var(--background-secondary));
|
background: var(--bg-overlay-5, var(--background-base-lower));
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
max-width: 70vw;
|
max-width: 70vw;
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-shc-allowed-users-and-roles-container-permdetails-btn {
|
.vc-shc-allowed-users-and-roles-container-permdetails-btn {
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-shc-allowed-users-and-roles-container > [class^="members"] {
|
.vc-shc-allowed-users-and-roles-container > [class^="members"] {
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,9 @@ export default definePlugin({
|
||||||
authors: [Devs.Rini, Devs.TheKodeToad],
|
authors: [Devs.Rini, Devs.TheKodeToad],
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: '"BaseUsername"',
|
find: '="SYSTEM_TAG"',
|
||||||
replacement: {
|
replacement: {
|
||||||
/* TODO: remove \i+\i once change makes it to stable */
|
match: /(?<=onContextMenu:\i,children:)\i/,
|
||||||
match: /(?<=onContextMenu:\i,children:)(?:\i\+\i|\i)/,
|
|
||||||
replace: "$self.renderUsername(arguments[0])"
|
replace: "$self.renderUsername(arguments[0])"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -86,5 +86,5 @@ export default definePlugin({
|
||||||
</TooltipContainer>
|
</TooltipContainer>
|
||||||
)}
|
)}
|
||||||
</div>;
|
</div>;
|
||||||
})
|
}, { noop: true })
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-light #vc-spotify-player {
|
.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 {
|
.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 {
|
.vc-spotify-button {
|
||||||
|
|
@ -143,7 +143,7 @@
|
||||||
|
|
||||||
#vc-spotify-progress-bar {
|
#vc-spotify-progress-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
|
@ -195,5 +195,5 @@
|
||||||
|
|
||||||
.vc-spotify-fallback {
|
.vc-spotify-fallback {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
.visual-refresh {
|
.visual-refresh {
|
||||||
#vc-spotify-player {
|
#vc-spotify-player {
|
||||||
padding: 12px;
|
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;
|
margin: 0;
|
||||||
border-top-left-radius: 10px;
|
border-top-left-radius: 10px;
|
||||||
border-top-right-radius: 10px;
|
border-top-right-radius: 10px;
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
#vc-spotify-progress-bar {
|
#vc-spotify-progress-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export function TranslationAccessory({ message }: { message: Message; }) {
|
||||||
<span className={cl("accessory")}>
|
<span className={cl("accessory")}>
|
||||||
<TranslateIcon width={16} height={16} className={cl("accessory-icon")} />
|
<TranslateIcon width={16} height={16} className={cl("accessory-icon")} />
|
||||||
{Parser.parse(translation.text)}
|
{Parser.parse(translation.text)}
|
||||||
{" "}
|
<br />
|
||||||
(translated from {translation.sourceLanguage} - <Dismiss onDismiss={() => setTranslation(undefined)} />)
|
(translated from {translation.sourceLanguage} - <Dismiss onDismiss={() => setTranslation(undefined)} />)
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
white-space: break-spaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-trans-accessory-icon {
|
.vc-trans-accessory-icon {
|
||||||
|
|
|
||||||
|
|
@ -69,26 +69,29 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s
|
||||||
|
|
||||||
const myId = UserStore.getCurrentUser()?.id;
|
const myId = UserStore.getCurrentUser()?.id;
|
||||||
|
|
||||||
const typingUsersArray = Object.keys(typingUsers).filter(id => id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers));
|
const typingUsersArray = Object.keys(typingUsers).filter(id =>
|
||||||
|
id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers)
|
||||||
|
);
|
||||||
|
const [a, b, c] = typingUsersArray;
|
||||||
let tooltipText: string;
|
let tooltipText: string;
|
||||||
|
|
||||||
switch (typingUsersArray.length) {
|
switch (typingUsersArray.length) {
|
||||||
case 0: break;
|
case 0: break;
|
||||||
case 1: {
|
case 1: {
|
||||||
tooltipText = getIntlMessage("ONE_USER_TYPING", { a: getDisplayName(guildId, typingUsersArray[0]) });
|
tooltipText = getIntlMessage("ONE_USER_TYPING", { a: getDisplayName(guildId, a) });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 2: {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case 3: {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
tooltipText = Settings.plugins.TypingTweaks.enabled
|
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");
|
: getIntlMessage("SEVERAL_USERS_TYPING");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +181,7 @@ export default definePlugin({
|
||||||
// Theads
|
// Theads
|
||||||
{
|
{
|
||||||
// This is the thread "spine" that shows in the left
|
// 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: {
|
replacement: {
|
||||||
match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/,
|
match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/,
|
||||||
replace: "$&,$self.TypingIndicator($1.id,$1.getGuildId())"
|
replace: "$&,$self.TypingIndicator($1.id,$1.getGuildId())"
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,17 @@ const settings = definePluginSettings({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function buildSeveralUsers({ a, b, count }: { a: string, b: string, count: number; }) {
|
export const buildSeveralUsers = ErrorBoundary.wrap(({ a, b, count, guildId }: { a: User, b: User, count: number; guildId: string; }) => {
|
||||||
return [
|
return (
|
||||||
<strong key="0">{a}</strong>,
|
<>
|
||||||
", ",
|
<TypingUser user={a} guildId={guildId} />
|
||||||
<strong key="1">{b}</strong>,
|
{", "}
|
||||||
`, and ${count} others are typing...`
|
<TypingUser user={b} guildId={guildId} />
|
||||||
];
|
{", "}
|
||||||
}
|
and {count} others are typing...
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, { noop: true });
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
|
|
@ -96,21 +99,23 @@ export default definePlugin({
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "#{intl::THREE_USERS_TYPING}",
|
find: "#{intl::THREE_USERS_TYPING}",
|
||||||
|
group: true,
|
||||||
replacement: [
|
replacement: [
|
||||||
{
|
{
|
||||||
// Style the indicator and add function call to modify the children before rendering
|
// 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).+?)/,
|
match: /(?<=children:\[(\i)\.length>0.{0,300}?"aria-atomic":!0,children:)\i/,
|
||||||
replace: "$self.renderTypingUsers({ users: $1, guildId: $2, children: $& })"
|
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
|
// 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: ""
|
replace: ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Adds the alternative formatting for several users typing
|
// 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.+?)/,
|
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
|
predicate: () => settings.store.alternativeFormatting
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ function VencordPopoutButton() {
|
||||||
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
|
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
|
||||||
children.splice(
|
children.splice(
|
||||||
children.length - 1, 0,
|
children.length - 1, 0,
|
||||||
<ErrorBoundary noop={true}>
|
<ErrorBoundary noop>
|
||||||
<VencordPopoutButton />
|
<VencordPopoutButton />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,14 @@ const openAvatar = (url: string) => openImage(url, 512, 512);
|
||||||
const openBanner = (url: string) => openImage(url, 1024);
|
const openBanner = (url: string) => openImage(url, 1024);
|
||||||
|
|
||||||
function openImage(url: string, width: number, height?: number) {
|
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 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.searchParams.set("size", settings.store.imgSize);
|
||||||
u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`);
|
u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`);
|
||||||
url = u.toString();
|
url = u.toString();
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import { Margins } from "@utils/margins";
|
||||||
import { copyWithToast } from "@utils/misc";
|
import { copyWithToast } from "@utils/misc";
|
||||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
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";
|
import { Message } from "discord-types/general";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -148,7 +148,7 @@ const devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id:
|
||||||
const guild = getCurrentGuild();
|
const guild = getCurrentGuild();
|
||||||
if (!guild) return;
|
if (!guild) return;
|
||||||
|
|
||||||
const role = GuildStore.getRole(guild.id, id);
|
const role = GuildRoleStore.getRole(guild.id, id);
|
||||||
if (!role) return;
|
if (!role) return;
|
||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-vmsg-preview {
|
.vc-vmsg-preview {
|
||||||
color: var(--text-normal);
|
color: var(--text-default);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background-color: var(--background-secondary);
|
background-color: var(--background-base-lower);
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ export default definePlugin({
|
||||||
replace: ";b=AS:800000;level-asymmetry-allowed=1"
|
replace: ";b=AS:800000;level-asymmetry-allowed=1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
match: "useinbandfec=1",
|
match: /;usedtx=".concat\((\i)\?"0":"1"\)/,
|
||||||
replace: "useinbandfec=1;stereo=1;sprop-stereo=1"
|
replace: '$&.concat($1?";stereo=1;sprop-stereo=1":"")'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { Devs } from "@utils/constants";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
|
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
|
||||||
import { findByCodeLazy, findLazy } from "@webpack";
|
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";
|
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
|
||||||
|
|
||||||
const ChannelTypes = findLazy(m => m.ANNOUNCEMENT_THREAD === 10);
|
const ChannelTypes = findLazy(m => m.ANNOUNCEMENT_THREAD === 10);
|
||||||
|
|
@ -267,7 +267,7 @@ export default definePlugin({
|
||||||
// color role mentions (unity styling btw lol)
|
// color role mentions (unity styling btw lol)
|
||||||
if (message.mention_roles.length > 0) {
|
if (message.mention_roles.length > 0) {
|
||||||
for (const roleId of message.mention_roles) {
|
for (const roleId of message.mention_roles) {
|
||||||
const role = GuildStore.getRole(channel.guild_id, roleId);
|
const role = GuildRoleStore.getRole(channel.guild_id, roleId);
|
||||||
if (!role) continue;
|
if (!role) continue;
|
||||||
const roleColor = role.colorString ?? `#${pingColor}`;
|
const roleColor = role.colorString ?? `#${pingColor}`;
|
||||||
finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`);
|
finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`);
|
||||||
|
|
|
||||||
|
|
@ -17,29 +17,38 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const enum IpcEvents {
|
export const enum IpcEvents {
|
||||||
QUICK_CSS_UPDATE = "VencordQuickCssUpdate",
|
OPEN_QUICKCSS = "VencordOpenQuickCss",
|
||||||
THEME_UPDATE = "VencordThemeUpdate",
|
|
||||||
GET_QUICK_CSS = "VencordGetQuickCss",
|
GET_QUICK_CSS = "VencordGetQuickCss",
|
||||||
SET_QUICK_CSS = "VencordSetQuickCss",
|
SET_QUICK_CSS = "VencordSetQuickCss",
|
||||||
UPLOAD_THEME = "VencordUploadTheme",
|
QUICK_CSS_UPDATE = "VencordQuickCssUpdate",
|
||||||
DELETE_THEME = "VencordDeleteTheme",
|
|
||||||
GET_THEMES_DIR = "VencordGetThemesDir",
|
GET_SETTINGS = "VencordGetSettings",
|
||||||
|
SET_SETTINGS = "VencordSetSettings",
|
||||||
|
|
||||||
GET_THEMES_LIST = "VencordGetThemesList",
|
GET_THEMES_LIST = "VencordGetThemesList",
|
||||||
GET_THEME_DATA = "VencordGetThemeData",
|
GET_THEME_DATA = "VencordGetThemeData",
|
||||||
GET_THEME_SYSTEM_VALUES = "VencordGetThemeSystemValues",
|
GET_THEME_SYSTEM_VALUES = "VencordGetThemeSystemValues",
|
||||||
GET_SETTINGS_DIR = "VencordGetSettingsDir",
|
UPLOAD_THEME = "VencordUploadTheme",
|
||||||
GET_SETTINGS = "VencordGetSettings",
|
DELETE_THEME = "VencordDeleteTheme",
|
||||||
SET_SETTINGS = "VencordSetSettings",
|
THEME_UPDATE = "VencordThemeUpdate",
|
||||||
|
|
||||||
OPEN_EXTERNAL = "VencordOpenExternal",
|
OPEN_EXTERNAL = "VencordOpenExternal",
|
||||||
OPEN_QUICKCSS = "VencordOpenQuickCss",
|
OPEN_THEMES_FOLDER = "VencordOpenThemesFolder",
|
||||||
|
OPEN_SETTINGS_FOLDER = "VencordOpenSettingsFolder",
|
||||||
|
|
||||||
GET_UPDATES = "VencordGetUpdates",
|
GET_UPDATES = "VencordGetUpdates",
|
||||||
GET_REPO = "VencordGetRepo",
|
GET_REPO = "VencordGetRepo",
|
||||||
UPDATE = "VencordUpdate",
|
UPDATE = "VencordUpdate",
|
||||||
BUILD = "VencordBuild",
|
BUILD = "VencordBuild",
|
||||||
|
|
||||||
OPEN_MONACO_EDITOR = "VencordOpenMonacoEditor",
|
OPEN_MONACO_EDITOR = "VencordOpenMonacoEditor",
|
||||||
|
|
||||||
GET_PLUGIN_IPC_METHOD_MAP = "VencordGetPluginIpcMethodMap",
|
GET_PLUGIN_IPC_METHOD_MAP = "VencordGetPluginIpcMethodMap",
|
||||||
|
|
||||||
OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect",
|
OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect",
|
||||||
VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording",
|
VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording",
|
||||||
|
|
||||||
|
CSP_IS_DOMAIN_ALLOWED = "VencordCspIsDomainAllowed",
|
||||||
|
CSP_REMOVE_OVERRIDE = "VencordCspRemoveOverride",
|
||||||
|
CSP_REQUEST_ADD_OVERRIDE = "VencordCspRequestAddOverride",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ export class SettingsStore<T extends object> {
|
||||||
// So, we need to extract the top-level setting path (plugins.pluginName.settingName),
|
// 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"]),
|
// to be able to notify globalListeners and top-level setting name listeners (let { settingName } = settings.use(["settingName"]),
|
||||||
// with the new value
|
// 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 settingPath = paths.slice(0, 3);
|
||||||
const settingPathStr = settingPath.join(".");
|
const settingPathStr = settingPath.join(".");
|
||||||
const settingValue = settingPath.reduce((acc, curr) => acc[curr], root);
|
const settingValue = settingPath.reduce((acc, curr) => acc[curr], root);
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,40 @@
|
||||||
import * as DataStore from "@api/DataStore";
|
import * as DataStore from "@api/DataStore";
|
||||||
import { showNotification } from "@api/Notifications";
|
import { showNotification } from "@api/Notifications";
|
||||||
import { Settings } from "@api/Settings";
|
import { Settings } from "@api/Settings";
|
||||||
import { OAuth2AuthorizeModal, UserStore } from "@webpack/common";
|
import { Alerts, OAuth2AuthorizeModal, UserStore } from "@webpack/common";
|
||||||
|
|
||||||
import { Logger } from "./Logger";
|
import { Logger } from "./Logger";
|
||||||
import { openModal } from "./modal";
|
import { openModal } from "./modal";
|
||||||
|
import { relaunch } from "./native";
|
||||||
|
|
||||||
export const cloudLogger = new Logger("Cloud", "#39b7e0");
|
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 getUserId = () => {
|
||||||
const id = UserStore.getCurrentUser()?.id;
|
const id = UserStore.getCurrentUser()?.id;
|
||||||
if (!id) throw new Error("User not yet logged in");
|
if (!id) throw new Error("User not yet logged in");
|
||||||
|
|
@ -37,7 +62,7 @@ const getUserId = () => {
|
||||||
export async function getAuthorization() {
|
export async function getAuthorization() {
|
||||||
const secrets = await DataStore.get<Record<string, string>>("Vencord_cloudSecret") ?? {};
|
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
|
// we need to migrate from the old format here
|
||||||
if (secrets[origin]) {
|
if (secrets[origin]) {
|
||||||
|
|
@ -59,7 +84,7 @@ export async function getAuthorization() {
|
||||||
async function setAuthorization(secret: string) {
|
async function setAuthorization(secret: string) {
|
||||||
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
||||||
secrets ??= {};
|
secrets ??= {};
|
||||||
secrets[`${cloudUrlOrigin()}:${getUserId()}`] = secret;
|
secrets[`${getCloudUrlOrigin()}:${getUserId()}`] = secret;
|
||||||
return secrets;
|
return secrets;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +92,7 @@ async function setAuthorization(secret: string) {
|
||||||
export async function deauthorizeCloud() {
|
export async function deauthorizeCloud() {
|
||||||
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
||||||
secrets ??= {};
|
secrets ??= {};
|
||||||
delete secrets[`${cloudUrlOrigin()}:${getUserId()}`];
|
delete secrets[`${getCloudUrlOrigin()}:${getUserId()}`];
|
||||||
return secrets;
|
return secrets;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +103,8 @@ export async function authorizeCloud() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await checkCloudUrlCsp()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const oauthConfiguration = await fetch(new URL("/v1/oauth/settings", getCloudUrl()));
|
const oauthConfiguration = await fetch(new URL("/v1/oauth/settings", getCloudUrl()));
|
||||||
var { clientId, redirectUri } = await oauthConfiguration.json();
|
var { clientId, redirectUri } = await oauthConfiguration.json();
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export interface Dev {
|
||||||
*/
|
*/
|
||||||
export const Devs = /* #__PURE__*/ Object.freeze({
|
export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
Ven: {
|
Ven: {
|
||||||
name: "Vee",
|
name: "V",
|
||||||
id: 343383572805058560n
|
id: 343383572805058560n
|
||||||
},
|
},
|
||||||
Arjix: {
|
Arjix: {
|
||||||
|
|
@ -194,7 +194,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
},
|
},
|
||||||
axyie: {
|
axyie: {
|
||||||
name: "'ax",
|
name: "'ax",
|
||||||
id: 273562710745284628n,
|
id: 929877747151548487n,
|
||||||
},
|
},
|
||||||
pointy: {
|
pointy: {
|
||||||
name: "pointy",
|
name: "pointy",
|
||||||
|
|
@ -587,7 +587,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||||
},
|
},
|
||||||
samsam: {
|
samsam: {
|
||||||
name: "samsam",
|
name: "samsam",
|
||||||
id: 836452332387565589n,
|
id: 400482410279469056n,
|
||||||
},
|
},
|
||||||
Cootshk: {
|
Cootshk: {
|
||||||
name: "Cootshk",
|
name: "Cootshk",
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue