Merge branch 'hive-up-2' into hive
upstream fixes
This commit is contained in:
commit
826e774af1
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
|
||||
|
||||

|
||||
[](https://codeberg.org/Vee/cord)
|
||||
|
||||
The cutest Discord client mod
|
||||
|
||||
|  |
|
||||
| :--------------------------------------------------------------------------------------------------: |
|
||||
| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) |
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Super easy to install (Download Installer, open, click install button, done)
|
||||
- 100+ plugins built in: [See a list](https://vencord.dev/plugins)
|
||||
- Some highlights: SpotifyControls, MessageLogger, Experiments, GameActivityToggle, Translate, NoTrack, QuickReply, Free Emotes/Stickers, PermissionsViewer, CustomCommands, ShowHiddenChannels, PronounDB
|
||||
- Easy to install
|
||||
- [100+ built in plugins](https://vencord.dev/plugins)
|
||||
- Fairly lightweight despite the many inbuilt plugins
|
||||
- Excellent Browser Support: Run Vencord in your Browser via extension or UserScript
|
||||
- Works on any Discord branch: Stable, Canary or PTB all work (though for the best experience I recommend stable!)
|
||||
- Works on any Discord branch: Stable, Canary or PTB all work
|
||||
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
|
||||
- Privacy friendly, blocks Discord analytics & crash reporting out of the box and has no telemetry
|
||||
- Privacy friendly: blocks Discord analytics & crash reporting out of the box and has no telemetry
|
||||
- Maintained very actively, broken plugins are usually fixed within 12 hours
|
||||
- Settings sync: Keep your plugins and their settings synchronised between devices / apps (optional)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,16 +20,13 @@
|
|||
/// <reference path="../src/globals.d.ts" />
|
||||
|
||||
import monacoHtmlLocal from "file://monacoWin.html?minify";
|
||||
import monacoHtmlCdn from "file://../src/main/monacoWin.html?minify";
|
||||
import * as DataStore from "../src/api/DataStore";
|
||||
import { debounce } from "../src/utils";
|
||||
import { debounce, localStorage } from "../src/utils";
|
||||
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
|
||||
import { getTheme, Theme } from "../src/utils/discord";
|
||||
import { getThemeInfo } from "../src/main/themes";
|
||||
import { Settings } from "../src/Vencord";
|
||||
|
||||
// Discord deletes this so need to store in variable
|
||||
const { localStorage } = window;
|
||||
import { getStylusWebStoreUrl } from "@utils/web";
|
||||
|
||||
// listeners for ipc.on
|
||||
const cssListeners = new Set<(css: string) => void>();
|
||||
|
|
@ -45,12 +42,13 @@ window.VencordNative = {
|
|||
themes: {
|
||||
uploadTheme: (fileName: string, fileData: string) => DataStore.set(fileName, fileData, themeStore),
|
||||
deleteTheme: (fileName: string) => DataStore.del(fileName, themeStore),
|
||||
getThemesDir: async () => "",
|
||||
getThemesList: () => DataStore.entries(themeStore).then(entries =>
|
||||
entries.map(([name, css]) => getThemeInfo(css, name.toString()))
|
||||
),
|
||||
getThemeData: (fileName: string) => DataStore.get(fileName, themeStore),
|
||||
getSystemValues: async () => ({}),
|
||||
|
||||
openFolder: async () => Promise.reject("themes:openFolder is not supported on web"),
|
||||
},
|
||||
|
||||
native: {
|
||||
|
|
@ -77,6 +75,14 @@ window.VencordNative = {
|
|||
addThemeChangeListener: NOOP,
|
||||
openFile: NOOP_ASYNC,
|
||||
async openEditor() {
|
||||
if (IS_USERSCRIPT) {
|
||||
const shouldOpenWebStore = confirm("QuickCSS is not supported on the Userscript. You can instead use the Stylus extension.\n\nDo you want to open the Stylus web store page?");
|
||||
if (shouldOpenWebStore) {
|
||||
window.open(getStylusWebStoreUrl(), "_blank");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const features = `popup,width=${Math.min(window.innerWidth, 1000)},height=${Math.min(window.innerHeight, 1000)}`;
|
||||
const win = open("about:blank", "VencordQuickCss", features);
|
||||
if (!win) {
|
||||
|
|
@ -92,7 +98,7 @@ window.VencordNative = {
|
|||
? "vs-light"
|
||||
: "vs-dark";
|
||||
|
||||
win.document.write(IS_EXTENSION ? monacoHtmlLocal : monacoHtmlCdn);
|
||||
win.document.write(monacoHtmlLocal);
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -106,8 +112,9 @@ window.VencordNative = {
|
|||
}
|
||||
},
|
||||
set: async (s: Settings) => localStorage.setItem("VencordSettings", JSON.stringify(s)),
|
||||
getSettingsDir: async () => "LocalStorage"
|
||||
openFolder: async () => Promise.reject("settings:openFolder is not supported on web"),
|
||||
},
|
||||
|
||||
pluginHelpers: {} as any,
|
||||
csp: {} as any,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.12.2",
|
||||
"version": "1.12.5",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const defines = stringifyValues({
|
|||
IS_UPDATER_DISABLED,
|
||||
IS_WEB: false,
|
||||
IS_EXTENSION: false,
|
||||
IS_USERSCRIPT: false,
|
||||
VERSION,
|
||||
BUILD_TIMESTAMP
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const commonOptions = {
|
|||
define: stringifyValues({
|
||||
IS_WEB: true,
|
||||
IS_EXTENSION: false,
|
||||
IS_USERSCRIPT: false,
|
||||
IS_STANDALONE: true,
|
||||
IS_DEV,
|
||||
IS_REPORTER,
|
||||
|
|
@ -98,6 +99,7 @@ const buildConfigs = [
|
|||
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
|
||||
define: {
|
||||
...commonOptions.define,
|
||||
IS_USERSCRIPT: "true",
|
||||
window: "unsafeWindow",
|
||||
},
|
||||
outfile: "dist/Vencord.user.js",
|
||||
|
|
|
|||
|
|
@ -134,7 +134,11 @@ async function init() {
|
|||
|
||||
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
||||
runUpdateCheck();
|
||||
setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes
|
||||
|
||||
// this tends to get really annoying, so only do this if the user has auto-update without notification enabled
|
||||
if (Settings.autoUpdate && !Settings.autoUpdateNotification) {
|
||||
setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes
|
||||
}
|
||||
}
|
||||
|
||||
if (IS_DEV) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import type { Settings } from "@api/Settings";
|
||||
import { CspRequestResult } from "@main/csp/manager";
|
||||
import { PluginIpcMappings } from "@main/ipcPlugins";
|
||||
import type { UserThemeHeader } from "@main/themes";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
|
|
@ -33,10 +34,11 @@ export default {
|
|||
themes: {
|
||||
uploadTheme: (fileName: string, fileData: string) => invoke<void>(IpcEvents.UPLOAD_THEME, fileName, fileData),
|
||||
deleteTheme: (fileName: string) => invoke<void>(IpcEvents.DELETE_THEME, fileName),
|
||||
getThemesDir: () => invoke<string>(IpcEvents.GET_THEMES_DIR),
|
||||
getThemesList: () => invoke<Array<UserThemeHeader>>(IpcEvents.GET_THEMES_LIST),
|
||||
getThemeData: (fileName: string) => invoke<string | undefined>(IpcEvents.GET_THEME_DATA, fileName),
|
||||
getSystemValues: () => invoke<Record<string, string>>(IpcEvents.GET_THEME_SYSTEM_VALUES),
|
||||
|
||||
openFolder: () => invoke<void>(IpcEvents.OPEN_THEMES_FOLDER),
|
||||
},
|
||||
|
||||
updater: {
|
||||
|
|
@ -49,7 +51,8 @@ export default {
|
|||
settings: {
|
||||
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
|
||||
set: (settings: Settings, pathToNotify?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, pathToNotify),
|
||||
getSettingsDir: () => invoke<string>(IpcEvents.GET_SETTINGS_DIR),
|
||||
|
||||
openFolder: () => invoke<void>(IpcEvents.OPEN_SETTINGS_FOLDER),
|
||||
},
|
||||
|
||||
quickCss: {
|
||||
|
|
@ -73,5 +76,17 @@ export default {
|
|||
openExternal: (url: string) => invoke<void>(IpcEvents.OPEN_EXTERNAL, url)
|
||||
},
|
||||
|
||||
csp: {
|
||||
/**
|
||||
* Note: Only supports full explicit matches, not wildcards.
|
||||
*
|
||||
* If `*.example.com` is allowed, `isDomainAllowed("https://sub.example.com")` will return false.
|
||||
*/
|
||||
isDomainAllowed: (url: string, directives: string[]) => invoke<boolean>(IpcEvents.CSP_IS_DOMAIN_ALLOWED, url, directives),
|
||||
removeOverride: (url: string) => invoke<boolean>(IpcEvents.CSP_REMOVE_OVERRIDE, url),
|
||||
requestAddOverride: (url: string, directives: string[], callerName: string) =>
|
||||
invoke<CspRequestResult>(IpcEvents.CSP_REQUEST_ADD_OVERRIDE, url, directives, callerName),
|
||||
},
|
||||
|
||||
pluginHelpers: PluginHelpers
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function _modifyAccessories(
|
|||
) {
|
||||
for (const [key, accessory] of accessories.entries()) {
|
||||
const res = (
|
||||
<ErrorBoundary message={`Failed to render ${key} Message Accessory`} key={key}>
|
||||
<ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}>
|
||||
<accessory.render {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
all: unset;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-secondary-alt);
|
||||
color: var(--text-default);
|
||||
background-color: var(--background-base-lower-alt);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
}
|
||||
|
||||
.visual-refresh .vc-notification-root {
|
||||
background-color: var(--bg-overlay-floating, var(--background-base-low));
|
||||
background-color: var(--background-base-low);
|
||||
}
|
||||
|
||||
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
|
||||
|
|
|
|||
|
|
@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => {
|
|||
logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
|
||||
}
|
||||
|
||||
get isNoop() {
|
||||
if (IS_DEV) return false;
|
||||
return this.props.noop;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error === NO_ERROR) return this.props.children;
|
||||
|
||||
if (this.props.noop) return null;
|
||||
if (this.isNoop) return null;
|
||||
|
||||
if (this.props.fallback)
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -3,5 +3,9 @@
|
|||
background-color: #e7828430;
|
||||
border: 1px solid #e78284;
|
||||
border-radius: 5px;
|
||||
color: var(--text-normal, white);
|
||||
color: var(--text-default, white);
|
||||
|
||||
& a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ export function Link(props: React.PropsWithChildren<Props>) {
|
|||
props.style.pointerEvents = "none";
|
||||
props["aria-disabled"] = true;
|
||||
}
|
||||
|
||||
props.rel ??= "noreferrer";
|
||||
|
||||
return (
|
||||
<a role="link" target="_blank" {...props}>
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
{!!plugin.settingsAboutComponent && (
|
||||
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
|
||||
<Forms.FormSection>
|
||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
|
||||
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
||||
</ErrorBoundary>
|
||||
</Forms.FormSection>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
padding: 1em;
|
||||
background: var(--info-warning-background);
|
||||
border: 1px solid var(--info-warning-foreground);
|
||||
color: var(--info-warning-text);
|
||||
color: var(--info-warning-foreground);
|
||||
}
|
||||
|
||||
.vc-plugins-restart-button {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { Settings, useSettings } from "@api/Settings";
|
|||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { Grid } from "@components/Grid";
|
||||
import { Link } from "@components/Link";
|
||||
import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
||||
import { authorizeCloud, checkCloudUrlCsp, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
|
||||
import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
|
||||
|
|
@ -38,6 +38,8 @@ function validateUrl(url: string) {
|
|||
}
|
||||
|
||||
async function eraseAllData() {
|
||||
if (!await checkCloudUrlCsp()) return;
|
||||
|
||||
const res = await fetch(new URL("/v1/", getCloudUrl()), {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: await getCloudAuth() }
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError
|
|||
)}
|
||||
|
||||
{compileResult &&
|
||||
<Forms.FormText style={{ color: compileResult[0] ? "var(--text-positive)" : "var(--text-danger)" }}>
|
||||
<Forms.FormText style={{ color: compileResult[0] ? "var(--status-positive)" : "var(--text-danger)" }}>
|
||||
{compileResult[1]}
|
||||
</Forms.FormText>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,17 +18,21 @@
|
|||
|
||||
import { Settings, useSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
|
||||
import { Link } from "@components/Link";
|
||||
import { openPluginModal } from "@components/PluginSettings/PluginModal";
|
||||
import type { UserThemeHeader } from "@main/themes";
|
||||
import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations";
|
||||
import { openInviteModal } from "@utils/discord";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { classes } from "@utils/misc";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { useAwaiter, useForceUpdater } from "@utils/react";
|
||||
import { getStylusWebStoreUrl } from "@utils/web";
|
||||
import { findLazy } from "@webpack";
|
||||
import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||
import { Alerts, Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
|
||||
import type { ComponentType, Ref, SyntheticEvent } from "react";
|
||||
|
||||
import Plugins from "~plugins";
|
||||
|
|
@ -65,7 +69,7 @@ function Validator({ link }: { link: string; }) {
|
|||
: "Valid!";
|
||||
|
||||
return <Forms.FormText style={{
|
||||
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--text-positive)"
|
||||
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--status-positive)"
|
||||
}}>{text}</Forms.FormText>;
|
||||
}
|
||||
|
||||
|
|
@ -159,7 +163,6 @@ function ThemesTab() {
|
|||
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
|
||||
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
|
||||
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
|
||||
const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir);
|
||||
|
||||
useEffect(() => {
|
||||
refreshLocalThemes();
|
||||
|
|
@ -219,6 +222,12 @@ function ThemesTab() {
|
|||
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
|
||||
</Card>
|
||||
|
||||
<Card className="vc-settings-card">
|
||||
<Forms.FormTitle tag="h5">External Resources</Forms.FormTitle>
|
||||
<Forms.FormText>For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked.</Forms.FormText>
|
||||
<Forms.FormText>Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts.</Forms.FormText>
|
||||
</Card>
|
||||
|
||||
<Forms.FormSection title="Local Themes">
|
||||
<QuickActionCard>
|
||||
<>
|
||||
|
|
@ -241,8 +250,7 @@ function ThemesTab() {
|
|||
) : (
|
||||
<QuickAction
|
||||
text="Open Themes Folder"
|
||||
action={() => showItemInFolder(themeDir!)}
|
||||
disabled={themeDirPending}
|
||||
action={() => VencordNative.themes.openFolder()}
|
||||
Icon={FolderIcon}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -347,10 +355,99 @@ function ThemesTab() {
|
|||
</TabBar.Item>
|
||||
</TabBar>
|
||||
|
||||
<CspErrorCard />
|
||||
{currentTab === ThemeTab.LOCAL && renderLocalThemes()}
|
||||
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
|
||||
</SettingsTab>
|
||||
);
|
||||
}
|
||||
|
||||
export default wrapTab(ThemesTab, "Themes");
|
||||
export function CspErrorCard() {
|
||||
if (IS_WEB) return null;
|
||||
|
||||
const errors = useCspErrors();
|
||||
const forceUpdate = useForceUpdater();
|
||||
|
||||
if (!errors.length) return null;
|
||||
|
||||
const isImgurHtmlDomain = (url: string) => url.startsWith("https://imgur.com/");
|
||||
|
||||
const allowUrl = async (url: string) => {
|
||||
const { origin: baseUrl, host } = new URL(url);
|
||||
|
||||
const result = await VencordNative.csp.requestAddOverride(baseUrl, ["connect-src", "img-src", "style-src", "font-src"], "Vencord Themes");
|
||||
if (result !== "ok") return;
|
||||
|
||||
CspBlockedUrls.forEach(url => {
|
||||
if (new URL(url).host === host) {
|
||||
CspBlockedUrls.delete(url);
|
||||
}
|
||||
});
|
||||
|
||||
forceUpdate();
|
||||
|
||||
Alerts.show({
|
||||
title: "Restart Required",
|
||||
body: "A restart is required to apply this change",
|
||||
confirmText: "Restart now",
|
||||
cancelText: "Later!",
|
||||
onConfirm: relaunch
|
||||
});
|
||||
};
|
||||
|
||||
const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);
|
||||
|
||||
return (
|
||||
<ErrorCard className="vc-settings-card">
|
||||
<Forms.FormTitle tag="h5">Blocked Resources</Forms.FormTitle>
|
||||
<Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>
|
||||
<Forms.FormText>It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.</Forms.FormText>
|
||||
<Forms.FormText>
|
||||
After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} to apply the change.
|
||||
</Forms.FormText>
|
||||
|
||||
<Forms.FormTitle tag="h5" className={classes(Margins.top16, Margins.bottom8)}>Blocked URLs</Forms.FormTitle>
|
||||
<div className="vc-settings-csp-list">
|
||||
{errors.map((url, i) => (
|
||||
<div key={url}>
|
||||
{i !== 0 && <Forms.FormDivider className={Margins.bottom8} />}
|
||||
<div className="vc-settings-csp-row">
|
||||
<Link href={url}>{url}</Link>
|
||||
<Button color={Button.Colors.PRIMARY} onClick={() => allowUrl(url)} disabled={isImgurHtmlDomain(url)}>
|
||||
Allow
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{hasImgurHtmlDomain && (
|
||||
<>
|
||||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom16)} />
|
||||
<Forms.FormText>
|
||||
Imgur links should be direct links in the form of <code>https://i.imgur.com/...</code>
|
||||
</Forms.FormText>
|
||||
<Forms.FormText>To obtain a direct link, right-click the image and select "Copy image address".</Forms.FormText>
|
||||
</>
|
||||
)}
|
||||
</ErrorCard>
|
||||
);
|
||||
}
|
||||
|
||||
function UserscriptThemesTab() {
|
||||
return (
|
||||
<SettingsTab title="Themes">
|
||||
<Card className="vc-settings-card">
|
||||
<Forms.FormTitle tag="h5">Themes are not supported on the Userscript!</Forms.FormTitle>
|
||||
|
||||
<Forms.FormText>
|
||||
You can instead install themes with the <Link href={getStylusWebStoreUrl()}>Stylus extension</Link>!
|
||||
</Forms.FormText>
|
||||
</Card>
|
||||
</SettingsTab>
|
||||
);
|
||||
}
|
||||
|
||||
export default IS_USERSCRIPT
|
||||
? wrapTab(UserscriptThemesTab, "Themes")
|
||||
: wrapTab(ThemesTab, "Themes");
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof
|
|||
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
|
||||
<span style={{
|
||||
marginLeft: "0.5em",
|
||||
color: "var(--text-normal)"
|
||||
color: "var(--text-default)"
|
||||
}}>{message} - {author}</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@ import { gitRemote } from "@shared/vencordUserAgent";
|
|||
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { identity, isPluginDev } from "@utils/misc";
|
||||
import { relaunch, showItemInFolder } from "@utils/native";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
|
||||
|
||||
import BadgeAPI from "../../plugins/_api/badges";
|
||||
|
|
@ -53,9 +52,6 @@ type KeysOfType<Object, Type> = {
|
|||
}[keyof Object];
|
||||
|
||||
function VencordSettings() {
|
||||
const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, {
|
||||
fallbackValue: "Loading..."
|
||||
});
|
||||
const settings = useSettings();
|
||||
|
||||
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
|
||||
|
|
@ -171,7 +167,7 @@ function VencordSettings() {
|
|||
<QuickAction
|
||||
Icon={FolderIcon}
|
||||
text="Open Settings Folder"
|
||||
action={() => showItemInFolder(settingsDir)}
|
||||
action={() => VencordNative.settings.openFolder()}
|
||||
/>
|
||||
)}
|
||||
<QuickAction
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.vc-addon-card {
|
||||
background-color: var(--background-secondary-alt);
|
||||
background-color: var(--background-base-lower-alt);
|
||||
color: var(--interactive-active);
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
.vc-settings-quickActions-pill {
|
||||
all: unset;
|
||||
background: var(--background-secondary);
|
||||
background: var(--background-base-lower);
|
||||
color: var(--header-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
}
|
||||
|
||||
.vc-settings-quickActions-pill:hover {
|
||||
background: var(--background-secondary-alt);
|
||||
background: var(--background-base-lower-alt);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--elevation-high);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@
|
|||
.vc-backup-restore-card {
|
||||
background-color: var(--info-warning-background);
|
||||
border-color: var(--info-warning-foreground);
|
||||
color: var(--info-warning-text);
|
||||
color: var(--info-warning-foreground);
|
||||
}
|
||||
|
||||
.vc-settings-theme-links {
|
||||
/* Needed to fix bad themes that hide certain textarea elements for whatever eldritch reason */
|
||||
display: inline-block !important;
|
||||
color: var(--text-normal) !important;
|
||||
color: var(--text-default) !important;
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--background-modifier-accent);
|
||||
max-height: unset;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
.vc-settings-theme-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-secondary-alt);
|
||||
background-color: var(--background-base-lower-alt);
|
||||
color: var(--interactive-active);
|
||||
border-radius: 8px;
|
||||
padding: 1em;
|
||||
|
|
@ -27,3 +27,26 @@
|
|||
.vc-settings-theme-author::before {
|
||||
content: "by ";
|
||||
}
|
||||
|
||||
.vc-settings-csp-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vc-settings-csp-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
|
||||
& a {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
--custom-button-button-md-height: 26px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
}
|
||||
|
||||
.vc-owner-crown-icon {
|
||||
color: var(--text-warning);
|
||||
color: var(--status-warning);
|
||||
}
|
||||
|
||||
.vc-heart-icon {
|
||||
|
|
|
|||
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
|
|
@ -29,11 +29,12 @@ declare global {
|
|||
* replace: "IS_WEB?foo:bar"
|
||||
* // GOOD
|
||||
* replace: IS_WEB ? "foo" : "bar"
|
||||
* // also good
|
||||
* // also okay
|
||||
* replace: `${IS_WEB}?foo:bar`
|
||||
*/
|
||||
export var IS_WEB: boolean;
|
||||
export var IS_EXTENSION: boolean;
|
||||
export var IS_USERSCRIPT: boolean;
|
||||
export var IS_STANDALONE: boolean;
|
||||
export var IS_UPDATER_DISABLED: boolean;
|
||||
export var IS_DEV: boolean;
|
||||
|
|
|
|||
154
src/main/csp/index.ts
Normal file
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/>.
|
||||
*/
|
||||
|
||||
import { app, protocol, session } from "electron";
|
||||
import { app, net, protocol } from "electron";
|
||||
import { join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
import { initCsp } from "./csp";
|
||||
import { ensureSafePath } from "./ipcMain";
|
||||
import { RendererSettings } from "./settings";
|
||||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||
|
|
@ -26,21 +28,27 @@ import { installExt } from "./utils/extensions";
|
|||
|
||||
if (IS_VESKTOP || !IS_VANILLA) {
|
||||
app.whenReady().then(() => {
|
||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
||||
// from a string I don't think any other form of sourcemaps would work
|
||||
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
||||
let url = unsafeUrl.slice("vencord://".length);
|
||||
protocol.handle("vencord", ({ url: unsafeUrl }) => {
|
||||
let url = decodeURI(unsafeUrl).slice("vencord://".length).replace(/\?v=\d+$/, "");
|
||||
|
||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||
|
||||
if (url.startsWith("/themes/")) {
|
||||
const theme = url.slice("/themes/".length);
|
||||
|
||||
const safeUrl = ensureSafePath(THEMES_DIR, theme);
|
||||
if (!safeUrl) {
|
||||
cb({ statusCode: 403 });
|
||||
return;
|
||||
return new Response(null, {
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
cb(safeUrl.replace(/\?v=\d+$/, ""));
|
||||
return;
|
||||
|
||||
return net.fetch(pathToFileURL(safeUrl).toString());
|
||||
}
|
||||
|
||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
||||
// from a string I don't think any other form of sourcemaps would work
|
||||
|
||||
switch (url) {
|
||||
case "renderer.js.map":
|
||||
case "vencordDesktopRenderer.js.map":
|
||||
|
|
@ -48,10 +56,11 @@ if (IS_VESKTOP || !IS_VANILLA) {
|
|||
case "vencordDesktopPreload.js.map":
|
||||
case "patcher.js.map":
|
||||
case "vencordDesktopMain.js.map":
|
||||
cb(join(__dirname, url));
|
||||
break;
|
||||
return net.fetch(pathToFileURL(join(__dirname, url)).toString());
|
||||
default:
|
||||
cb({ statusCode: 403 });
|
||||
return new Response(null, {
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -63,70 +72,7 @@ if (IS_VESKTOP || !IS_VANILLA) {
|
|||
} catch { }
|
||||
|
||||
|
||||
const findHeader = (headers: Record<string, string[]>, headerName: Lowercase<string>) => {
|
||||
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
|
||||
};
|
||||
|
||||
// Remove CSP
|
||||
type PolicyResult = Record<string, string[]>;
|
||||
|
||||
const parsePolicy = (policy: string): PolicyResult => {
|
||||
const result: PolicyResult = {};
|
||||
policy.split(";").forEach(directive => {
|
||||
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
|
||||
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
|
||||
result[directiveKey] = directiveValue;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
const stringifyPolicy = (policy: PolicyResult): string =>
|
||||
Object.entries(policy)
|
||||
.filter(([, values]) => values?.length)
|
||||
.map(directive => directive.flat().join(" "))
|
||||
.join("; ");
|
||||
|
||||
const patchCsp = (headers: Record<string, string[]>) => {
|
||||
const header = findHeader(headers, "content-security-policy");
|
||||
|
||||
if (header) {
|
||||
const csp = parsePolicy(headers[header][0]);
|
||||
|
||||
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
|
||||
csp[directive] ??= [];
|
||||
csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'");
|
||||
}
|
||||
|
||||
// TODO: Restrict this to only imported packages with fixed version.
|
||||
// Perhaps auto generate with esbuild
|
||||
csp["script-src"] ??= [];
|
||||
csp["script-src"].push("'unsafe-eval'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com");
|
||||
headers[header] = [stringifyPolicy(csp)];
|
||||
}
|
||||
};
|
||||
|
||||
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
||||
if (responseHeaders) {
|
||||
if (resourceType === "mainFrame")
|
||||
patchCsp(responseHeaders);
|
||||
|
||||
// Fix hosts that don't properly set the css content type, such as
|
||||
// raw.githubusercontent.com
|
||||
if (resourceType === "stylesheet") {
|
||||
const header = findHeader(responseHeaders, "content-type");
|
||||
if (header)
|
||||
responseHeaders[header] = ["text/css"];
|
||||
}
|
||||
}
|
||||
|
||||
cb({ cancel: false, responseHeaders });
|
||||
});
|
||||
|
||||
// assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones.
|
||||
// For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it
|
||||
// impossible to load css from github raw despite our fix above
|
||||
session.defaultSession.webRequest.onHeadersReceived = () => { };
|
||||
initCsp();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,14 +28,17 @@ import { FSWatcher, mkdirSync, watch, writeFileSync } from "fs";
|
|||
import { open, readdir, readFile } from "fs/promises";
|
||||
import { join, normalize } from "path";
|
||||
|
||||
import { registerCspIpcHandlers } from "./csp/manager";
|
||||
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
|
||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, THEMES_DIR } from "./utils/constants";
|
||||
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants";
|
||||
import { makeLinksOpenExternally } from "./utils/externalLinks";
|
||||
|
||||
mkdirSync(THEMES_DIR, { recursive: true });
|
||||
|
||||
registerCspIpcHandlers();
|
||||
|
||||
export function ensureSafePath(basePath: string, path: string) {
|
||||
const normalizedBasePath = normalize(basePath);
|
||||
const normalizedBasePath = normalize(basePath + "/");
|
||||
const newPath = join(basePath, path);
|
||||
const normalizedPath = normalize(newPath);
|
||||
return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null;
|
||||
|
|
@ -89,7 +92,6 @@ ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
|
|||
writeFileSync(QUICKCSS_PATH, css)
|
||||
);
|
||||
|
||||
ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR);
|
||||
ipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes());
|
||||
ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName));
|
||||
ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
|
||||
|
|
@ -97,6 +99,8 @@ ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => ({
|
|||
"os-accent-color": `#${systemPreferences.getAccentColor?.() || ""}`
|
||||
}));
|
||||
|
||||
ipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR));
|
||||
ipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR));
|
||||
|
||||
export function initIpc(mainWindow: BrowserWindow) {
|
||||
let quickCssWatcher: FSWatcher | undefined;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ RendererSettings.addGlobalChangeListener(() => {
|
|||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
|
||||
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = RendererSettings.plain);
|
||||
|
||||
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string) => {
|
||||
|
|
@ -49,16 +48,18 @@ export interface NativeSettings {
|
|||
[setting: string]: any;
|
||||
};
|
||||
};
|
||||
customCspRules: Record<string, string[]>;
|
||||
}
|
||||
|
||||
const DefaultNativeSettings: NativeSettings = {
|
||||
plugins: {}
|
||||
plugins: {},
|
||||
customCspRules: {}
|
||||
};
|
||||
|
||||
const nativeSettings = readSettings<NativeSettings>("native", NATIVE_SETTINGS_FILE);
|
||||
mergeDefaults(nativeSettings, DefaultNativeSettings);
|
||||
|
||||
export const NativeSettings = new SettingsStore(nativeSettings);
|
||||
export const NativeSettings = new SettingsStore(nativeSettings as NativeSettings);
|
||||
|
||||
NativeSettings.addGlobalChangeListener(() => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@ export function serializeErrors(func: (...args: any[]) => any) {
|
|||
ok: false,
|
||||
error: e instanceof Error ? {
|
||||
// prototypes get lost, so turn error into plain object
|
||||
...e
|
||||
...e,
|
||||
message: e.message,
|
||||
name: e.name,
|
||||
stack: e.stack
|
||||
} : e
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { get } from "@main/utils/simpleGet";
|
||||
import { fetchBuffer, fetchJson } from "@main/utils/http";
|
||||
import { IpcEvents } from "@shared/IpcEvents";
|
||||
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
|
||||
import { ipcMain } from "electron";
|
||||
|
|
@ -31,8 +31,8 @@ import { serializeErrors, VENCORD_FILES } from "./common";
|
|||
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
|
||||
let PendingUpdates = [] as [string, string][];
|
||||
|
||||
async function githubGet(endpoint: string) {
|
||||
return get(API_BASE + endpoint, {
|
||||
async function githubGet<T = any>(endpoint: string) {
|
||||
return fetchJson<T>(API_BASE + endpoint, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
// "All API requests MUST include a valid User-Agent header.
|
||||
|
|
@ -46,9 +46,8 @@ async function calculateGitChanges() {
|
|||
const isOutdated = await fetchUpdates();
|
||||
if (!isOutdated) return [];
|
||||
|
||||
const res = await githubGet(`/compare/${gitHash}...HEAD`);
|
||||
const data = await githubGet(`/compare/${gitHash}...HEAD`);
|
||||
|
||||
const data = JSON.parse(res.toString("utf-8"));
|
||||
return data.commits.map((c: any) => ({
|
||||
// github api only sends the long sha
|
||||
hash: c.sha.slice(0, 7),
|
||||
|
|
@ -58,9 +57,8 @@ async function calculateGitChanges() {
|
|||
}
|
||||
|
||||
async function fetchUpdates() {
|
||||
const release = await githubGet("/releases/latest");
|
||||
const data = await githubGet("/releases/latest");
|
||||
|
||||
const data = JSON.parse(release.toString());
|
||||
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
|
||||
if (hash === gitHash)
|
||||
return false;
|
||||
|
|
@ -70,16 +68,20 @@ async function fetchUpdates() {
|
|||
PendingUpdates.push([name, browser_download_url]);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function applyUpdates() {
|
||||
await Promise.all(PendingUpdates.map(
|
||||
async ([name, data]) => writeFile(
|
||||
join(__dirname, name),
|
||||
await get(data)
|
||||
)
|
||||
));
|
||||
const fileContents = await Promise.all(PendingUpdates.map(async ([name, url]) => {
|
||||
const contents = await fetchBuffer(url);
|
||||
return [join(__dirname, name), contents] as const;
|
||||
}));
|
||||
|
||||
await Promise.all(fileContents.map(async ([filename, contents]) =>
|
||||
writeFile(filename, contents))
|
||||
);
|
||||
|
||||
PendingUpdates = [];
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { join } from "path";
|
|||
|
||||
import { DATA_DIR } from "./constants";
|
||||
import { crxToZip } from "./crxToZip";
|
||||
import { get } from "./simpleGet";
|
||||
import { fetchBuffer } from "./http";
|
||||
|
||||
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
|
||||
|
||||
|
|
@ -69,13 +69,14 @@ export async function installExt(id: string) {
|
|||
} catch (err) {
|
||||
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;
|
||||
|
||||
const buf = await get(url, {
|
||||
const buf = await fetchBuffer(url, {
|
||||
headers: {
|
||||
"User-Agent": `Electron ${process.versions.electron} ~ Vencord (https://github.com/Vendicated/Vencord)`
|
||||
}
|
||||
});
|
||||
|
||||
await extract(crxToZip(buf), extDir).catch(console.error);
|
||||
await extract(crxToZip(buf), extDir)
|
||||
.catch(err => console.error(`Failed to extract extension ${id}`, err));
|
||||
}
|
||||
|
||||
session.defaultSession.loadExtension(extDir);
|
||||
|
|
|
|||
70
src/main/utils/http.ts
Normal file
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 { User } from "discord-types/general";
|
||||
|
||||
const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
|
||||
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64";
|
||||
|
||||
const ContributorBadge: ProfileBadge = {
|
||||
description: "Vencord Contributor",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default definePlugin({
|
|||
{
|
||||
find: "#{intl::MESSAGE_UTILITIES_A11Y_LABEL}",
|
||||
replacement: {
|
||||
match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?:null,(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
|
||||
match: /(?<=:null),(.{0,40}togglePopout:.+?}\)),(.+?)\]}\):null,(?<=\((\i\.\i),{label:.+?children:\[(\i&&!\i)\?\(0,\i\.jsxs?\)\(\i\.Fragment.+?message:(\i).+?)/,
|
||||
replace: (_, ReactButton, PotionButton, ButtonComponent, showReactButton, message) => "" +
|
||||
`]}):null,Vencord.Api.MessagePopover._buildPopoverElements(${ButtonComponent},${message}),${showReactButton}?${ReactButton}:null,${showReactButton}&&${PotionButton},`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,11 +75,7 @@ export default definePlugin({
|
|||
find: "async uploadFiles(",
|
||||
replacement: [
|
||||
{
|
||||
match: /async uploadFiles\((\i),\i\){/,
|
||||
replace: "$&$1.forEach($self.anonymise);"
|
||||
},
|
||||
{
|
||||
match: /async uploadFilesSimple\((\i)\){/,
|
||||
match: /async uploadFiles\((\i)\){/,
|
||||
replace: "$&$1.forEach($self.anonymise);"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ interface RemoteData {
|
|||
let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
|
||||
|
||||
const APPLE_MUSIC_BUNDLE_REGEX = /<script type="module" crossorigin src="([a-zA-Z0-9.\-/]+)"><\/script>/;
|
||||
const APPLE_MUSIC_TOKEN_REGEX = canonicalizeMatch(/Promise.allSettled\(\i\)\}const \i="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)"/);
|
||||
const APPLE_MUSIC_TOKEN_REGEX = canonicalizeMatch(/\b(\i)="([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)"(?=.+?Bearer \$\{\1\})/);
|
||||
|
||||
let cachedToken: string | undefined = undefined;
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ const getToken = async () => {
|
|||
const bundleUrl = new URL(html.match(APPLE_MUSIC_BUNDLE_REGEX)![1], "https://music.apple.com/");
|
||||
|
||||
const bundle = await fetch(bundleUrl).then(r => r.text());
|
||||
const token = bundle.match(APPLE_MUSIC_TOKEN_REGEX)![1];
|
||||
const token = bundle.match(APPLE_MUSIC_TOKEN_REGEX)![2];
|
||||
|
||||
cachedToken = token;
|
||||
return token;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
match: /unreadMentionsFixedFooter\].+?\]/,
|
||||
match: /unreadMentionsFixedFooter\].+?\}\)\]/,
|
||||
replace: "$&.filter($self.makeGuildsBarSidebarFilter(!!arguments[0]?.isBetterFolders))"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { Devs } from "@utils/constants";
|
|||
import { getCurrentGuild, openImageModal } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { GuildStore, Menu, PermissionStore } from "@webpack/common";
|
||||
import { GuildRoleStore, Menu, PermissionStore } from "@webpack/common";
|
||||
|
||||
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ export default definePlugin({
|
|||
const guild = getCurrentGuild();
|
||||
if (!guild) return;
|
||||
|
||||
const role = GuildStore.getRole(guild.id, id);
|
||||
const role = GuildRoleStore.getRole(guild.id, id);
|
||||
if (!role) return;
|
||||
|
||||
if (role.colorString) {
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export default definePlugin({
|
|||
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--interactive-normal)",
|
||||
color: "var(--background-secondary)",
|
||||
color: "var(--background-base-lower)",
|
||||
}}
|
||||
>
|
||||
<PlatformIcon width={14} height={14} />
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: ".embedWrapper,embed",
|
||||
find: "}renderEmbeds(",
|
||||
replacement: [{
|
||||
match: /\.container/,
|
||||
replace: "$&+(this.props.channel.nsfw? ' vc-nsfw-img': '')"
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ export default definePlugin({
|
|||
patches: [{
|
||||
find: "renderConnectionStatus(){",
|
||||
replacement: {
|
||||
match: /(renderConnectionStatus\(\){.+\.channel,children:)(.+?}\):\i)(?=}\))/,
|
||||
// in renderConnectionStatus()
|
||||
match: /(lineClamp:1,children:)(\i)(?=,|}\))/,
|
||||
replace: "$1[$2,$self.renderTimer(this.props.channel.id)]"
|
||||
}
|
||||
}],
|
||||
|
|
|
|||
|
|
@ -7,14 +7,13 @@
|
|||
import { classNameFactory } from "@api/Styles";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
|
||||
import { findByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, ColorPicker, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { settings } from "..";
|
||||
import { relativeLuminance } from "../utils/colorUtils";
|
||||
import { createOrUpdateThemeColorVars } from "../utils/styleUtils";
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
|
||||
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"===');
|
||||
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
|
||||
|
||||
|
|
|
|||
|
|
@ -456,7 +456,7 @@ export default definePlugin({
|
|||
|
||||
<Forms.FormDivider className={Margins.top8} />
|
||||
|
||||
<div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--bg-mod-faint)" }}>
|
||||
<div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--background-mod-faint)" }}>
|
||||
{activity[0] && <ActivityView
|
||||
activity={activity[0]}
|
||||
user={UserStore.getCurrentUser()}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
let wasConnected = isManual;
|
||||
let hasErrored = false;
|
||||
const ws = socket = new WebSocket(`ws://localhost:${PORT}`);
|
||||
const ws = socket = new WebSocket(`ws://127.0.0.1:${PORT}`);
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
wasConnected = true;
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export default definePlugin({
|
|||
},
|
||||
// Change top right chat toolbar button from the help one to the dev one
|
||||
{
|
||||
find: ".CONTEXTLESS,isActivityPanelMode:",
|
||||
find: '"M9 3v18"',
|
||||
replacement: {
|
||||
match: /hasBugReporterAccess:(\i)/,
|
||||
replace: "_hasBugReporterAccess:$1=true"
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ function CloneModal({ data }: { data: Sticker | Emoji; }) {
|
|||
aria-disabled={isCloning}
|
||||
style={{
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--background-secondary)",
|
||||
backgroundColor: "var(--background-base-lower)",
|
||||
display: "inline-flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ const settings = definePluginSettings({
|
|||
description: "Size of the emojis when sending",
|
||||
type: OptionType.SLIDER,
|
||||
default: 48,
|
||||
markers: [32, 48, 64, 128, 160, 256, 512]
|
||||
markers: [32, 48, 64, 96, 128, 160, 256, 512]
|
||||
},
|
||||
transformEmojis: {
|
||||
description: "Whether to transform fake emojis into real ones",
|
||||
|
|
@ -394,7 +394,7 @@ export default definePlugin({
|
|||
},
|
||||
// Separate patch for allowing using custom app icons
|
||||
{
|
||||
find: "?24:30,",
|
||||
find: "getCurrentDesktopIcon(),",
|
||||
replacement: {
|
||||
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,
|
||||
replace: "true"
|
||||
|
|
@ -671,32 +671,38 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) {
|
||||
const contentItems = message.content.split(/\s/);
|
||||
if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false;
|
||||
try {
|
||||
const contentItems = message.content.split(/\s/);
|
||||
if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false;
|
||||
|
||||
switch (embed.type) {
|
||||
case "image": {
|
||||
if (
|
||||
!settings.store.transformCompoundSentence
|
||||
&& !contentItems.some(item => item === embed.url! || item.match(hyperLinkRegex)?.[1] === embed.url!)
|
||||
) return false;
|
||||
switch (embed.type) {
|
||||
case "image": {
|
||||
const url = embed.url ?? embed.image?.url;
|
||||
if (!url) return false;
|
||||
if (
|
||||
!settings.store.transformCompoundSentence
|
||||
&& !contentItems.some(item => item === url || item.match(hyperLinkRegex)?.[1] === url)
|
||||
) return false;
|
||||
|
||||
if (settings.store.transformEmojis) {
|
||||
if (fakeNitroEmojiRegex.test(embed.url!)) return true;
|
||||
}
|
||||
|
||||
if (settings.store.transformStickers) {
|
||||
if (fakeNitroStickerRegex.test(embed.url!)) return true;
|
||||
|
||||
const gifMatch = embed.url!.match(fakeNitroGifStickerRegex);
|
||||
if (gifMatch) {
|
||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
||||
if (StickerStore.getStickerById(gifMatch[1])) return true;
|
||||
if (settings.store.transformEmojis) {
|
||||
if (fakeNitroEmojiRegex.test(url)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
if (settings.store.transformStickers) {
|
||||
if (fakeNitroStickerRegex.test(url)) return true;
|
||||
|
||||
const gifMatch = url.match(fakeNitroGifStickerRegex);
|
||||
if (gifMatch) {
|
||||
// There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
|
||||
if (StickerStore.getStickerById(gifMatch[1])) return true;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
new Logger("FakeNitro").error("Error in shouldIgnoreEmbed:", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -24,10 +24,9 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes, copyWithToast } from "@utils/misc";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, ColorPicker, Flex, Forms, React, Text, UserProfileStore, UserStore, useState } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
import { ReactElement } from "react";
|
||||
import virtualMerge from "virtual-merge";
|
||||
|
|
@ -109,10 +108,8 @@ interface ProfileModalProps {
|
|||
isTryItOutFlow: boolean;
|
||||
}
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
|
||||
const ProfileModal = findComponentByCodeLazy<ProfileModalProps>("isTryItOutFlow:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER");
|
||||
|
||||
const requireColorPicker = extractAndLoadChunksLazy(["#{intl::USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON}"], /createPromise:\(\)=>\i\.\i(\("?.+?"?\)).then\(\i\.bind\(\i,"?(.+?)"?\)\)/);
|
||||
|
||||
export default definePlugin({
|
||||
name: "FakeProfileThemes",
|
||||
|
|
@ -141,8 +138,6 @@ export default definePlugin({
|
|||
const [color1, setColor1] = useState(existingColors[0]);
|
||||
const [color2, setColor2] = useState(existingColors[1]);
|
||||
|
||||
const [, , loadingColorPickerChunk] = useAwaiter(requireColorPicker);
|
||||
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
|
||||
|
|
@ -162,51 +157,49 @@ export default definePlugin({
|
|||
className={classes(Margins.top8, Margins.bottom8)}
|
||||
/>
|
||||
<Forms.FormTitle tag="h3">Color pickers</Forms.FormTitle>
|
||||
{!loadingColorPickerChunk && (
|
||||
<Flex
|
||||
direction={Flex.Direction.HORIZONTAL}
|
||||
style={{ gap: "1rem" }}
|
||||
<Flex
|
||||
direction={Flex.Direction.HORIZONTAL}
|
||||
style={{ gap: "1rem" }}
|
||||
>
|
||||
<ColorPicker
|
||||
color={color1}
|
||||
label={
|
||||
<Text
|
||||
variant={"text-xs/normal"}
|
||||
style={{ marginTop: "4px" }}
|
||||
>
|
||||
Primary
|
||||
</Text>
|
||||
}
|
||||
onChange={(color: number) => {
|
||||
setColor1(color);
|
||||
}}
|
||||
/>
|
||||
<ColorPicker
|
||||
color={color2}
|
||||
label={
|
||||
<Text
|
||||
variant={"text-xs/normal"}
|
||||
style={{ marginTop: "4px" }}
|
||||
>
|
||||
Accent
|
||||
</Text>
|
||||
}
|
||||
onChange={(color: number) => {
|
||||
setColor2(color);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const colorString = encode(color1, color2);
|
||||
copyWithToast(colorString);
|
||||
}}
|
||||
color={Button.Colors.PRIMARY}
|
||||
size={Button.Sizes.XLARGE}
|
||||
>
|
||||
<ColorPicker
|
||||
color={color1}
|
||||
label={
|
||||
<Text
|
||||
variant={"text-xs/normal"}
|
||||
style={{ marginTop: "4px" }}
|
||||
>
|
||||
Primary
|
||||
</Text>
|
||||
}
|
||||
onChange={(color: number) => {
|
||||
setColor1(color);
|
||||
}}
|
||||
/>
|
||||
<ColorPicker
|
||||
color={color2}
|
||||
label={
|
||||
<Text
|
||||
variant={"text-xs/normal"}
|
||||
style={{ marginTop: "4px" }}
|
||||
>
|
||||
Accent
|
||||
</Text>
|
||||
}
|
||||
onChange={(color: number) => {
|
||||
setColor2(color);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const colorString = encode(color1, color2);
|
||||
copyWithToast(colorString);
|
||||
}}
|
||||
color={Button.Colors.PRIMARY}
|
||||
size={Button.Sizes.XLARGE}
|
||||
>
|
||||
Copy 3y3
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
Copy 3y3
|
||||
</Button>
|
||||
</Flex>
|
||||
<Forms.FormDivider
|
||||
className={classes(Margins.top8, Margins.bottom8)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export default definePlugin({
|
|||
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
|
||||
this.instance = instance;
|
||||
return (
|
||||
<ErrorBoundary noop={true}>
|
||||
<ErrorBoundary noop>
|
||||
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
import { Constants, FluxDispatcher, GuildStore, RelationshipStore, RestAPI, SnowflakeUtils, UserStore } from "@webpack/common";
|
||||
import { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common";
|
||||
import { Settings } from "Vencord";
|
||||
|
||||
const UserAffinitiesStore = findStoreLazy("UserAffinitiesStore");
|
||||
const UserAffinitiesStore = findStoreLazy("UserAffinitiesV2Store");
|
||||
|
||||
export default definePlugin({
|
||||
name: "ImplicitRelationships",
|
||||
|
|
@ -117,39 +117,24 @@ export default definePlugin({
|
|||
|
||||
wrapSort(comparator: Function, row: any) {
|
||||
return row.type === 5
|
||||
? -(UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0)
|
||||
? (UserAffinitiesStore.getUserAffinity(row.user.id)?.communicationRank ?? 0)
|
||||
: comparator(row);
|
||||
},
|
||||
|
||||
async refreshUserAffinities() {
|
||||
try {
|
||||
await RestAPI.get({ url: "/users/@me/affinities/users", retries: 3 }).then(({ body }) => {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOAD_USER_AFFINITIES_SUCCESS",
|
||||
affinities: body,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
// Not a critical error if this fails for some reason
|
||||
}
|
||||
},
|
||||
|
||||
async fetchImplicitRelationships() {
|
||||
// Implicit relationships are defined as users that you:
|
||||
// 1. Have an affinity for
|
||||
// 2. Do not have a relationship with
|
||||
await this.refreshUserAffinities();
|
||||
const userAffinities: Set<string> = UserAffinitiesStore.getUserAffinitiesUserIds();
|
||||
const relationships = RelationshipStore.getRelationships();
|
||||
const nonFriendAffinities = Array.from(userAffinities).filter(
|
||||
id => !RelationshipStore.getRelationshipType(id)
|
||||
);
|
||||
nonFriendAffinities.forEach(id => {
|
||||
relationships[id] = 5;
|
||||
const userAffinities: Record<string, any>[] = UserAffinitiesStore.getUserAffinities();
|
||||
const relationships = RelationshipStore.getMutableRelationships();
|
||||
const nonFriendAffinities = userAffinities.filter(a => !RelationshipStore.getRelationshipType(a.otherUserId));
|
||||
nonFriendAffinities.forEach(a => {
|
||||
relationships[a.otherUserId] = 5;
|
||||
});
|
||||
RelationshipStore.emitChange();
|
||||
|
||||
const toRequest = nonFriendAffinities.filter(id => !UserStore.getUser(id));
|
||||
const toRequest = nonFriendAffinities.filter(a => !UserStore.getUser(a.otherUserId));
|
||||
const allGuildIds = Object.keys(GuildStore.getGuilds());
|
||||
const sentNonce = SnowflakeUtils.fromTimestamp(Date.now());
|
||||
let count = allGuildIds.length * Math.ceil(toRequest.length / 100);
|
||||
|
|
|
|||
|
|
@ -67,9 +67,8 @@ export default definePlugin({
|
|||
find: '="SYSTEM_TAG"',
|
||||
replacement: {
|
||||
// Override colorString with our custom color and disable gradients if applying the custom color.
|
||||
match: /&&null!=\i\.secondaryColor,(?<=colorString:(\i).+?(\i)=.+?)/,
|
||||
replace: (m, colorString, hasGradientColors) => `${m}` +
|
||||
`vcIrcColorsDummy=[${colorString},${hasGradientColors}]=$self.getMessageColorsVariables(arguments[0],${hasGradientColors}),`
|
||||
match: /(?<=colorString:\i,colorStrings:\i,colorRoleName:\i}=)(\i),/,
|
||||
replace: "$self.wrapMessageColorProps($1, arguments[0]),"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -82,11 +81,26 @@ export default definePlugin({
|
|||
}
|
||||
],
|
||||
|
||||
getMessageColorsVariables(context: any, hasGradientColors: boolean) {
|
||||
const colorString = this.calculateNameColorForMessageContext(context);
|
||||
const originalColorString = context?.author?.colorString;
|
||||
wrapMessageColorProps(colorProps: { colorString: string, colorStrings?: Record<"primaryColor" | "secondaryColor" | "tertiaryColor", string>; }, context: any) {
|
||||
try {
|
||||
const colorString = this.calculateNameColorForMessageContext(context);
|
||||
if (colorString === colorProps.colorString) {
|
||||
return colorProps;
|
||||
}
|
||||
|
||||
return [colorString, hasGradientColors && colorString === originalColorString];
|
||||
return {
|
||||
...colorProps,
|
||||
colorString,
|
||||
colorStrings: colorProps.colorStrings && {
|
||||
primaryColor: colorString,
|
||||
secondaryColor: undefined,
|
||||
tertiaryColor: undefined
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Failed to calculate message color strings:", e);
|
||||
return colorProps;
|
||||
}
|
||||
},
|
||||
|
||||
calculateNameColorForMessageContext(context: any) {
|
||||
|
|
@ -108,16 +122,20 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
calculateNameColorForListContext(context: any) {
|
||||
const id = context?.user?.id;
|
||||
const colorString = context?.colorString;
|
||||
const color = calculateNameColorForUser(id);
|
||||
try {
|
||||
const id = context?.user?.id;
|
||||
const colorString = context?.colorString;
|
||||
const color = calculateNameColorForUser(id);
|
||||
|
||||
if (settings.store.applyColorOnlyInDms && !context?.channel?.isPrivate()) {
|
||||
return colorString;
|
||||
if (settings.store.applyColorOnlyInDms && !context?.channel?.isPrivate()) {
|
||||
return colorString;
|
||||
}
|
||||
|
||||
return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString)
|
||||
? color
|
||||
: colorString;
|
||||
} catch (e) {
|
||||
console.error("Failed to calculate name color for list context:", e);
|
||||
}
|
||||
|
||||
return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString)
|
||||
? color
|
||||
: colorString;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { GuildStore, SelectedGuildStore, useState } from "@webpack/common";
|
||||
import { GuildRoleStore, SelectedGuildStore, useState } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
|
|
@ -89,7 +89,7 @@ export default definePlugin({
|
|||
// Discord uses Role Mentions for uncached users because .... idk
|
||||
if (!roleId) return null;
|
||||
|
||||
const role = GuildStore.getRole(guildId, roleId);
|
||||
const role = GuildRoleStore.getRole(guildId, roleId);
|
||||
|
||||
if (!role?.icon) return <DefaultRoleIcon />;
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ export default definePlugin({
|
|||
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
}, { noop: true }),
|
||||
});
|
||||
|
||||
function getUsernameString(username: string) {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common";
|
||||
import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore, WindowStore } from "@webpack/common";
|
||||
import NoReplyMentionPlugin from "plugins/noReplyMention";
|
||||
|
||||
const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage");
|
||||
const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
||||
|
|
@ -28,6 +29,7 @@ const EditStore = findByPropsLazy("isEditing", "isEditingAny");
|
|||
let isDeletePressed = false;
|
||||
const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true);
|
||||
const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false);
|
||||
const focusChanged = () => !WindowStore.isFocused() && (isDeletePressed = false);
|
||||
|
||||
const settings = definePluginSettings({
|
||||
enableDeleteOnClick: {
|
||||
|
|
@ -62,11 +64,13 @@ export default definePlugin({
|
|||
start() {
|
||||
document.addEventListener("keydown", keydown);
|
||||
document.addEventListener("keyup", keyup);
|
||||
WindowStore.addChangeListener(focusChanged);
|
||||
},
|
||||
|
||||
stop() {
|
||||
document.removeEventListener("keydown", keydown);
|
||||
document.removeEventListener("keyup", keyup);
|
||||
WindowStore.removeChangeListener(focusChanged);
|
||||
},
|
||||
|
||||
onMessageClick(msg: any, channel, event) {
|
||||
|
|
@ -89,9 +93,8 @@ export default definePlugin({
|
|||
if (msg.hasFlag(EPHEMERAL)) return;
|
||||
|
||||
const isShiftPress = event.shiftKey && !settings.store.requireModifier;
|
||||
const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default;
|
||||
const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention")
|
||||
? NoReplyMention.shouldMention(msg, isShiftPress)
|
||||
const shouldMention = Vencord.Plugins.isPluginEnabled(NoReplyMentionPlugin.name)
|
||||
? NoReplyMentionPlugin.shouldMention(msg, isShiftPress)
|
||||
: !isShiftPress;
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
|
|||
import { updateMessage } from "@api/MessageUpdater";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants.js";
|
||||
import { classes } from "@utils/misc";
|
||||
import { Queue } from "@utils/Queue";
|
||||
|
|
@ -295,7 +294,7 @@ function ChannelMessageEmbedAccessory({ message, channel }: MessageEmbedProps):
|
|||
<Embed
|
||||
embed={{
|
||||
rawDescription: "",
|
||||
color: "var(--background-secondary)",
|
||||
color: "var(--background-base-lower)",
|
||||
author: {
|
||||
name: <Text variant="text-xs/medium" tag="span">
|
||||
<span>{channelLabel} - </span>
|
||||
|
|
@ -373,7 +372,7 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
start() {
|
||||
addMessageAccessory("messageLinkEmbed", props => {
|
||||
addMessageAccessory("MessageLinkEmbeds", props => {
|
||||
if (!messageLinkRegex.test(props.message.content))
|
||||
return null;
|
||||
|
||||
|
|
@ -381,16 +380,14 @@ export default definePlugin({
|
|||
messageLinkRegex.lastIndex = 0;
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<MessageEmbedAccessory
|
||||
message={props.message}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<MessageEmbedAccessory
|
||||
message={props.message}
|
||||
/>
|
||||
);
|
||||
}, 4 /* just above rich embeds */);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeMessageAccessory("messageLinkEmbed");
|
||||
removeMessageAccessory("MessageLinkEmbeds");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.messagelogger-deleted {
|
||||
--text-normal: var(--status-danger, #f04747);
|
||||
--text-default: var(--status-danger, #f04747);
|
||||
--interactive-normal: var(--status-danger, #f04747);
|
||||
--text-muted: var(--status-danger, #f04747);
|
||||
--embed-title: var(--red-460, #be3535);
|
||||
|
|
|
|||
|
|
@ -204,5 +204,5 @@ export default definePlugin({
|
|||
/>
|
||||
</>
|
||||
);
|
||||
})
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,12 +21,9 @@ import { Devs } from "@utils/constants";
|
|||
import { runtimeHashMessageKey } from "@utils/intlHash";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { i18n } from "@webpack/common";
|
||||
import { i18n, RelationshipStore } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
|
||||
|
||||
interface MessageDeleteProps {
|
||||
// Internal intl message for BLOCKED_MESSAGE_COUNT
|
||||
collapsedReason: () => any;
|
||||
|
|
|
|||
|
|
@ -75,5 +75,5 @@ export default definePlugin({
|
|||
}}> Pause Indefinitely.</a>}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { copyToClipboard } from "@utils/clipboard";
|
|||
import { getIntlMessage, getUniqueUsername } from "@utils/discord";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildRoleStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
import { UnicodeEmoji } from "@webpack/types";
|
||||
import type { Guild, Role, User } from "discord-types/general";
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
|
|||
const [selectedItemIndex, selectItem] = useState(0);
|
||||
const selectedItem = permissions[selectedItemIndex];
|
||||
|
||||
const roles = GuildStore.getRoles(guild.id);
|
||||
const roles = GuildRoleStore.getRoles(guild.id);
|
||||
|
||||
return (
|
||||
<ModalRoot
|
||||
|
|
@ -238,7 +238,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
|||
id={cl("view-as-role")}
|
||||
label={getIntlMessage("VIEW_AS_ROLE")}
|
||||
action={() => {
|
||||
const role = GuildStore.getRole(guild.id, roleId);
|
||||
const role = GuildRoleStore.getRole(guild.id, roleId);
|
||||
if (!role) return;
|
||||
|
||||
onClose();
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ function UserPermissionsComponent({ guild, guildMember, closePopout }: { guild:
|
|||
viewBox="0 96 960 960"
|
||||
transform={permissionsSortOrder === PermissionsSortOrder.HighestRole ? "scale(1 1)" : "scale(1 -1)"}
|
||||
>
|
||||
<path fill="var(--text-normal)" d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" />
|
||||
<path fill="var(--text-default)" d="M440 896V409L216 633l-56-57 320-320 320 320-56 57-224-224v487h-80Z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -181,7 +181,7 @@ function UserPermissionsComponent({ guild, guildMember, closePopout }: { guild:
|
|||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill="var(--text-normal)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
|
||||
<path fill="var(--text-default)" d="M7 12.001C7 10.8964 6.10457 10.001 5 10.001C3.89543 10.001 3 10.8964 3 12.001C3 13.1055 3.89543 14.001 5 14.001C6.10457 14.001 7 13.1055 7 12.001ZM14 12.001C14 10.8964 13.1046 10.001 12 10.001C10.8954 10.001 10 10.8964 10 12.001C10 13.1055 10.8954 14.001 12 14.001C13.1046 14.001 14 13.1055 14 12.001ZM19 10.001C20.1046 10.001 21 10.8964 21 12.001C21 13.1055 20.1046 14.001 19 14.001C17.8954 14.001 17 13.1055 17 12.001C17 10.8964 17.8954 10.001 19 10.001Z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function PermissionAllowedIcon() {
|
|||
viewBox="0 0 24 24"
|
||||
>
|
||||
<title>Allowed</title>
|
||||
<path fill="var(--text-positive)" d="M8.99991 16.17L4.82991 12L3.40991 13.41L8.99991 19L20.9999 7.00003L19.5899 5.59003L8.99991 16.17ZZ" />
|
||||
<path fill="var(--status-positive)" d="M8.99991 16.17L4.82991 12L3.40991 13.41L8.99991 19L20.9999 7.00003L19.5899 5.59003L8.99991 16.17ZZ" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ export function PermissionDefaultIcon() {
|
|||
>
|
||||
<g>
|
||||
<title>Not overwritten</title>
|
||||
<polygon fill="var(--text-normal)" points="12 2.32 10.513 2 4 13.68 5.487 14" />
|
||||
<polygon fill="var(--text-default)" points="12 2.32 10.513 2 4 13.68 5.487 14" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import { Devs } from "@utils/constants";
|
|||
import { classes } from "@utils/misc";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildStore, match, Menu, PermissionsBits, Popout, TooltipContainer, useRef, UserStore } from "@webpack/common";
|
||||
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildRoleStore, GuildStore, match, Menu, PermissionsBits, Popout, TooltipContainer, useRef, UserStore } from "@webpack/common";
|
||||
import type { Guild, GuildMember } from "discord-types/general";
|
||||
|
||||
import openRolesAndUsersPermissionsModal, { PermissionType, RoleOrUserPermission } from "./components/RolesAndUsersPermissions";
|
||||
|
|
@ -107,7 +107,7 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) {
|
|||
};
|
||||
})
|
||||
.otherwise(() => {
|
||||
const permissions = Object.values(GuildStore.getRoles(guild.id)).map(role => ({
|
||||
const permissions = Object.values(GuildRoleStore.getRoles(guild.id)).map(role => ({
|
||||
type: PermissionType.Role,
|
||||
...role
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -140,10 +140,9 @@
|
|||
|
||||
/* copy pasted from discord cause impossible to webpack find */
|
||||
.vc-permviewer-role-button {
|
||||
border-radius: var(--radius-xs);
|
||||
background: var(--bg-mod-faint);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--interactive-normal);
|
||||
border: 1px solid var(--border-faint);
|
||||
border: 1px solid var(--user-profile-border);
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
|
|
@ -151,9 +150,8 @@
|
|||
padding: 4px
|
||||
}
|
||||
|
||||
.custom-profile-theme .vc-permviewer-role-button {
|
||||
background: rgb(var(--bg-overlay-color)/var(--bg-overlay-opacity-6));
|
||||
border-color: var(--profile-body-border-color)
|
||||
.vc-permviewer-role-button:hover {
|
||||
background-color: var(--user-profile-background-hover);
|
||||
}
|
||||
|
||||
.vc-permviewer-granted-by-container {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { GuildStore } from "@webpack/common";
|
||||
import { GuildRoleStore } from "@webpack/common";
|
||||
import { Guild, GuildMember, Role } from "discord-types/general";
|
||||
|
||||
import { PermissionsSortOrder, settings } from ".";
|
||||
|
|
@ -29,7 +29,7 @@ export const { getGuildPermissionSpecMap } = findByPropsLazy("getGuildPermission
|
|||
export const cl = classNameFactory("vc-permviewer-");
|
||||
|
||||
export function getSortedRoles({ id }: Guild, member: GuildMember) {
|
||||
const roles = GuildStore.getRoles(id);
|
||||
const roles = GuildRoleStore.getRoles(id);
|
||||
|
||||
return [...member.roles, id]
|
||||
.map(id => roles[id])
|
||||
|
|
@ -48,7 +48,7 @@ export function sortUserRoles(roles: Role[]) {
|
|||
}
|
||||
|
||||
export function sortPermissionOverwrites<T extends { id: string; type: number; }>(overwrites: T[], guildId: string) {
|
||||
const roles = GuildStore.getRoles(guildId);
|
||||
const roles = GuildRoleStore.getRoles(guildId);
|
||||
|
||||
return overwrites.sort((a, b) => {
|
||||
if (a.type !== PermissionType.Role || b.type !== PermissionType.Role) return 0;
|
||||
|
|
|
|||
|
|
@ -7,18 +7,11 @@
|
|||
import { classNameFactory } from "@api/Styles";
|
||||
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModalLazy } from "@utils/modal";
|
||||
import { extractAndLoadChunksLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { Button, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common";
|
||||
import { Button, ColorPicker, Forms, Text, TextInput, Toasts, useMemo, useState } from "@webpack/common";
|
||||
|
||||
import { DEFAULT_COLOR, SWATCHES } from "../constants";
|
||||
import { categoryLen, createCategory, getCategory } from "../data";
|
||||
|
||||
interface ColorPickerProps {
|
||||
color: number | null;
|
||||
showEyeDropper?: boolean;
|
||||
suggestedColors?: string[];
|
||||
onChange(value: number | null): void;
|
||||
}
|
||||
|
||||
interface ColorPickerWithSwatchesProps {
|
||||
defaultColor: number;
|
||||
colors: number[];
|
||||
|
|
@ -29,7 +22,6 @@ interface ColorPickerWithSwatchesProps {
|
|||
renderCustomButton?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy<ColorPickerProps>("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
|
||||
const ColorPickerWithSwatches = findComponentByCodeLazy<ColorPickerWithSwatchesProps>('id:"color-picker"');
|
||||
|
||||
export const requireSettingsMenu = extractAndLoadChunksLazy(['name:"UserSettings"'], /createPromise:.{0,20}(\i\.\i\("?.+?"?\).*?).then\(\i\.bind\(\i,"?(.+?)"?\)\).{0,50}"UserSettings"/);
|
||||
|
|
|
|||
|
|
@ -16,20 +16,17 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { ChannelStore, ComponentDispatch, FluxDispatcher as Dispatcher, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||
import { ChannelStore, ComponentDispatch, FluxDispatcher as Dispatcher, MessageActions, MessageStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
const Kangaroo = findByPropsLazy("jumpToMessage");
|
||||
const RelationshipStore = findByPropsLazy("getRelationships", "isBlocked");
|
||||
import NoBlockedMessagesPlugin from "plugins/noBlockedMessages";
|
||||
import NoReplyMentionPlugin from "plugins/noReplyMention";
|
||||
|
||||
const isMac = navigator.platform.includes("Mac"); // bruh
|
||||
let replyIdx = -1;
|
||||
let editIdx = -1;
|
||||
|
||||
let currentlyReplyingId: string | null = null;
|
||||
let currentlyEditingId: string | null = null;
|
||||
|
||||
const enum MentionOptions {
|
||||
DISABLED,
|
||||
|
|
@ -69,36 +66,29 @@ export default definePlugin({
|
|||
|
||||
flux: {
|
||||
DELETE_PENDING_REPLY() {
|
||||
replyIdx = -1;
|
||||
currentlyReplyingId = null;
|
||||
},
|
||||
MESSAGE_END_EDIT() {
|
||||
editIdx = -1;
|
||||
currentlyEditingId = null;
|
||||
},
|
||||
CHANNEL_SELECT() {
|
||||
currentlyReplyingId = null;
|
||||
currentlyEditingId = null;
|
||||
},
|
||||
MESSAGE_START_EDIT: onStartEdit,
|
||||
CREATE_PENDING_REPLY: onCreatePendingReply
|
||||
}
|
||||
});
|
||||
|
||||
function calculateIdx(messages: Message[], id: string) {
|
||||
const idx = messages.findIndex(m => m.id === id);
|
||||
return idx === -1
|
||||
? idx
|
||||
: messages.length - idx - 1;
|
||||
}
|
||||
|
||||
function onStartEdit({ channelId, messageId, _isQuickEdit }: any) {
|
||||
function onStartEdit({ messageId, _isQuickEdit }: any) {
|
||||
if (_isQuickEdit) return;
|
||||
|
||||
const meId = UserStore.getCurrentUser().id;
|
||||
|
||||
const messages = MessageStore.getMessages(channelId)._array.filter(m => m.author.id === meId);
|
||||
editIdx = calculateIdx(messages, messageId);
|
||||
currentlyEditingId = messageId;
|
||||
}
|
||||
|
||||
function onCreatePendingReply({ message, _isQuickReply }: { message: Message; _isQuickReply: boolean; }) {
|
||||
if (_isQuickReply) return;
|
||||
|
||||
replyIdx = calculateIdx(MessageStore.getMessages(message.channel_id)._array, message.id);
|
||||
currentlyReplyingId = message.id;
|
||||
}
|
||||
|
||||
const isCtrl = (e: KeyboardEvent) => isMac ? e.metaKey : e.ctrlKey;
|
||||
|
|
@ -123,10 +113,10 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
|
|||
|
||||
const vh = Math.max(document.documentElement.clientHeight, window.innerHeight);
|
||||
const rect = element.getBoundingClientRect();
|
||||
const isOffscreen = rect.bottom < 200 || rect.top - vh >= -200;
|
||||
const isOffscreen = rect.bottom < 150 || rect.top - vh >= -150;
|
||||
|
||||
if (isOffscreen) {
|
||||
Kangaroo.jumpToMessage({
|
||||
MessageActions.jumpToMessage({
|
||||
channelId,
|
||||
messageId,
|
||||
flash: false,
|
||||
|
|
@ -137,43 +127,48 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
|
|||
|
||||
function getNextMessage(isUp: boolean, isReply: boolean) {
|
||||
let messages: Array<Message & { deleted?: boolean; }> = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
|
||||
if (!isReply) { // we are editing so only include own
|
||||
const meId = UserStore.getCurrentUser().id;
|
||||
messages = messages.filter(m => m.author.id === meId);
|
||||
}
|
||||
|
||||
if (Vencord.Plugins.isPluginEnabled("NoBlockedMessages")) {
|
||||
messages = messages.filter(m => !RelationshipStore.isBlocked(m.author.id));
|
||||
}
|
||||
const meId = UserStore.getCurrentUser().id;
|
||||
const hasNoBlockedMessages = Vencord.Plugins.isPluginEnabled(NoBlockedMessagesPlugin.name);
|
||||
|
||||
const mutate = (i: number) => isUp
|
||||
? Math.min(messages.length - 1, i + 1)
|
||||
: Math.max(-1, i - 1);
|
||||
messages = messages.filter(m => {
|
||||
if (m.deleted) return false;
|
||||
if (!isReply && m.author.id !== meId) return false; // editing only own messages
|
||||
if (hasNoBlockedMessages && NoBlockedMessagesPlugin.shouldIgnoreMessage(m)) return false;
|
||||
|
||||
const findNextNonDeleted = (i: number) => {
|
||||
do {
|
||||
i = mutate(i);
|
||||
} while (i !== -1 && messages[messages.length - i - 1]?.deleted === true);
|
||||
return i;
|
||||
return true;
|
||||
});
|
||||
|
||||
const findNextNonDeleted = (id: string | null) => {
|
||||
if (id === null) return messages[messages.length - 1];
|
||||
|
||||
const idx = messages.findIndex(m => m.id === id);
|
||||
if (idx === -1) return messages[messages.length - 1];
|
||||
|
||||
const i = isUp ? idx - 1 : idx + 1;
|
||||
return messages[i] ?? null;
|
||||
};
|
||||
|
||||
let i: number;
|
||||
if (isReply)
|
||||
replyIdx = i = findNextNonDeleted(replyIdx);
|
||||
else
|
||||
editIdx = i = findNextNonDeleted(editIdx);
|
||||
|
||||
return i === - 1 ? undefined : messages[messages.length - i - 1];
|
||||
if (isReply) {
|
||||
const msg = findNextNonDeleted(currentlyReplyingId);
|
||||
currentlyReplyingId = msg?.id ?? null;
|
||||
return msg;
|
||||
} else {
|
||||
const msg = findNextNonDeleted(currentlyEditingId);
|
||||
currentlyEditingId = msg?.id ?? null;
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldMention(message) {
|
||||
const { enabled, userList, shouldPingListed } = Settings.plugins.NoReplyMention;
|
||||
const shouldPing = !enabled || (shouldPingListed === userList.includes(message.author.id));
|
||||
|
||||
function shouldMention(message: Message) {
|
||||
switch (settings.store.shouldMention) {
|
||||
case MentionOptions.NO_REPLY_MENTION_PLUGIN: return shouldPing;
|
||||
case MentionOptions.DISABLED: return false;
|
||||
default: return true;
|
||||
case MentionOptions.NO_REPLY_MENTION_PLUGIN:
|
||||
if (!Vencord.Plugins.isPluginEnabled(NoReplyMentionPlugin.name)) return true;
|
||||
return NoReplyMentionPlugin.shouldMention(message, false);
|
||||
case MentionOptions.DISABLED:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,13 +176,16 @@ function shouldMention(message) {
|
|||
function nextReply(isUp: boolean) {
|
||||
const currChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
|
||||
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
||||
|
||||
const message = getNextMessage(isUp, true);
|
||||
|
||||
if (!message)
|
||||
if (!message) {
|
||||
return void Dispatcher.dispatch({
|
||||
type: "DELETE_PENDING_REPLY",
|
||||
channelId: SelectedChannelStore.getChannelId(),
|
||||
});
|
||||
}
|
||||
|
||||
const channel = ChannelStore.getChannel(message.channel_id);
|
||||
const meId = UserStore.getCurrentUser().id;
|
||||
|
||||
|
|
@ -199,6 +197,7 @@ function nextReply(isUp: boolean) {
|
|||
showMentionToggle: !channel.isPrivate() && message.author.id !== meId,
|
||||
_isQuickReply: true
|
||||
});
|
||||
|
||||
ComponentDispatch.dispatchToLastSubscribed("TEXTAREA_FOCUS");
|
||||
jumpIfOffScreen(channel.id, message.id);
|
||||
}
|
||||
|
|
@ -209,11 +208,13 @@ function nextEdit(isUp: boolean) {
|
|||
if (currChannel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, currChannel)) return;
|
||||
const message = getNextMessage(isUp, false);
|
||||
|
||||
if (!message)
|
||||
if (!message) {
|
||||
return Dispatcher.dispatch({
|
||||
type: "MESSAGE_END_EDIT",
|
||||
channelId: SelectedChannelStore.getChannelId()
|
||||
});
|
||||
}
|
||||
|
||||
Dispatcher.dispatch({
|
||||
type: "MESSAGE_START_EDIT",
|
||||
channelId: message.channel_id,
|
||||
|
|
@ -221,5 +222,6 @@ function nextEdit(isUp: boolean) {
|
|||
content: message.content,
|
||||
_isQuickEdit: true
|
||||
});
|
||||
|
||||
jumpIfOffScreen(message.channel_id, message.id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ export async function syncFriends() {
|
|||
friends.friends = [];
|
||||
friends.requests = [];
|
||||
|
||||
const relationShips = RelationshipStore.getRelationships();
|
||||
const relationShips = RelationshipStore.getMutableRelationships();
|
||||
for (const id in relationShips) {
|
||||
switch (relationShips[id]) {
|
||||
case RelationshipType.FRIEND:
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function makeSearchItem(src: string) {
|
|||
aria-hidden="true"
|
||||
height={16}
|
||||
width={16}
|
||||
src={`https://www.google.com/s2/favicons?domain=${Engines[engine]}&sz=64`}
|
||||
src={`https://icons.duckduckgo.com/ip3/${new URL(Engines[engine]).hostname}.ico`}
|
||||
/>
|
||||
{engine}
|
||||
</Flex>
|
||||
|
|
|
|||
|
|
@ -53,14 +53,12 @@ function makeSearchItem(src: string) {
|
|||
<Flex style={{ alignItems: "center", gap: "0.5em" }}>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: i >= 3 // Do not round Google, Yandex & SauceNAO
|
||||
? "50%"
|
||||
: void 0
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
height={16}
|
||||
width={16}
|
||||
src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")}
|
||||
src={`https://icons.duckduckgo.com/ip3/${new URL(Engines[engine]).host}.ico`}
|
||||
/>
|
||||
{engine}
|
||||
</Flex>
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
overflow-y: hidden;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-normal);
|
||||
color: var(--text-default);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { Devs } from "@utils/constants";
|
|||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { ChannelStore, GuildMemberStore, GuildStore } from "@webpack/common";
|
||||
import { ChannelStore, GuildMemberStore, GuildRoleStore, GuildStore } from "@webpack/common";
|
||||
|
||||
const useMessageAuthor = findByCodeLazy('"Result cannot be null because the message is not null"');
|
||||
|
||||
|
|
@ -84,8 +84,8 @@ export default definePlugin({
|
|||
find: ".USER_MENTION)",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=onContextMenu:\i,color:)\i(?<=\.getNickname\((\i),\i,(\i).+?)/,
|
||||
replace: "$self.getColorInt($2?.id,$1)",
|
||||
match: /(?<=user:(\i),guildId:([^,]+?),.{0,100}?children:\i=>\i)\((\i)\)/,
|
||||
replace: "({...$3,color:$self.getColorInt($1?.id,$2)})",
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.chatMentions
|
||||
|
|
@ -197,7 +197,7 @@ export default definePlugin({
|
|||
const value = `color-mix(in oklab, ${author.colorString} ${messageSaturation}%, var({DEFAULT}))`;
|
||||
|
||||
return {
|
||||
color: value.replace("{DEFAULT}", "--text-normal"),
|
||||
color: value.replace("{DEFAULT}", "--text-default"),
|
||||
"--header-primary": value.replace("{DEFAULT}", "--header-primary"),
|
||||
"--text-muted": value.replace("{DEFAULT}", "--text-muted")
|
||||
};
|
||||
|
|
@ -210,7 +210,7 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
RoleGroupColor: ErrorBoundary.wrap(({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) => {
|
||||
const role = GuildStore.getRole(guildId, id);
|
||||
const role = GuildRoleStore.getRole(guildId, id);
|
||||
|
||||
return (
|
||||
<span style={{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.vc-st-date-picker {
|
||||
background-color: var(--input-background);
|
||||
color: var(--text-normal);
|
||||
color: var(--text-default);
|
||||
width: 95%;
|
||||
padding: 8px 8px 8px 12px;
|
||||
margin: 1em 0;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { classes } from "@utils/misc";
|
|||
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { useAwaiter } from "@utils/react";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
|
||||
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildRoleStore, IconUtils, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
|
||||
import { Guild, User } from "discord-types/general";
|
||||
|
||||
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
|
||||
|
|
@ -198,9 +198,9 @@ function ServerInfoTab({ guild }: GuildProps) {
|
|||
"Vanity Link": guild.vanityURLCode ? (<a>{`discord.gg/${guild.vanityURLCode}`}</a>) : "-", // Making the anchor href valid would cause Discord to reload
|
||||
"Preferred Locale": guild.preferredLocale || "-",
|
||||
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
|
||||
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
|
||||
"Server Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
|
||||
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category
|
||||
"Roles": Object.keys(GuildStore.getRoles(guild.id)).length - 1, // - @everyone
|
||||
"Roles": Object.keys(GuildRoleStore.getRoles(guild.id)).length - 1, // - @everyone
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -220,12 +220,12 @@ function FriendsTab({ guild, setCount }: RelationshipProps) {
|
|||
}
|
||||
|
||||
function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
|
||||
const blockedIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isBlocked(id));
|
||||
const blockedIds = RelationshipStore.getBlockedIDs();
|
||||
return UserList("blocked", guild, blockedIds, setCount);
|
||||
}
|
||||
|
||||
function IgnoredUserTab({ guild, setCount }: RelationshipProps) {
|
||||
const ignoredIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isIgnored(id));
|
||||
const ignoredIds = RelationshipStore.getIgnoredIDs();
|
||||
return UserList("ignored", guild, ignoredIds, setCount);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
}
|
||||
|
||||
.vc-gp-server-info-pair {
|
||||
color: var(--text-normal);
|
||||
color: var(--text-default);
|
||||
}
|
||||
|
||||
.vc-gp-server-info-pair [class^="timestamp"] {
|
||||
|
|
|
|||
|
|
@ -78,12 +78,12 @@ export const Highlighter = ({
|
|||
});
|
||||
|
||||
const themeBase: ThemeBase = {
|
||||
plainColor: currentTheme?.fg || "var(--text-normal)",
|
||||
plainColor: currentTheme?.fg || "var(--text-default)",
|
||||
accentBgColor:
|
||||
currentTheme?.colors?.["statusBar.background"] || (useHljs ? "#7289da" : "#007BC8"),
|
||||
accentFgColor: currentTheme?.colors?.["statusBar.foreground"] || "#FFF",
|
||||
backgroundColor:
|
||||
currentTheme?.colors?.["editor.background"] || "var(--background-secondary)",
|
||||
currentTheme?.colors?.["editor.background"] || "var(--background-base-lower)",
|
||||
};
|
||||
|
||||
let langName;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.vc-shiki-container {
|
||||
border: 4px;
|
||||
background-color: var(--background-secondary);
|
||||
background-color: var(--background-base-lower);
|
||||
}
|
||||
|
||||
.vc-shiki-root {
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@
|
|||
}
|
||||
|
||||
.vc-shc-heading-nsfw-icon {
|
||||
color: var(--text-normal);
|
||||
color: var(--text-default);
|
||||
}
|
||||
|
||||
.vc-shc-topic-container {
|
||||
color: var(--text-normal);
|
||||
background: var(--bg-overlay-3, var(--background-secondary));
|
||||
color: var(--text-default);
|
||||
background: var(--bg-overlay-5, var(--background-base-lower));
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
max-width: 70vw;
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: var(--bg-overlay-3, var(--background-secondary));
|
||||
background: var(--bg-overlay-5, var(--background-base-lower));
|
||||
border-radius: 8px;
|
||||
padding: 0.75em;
|
||||
margin-left: 0.75em;
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
.vc-shc-tags-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-overlay-3, var(--background-secondary));
|
||||
background: var(--bg-overlay-5, var(--background-base-lower));
|
||||
border-radius: 5px;
|
||||
padding: 0.75em;
|
||||
gap: 0.75em;
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: var(--bg-overlay-3, var(--background-secondary));
|
||||
background: var(--bg-overlay-5, var(--background-base-lower));
|
||||
border-radius: 5px;
|
||||
padding: 0.75em;
|
||||
max-width: 70vw;
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-normal);
|
||||
color: var(--text-default);
|
||||
}
|
||||
|
||||
.vc-shc-allowed-users-and-roles-container-permdetails-btn {
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-normal);
|
||||
color: var(--text-default);
|
||||
}
|
||||
|
||||
.vc-shc-allowed-users-and-roles-container > [class^="members"] {
|
||||
|
|
|
|||
|
|
@ -48,10 +48,9 @@ export default definePlugin({
|
|||
authors: [Devs.Rini, Devs.TheKodeToad],
|
||||
patches: [
|
||||
{
|
||||
find: '"BaseUsername"',
|
||||
find: '="SYSTEM_TAG"',
|
||||
replacement: {
|
||||
/* TODO: remove \i+\i once change makes it to stable */
|
||||
match: /(?<=onContextMenu:\i,children:)(?:\i\+\i|\i)/,
|
||||
match: /(?<=onContextMenu:\i,children:)\i/,
|
||||
replace: "$self.renderUsername(arguments[0])"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -86,5 +86,5 @@ export default definePlugin({
|
|||
</TooltipContainer>
|
||||
)}
|
||||
</div>;
|
||||
})
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@
|
|||
}
|
||||
|
||||
.theme-light #vc-spotify-player {
|
||||
background: var(--bg-overlay-3, var(--background-secondary-alt));
|
||||
background: var(--bg-overlay-3, var(--background-base-lower-alt));
|
||||
}
|
||||
|
||||
.theme-dark #vc-spotify-player {
|
||||
background: var(--bg-overlay-1, var(--background-secondary-alt));
|
||||
background: var(--bg-overlay-1, var(--background-base-lower-alt));
|
||||
}
|
||||
|
||||
.vc-spotify-button {
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
|
||||
#vc-spotify-progress-bar {
|
||||
position: relative;
|
||||
color: var(--text-normal);
|
||||
color: var(--text-default);
|
||||
width: 100%;
|
||||
margin: 0.5em 0;
|
||||
margin-bottom: 5px;
|
||||
|
|
@ -195,5 +195,5 @@
|
|||
|
||||
.vc-spotify-fallback {
|
||||
padding: 0.5em;
|
||||
color: var(--text-normal);
|
||||
color: var(--text-default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
.visual-refresh {
|
||||
#vc-spotify-player {
|
||||
padding: 12px;
|
||||
background: var(--bg-overlay-floating, var(--background-base-low, var(--background-secondary-alt)));
|
||||
background: var(--bg-overlay-floating, var(--background-base-low, var(--background-base-lower-alt)));
|
||||
margin: 0;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
#vc-spotify-progress-bar {
|
||||
position: relative;
|
||||
color: var(--text-normal);
|
||||
color: var(--text-default);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export function TranslationAccessory({ message }: { message: Message; }) {
|
|||
<span className={cl("accessory")}>
|
||||
<TranslateIcon width={16} height={16} className={cl("accessory-icon")} />
|
||||
{Parser.parse(translation.text)}
|
||||
{" "}
|
||||
<br />
|
||||
(translated from {translation.sourceLanguage} - <Dismiss onDismiss={() => setTranslation(undefined)} />)
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
margin-top: 0.5em;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
line-height: 1.2rem;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.vc-trans-accessory-icon {
|
||||
|
|
|
|||
|
|
@ -69,26 +69,29 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s
|
|||
|
||||
const myId = UserStore.getCurrentUser()?.id;
|
||||
|
||||
const typingUsersArray = Object.keys(typingUsers).filter(id => id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers));
|
||||
const typingUsersArray = Object.keys(typingUsers).filter(id =>
|
||||
id !== myId && !(RelationshipStore.isBlocked(id) && !settings.store.includeBlockedUsers)
|
||||
);
|
||||
const [a, b, c] = typingUsersArray;
|
||||
let tooltipText: string;
|
||||
|
||||
switch (typingUsersArray.length) {
|
||||
case 0: break;
|
||||
case 1: {
|
||||
tooltipText = getIntlMessage("ONE_USER_TYPING", { a: getDisplayName(guildId, typingUsersArray[0]) });
|
||||
tooltipText = getIntlMessage("ONE_USER_TYPING", { a: getDisplayName(guildId, a) });
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
tooltipText = getIntlMessage("TWO_USERS_TYPING", { a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]) });
|
||||
tooltipText = getIntlMessage("TWO_USERS_TYPING", { a: getDisplayName(guildId, a), b: getDisplayName(guildId, b) });
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
tooltipText = getIntlMessage("THREE_USERS_TYPING", { a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), c: getDisplayName(guildId, typingUsersArray[2]) });
|
||||
tooltipText = getIntlMessage("THREE_USERS_TYPING", { a: getDisplayName(guildId, a), b: getDisplayName(guildId, b), c: getDisplayName(guildId, c) });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
tooltipText = Settings.plugins.TypingTweaks.enabled
|
||||
? buildSeveralUsers({ a: getDisplayName(guildId, typingUsersArray[0]), b: getDisplayName(guildId, typingUsersArray[1]), count: typingUsersArray.length - 2 })
|
||||
? buildSeveralUsers({ a: UserStore.getUser(a), b: UserStore.getUser(b), count: typingUsersArray.length - 2, guildId })
|
||||
: getIntlMessage("SEVERAL_USERS_TYPING");
|
||||
break;
|
||||
}
|
||||
|
|
@ -178,7 +181,7 @@ export default definePlugin({
|
|||
// Theads
|
||||
{
|
||||
// This is the thread "spine" that shows in the left
|
||||
find: "M11 9H4C2.89543 9 2 8.10457 2 7V1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1V7C0 9.20914 1.79086 11 4 11H11C11.5523 11 12 10.5523 12 10C12 9.44771 11.5523 9 11 9Z",
|
||||
find: "M0 15H2c0 1.6569",
|
||||
replacement: {
|
||||
match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/,
|
||||
replace: "$&,$self.TypingIndicator($1.id,$1.getGuildId())"
|
||||
|
|
|
|||
|
|
@ -45,14 +45,17 @@ const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
export function buildSeveralUsers({ a, b, count }: { a: string, b: string, count: number; }) {
|
||||
return [
|
||||
<strong key="0">{a}</strong>,
|
||||
", ",
|
||||
<strong key="1">{b}</strong>,
|
||||
`, and ${count} others are typing...`
|
||||
];
|
||||
}
|
||||
export const buildSeveralUsers = ErrorBoundary.wrap(({ a, b, count, guildId }: { a: User, b: User, count: number; guildId: string; }) => {
|
||||
return (
|
||||
<>
|
||||
<TypingUser user={a} guildId={guildId} />
|
||||
{", "}
|
||||
<TypingUser user={b} guildId={guildId} />
|
||||
{", "}
|
||||
and {count} others are typing...
|
||||
</>
|
||||
);
|
||||
}, { noop: true });
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
|
|
@ -96,21 +99,23 @@ export default definePlugin({
|
|||
patches: [
|
||||
{
|
||||
find: "#{intl::THREE_USERS_TYPING}",
|
||||
group: true,
|
||||
replacement: [
|
||||
{
|
||||
// Style the indicator and add function call to modify the children before rendering
|
||||
match: /(?<=children:\[(\i)\.length>0.{0,200}?"aria-atomic":!0,children:)\i(?<=guildId:(\i).+?)/,
|
||||
replace: "$self.renderTypingUsers({ users: $1, guildId: $2, children: $& })"
|
||||
match: /(?<=children:\[(\i)\.length>0.{0,300}?"aria-atomic":!0,children:)\i/,
|
||||
replace: "$self.renderTypingUsers({ users: $1, guildId: arguments[0]?.channel?.guild_id, children: $& })"
|
||||
},
|
||||
{
|
||||
// Changes the indicator to keep the user object when creating the list of typing users
|
||||
match: /\.map\((\i)=>\i\.\i\.getName\(\i,\i\.id,\1\)\)/,
|
||||
match: /\.map\((\i)=>\i\.\i\.getName\(\i(?:\.guild_id)?,\i\.id,\1\)\)/,
|
||||
replace: ""
|
||||
},
|
||||
{
|
||||
// Adds the alternative formatting for several users typing
|
||||
match: /(,{a:(\i),b:(\i),c:\i}\):\i\.length>3&&\(\i=)\i\.\i\.string\(\i\.\i#{intl::SEVERAL_USERS_TYPING}\)(?<=(\i)\.length.+?)/,
|
||||
replace: (_, rest, a, b, users) => `${rest}$self.buildSeveralUsers({ a: ${a}, b: ${b}, count: ${users}.length - 2 })`,
|
||||
replace: (_, rest, a, b, users) =>
|
||||
`${rest}$self.buildSeveralUsers({ a: ${a}, b: ${b}, count: ${users}.length - 2, guildId: arguments[0]?.channel?.guild_id })`,
|
||||
predicate: () => settings.store.alternativeFormatting
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ function VencordPopoutButton() {
|
|||
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
|
||||
children.splice(
|
||||
children.length - 1, 0,
|
||||
<ErrorBoundary noop={true}>
|
||||
<ErrorBoundary noop>
|
||||
<VencordPopoutButton />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,9 +71,14 @@ const openAvatar = (url: string) => openImage(url, 512, 512);
|
|||
const openBanner = (url: string) => openImage(url, 1024);
|
||||
|
||||
function openImage(url: string, width: number, height?: number) {
|
||||
const format = url.startsWith("/") ? "png" : settings.store.format;
|
||||
|
||||
const u = new URL(url, window.location.href);
|
||||
|
||||
const format = url.startsWith("/")
|
||||
? "png"
|
||||
: u.searchParams.get("animated") === "true"
|
||||
? "gif"
|
||||
: settings.store.format;
|
||||
|
||||
u.searchParams.set("size", settings.store.imgSize);
|
||||
u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`);
|
||||
url = u.toString();
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { Margins } from "@utils/margins";
|
|||
import { copyWithToast } from "@utils/misc";
|
||||
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Button, ChannelStore, Forms, GuildStore, Menu, Text } from "@webpack/common";
|
||||
import { Button, ChannelStore, Forms, GuildRoleStore, Menu, Text } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ const devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id:
|
|||
const guild = getCurrentGuild();
|
||||
if (!guild) return;
|
||||
|
||||
const role = GuildStore.getRole(guild.id, id);
|
||||
const role = GuildRoleStore.getRole(guild.id, id);
|
||||
if (!role) return;
|
||||
|
||||
children.push(
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
}
|
||||
|
||||
.vc-vmsg-preview {
|
||||
color: var(--text-normal);
|
||||
color: var(--text-default);
|
||||
border-radius: 24px;
|
||||
background-color: var(--background-secondary);
|
||||
background-color: var(--background-base-lower);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ export default definePlugin({
|
|||
replace: ";b=AS:800000;level-asymmetry-allowed=1"
|
||||
},
|
||||
{
|
||||
match: "useinbandfec=1",
|
||||
replace: "useinbandfec=1;stereo=1;sprop-stereo=1"
|
||||
match: /;usedtx=".concat\((\i)\?"0":"1"\)/,
|
||||
replace: '$&.concat($1?";stereo=1;sprop-stereo=1":"")'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { Devs } from "@utils/constants";
|
|||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
|
||||
import { findByCodeLazy, findLazy } from "@webpack";
|
||||
import { Button, ChannelStore, GuildStore, UserStore } from "@webpack/common";
|
||||
import { Button, ChannelStore, GuildRoleStore, GuildStore, UserStore } from "@webpack/common";
|
||||
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
|
||||
|
||||
const ChannelTypes = findLazy(m => m.ANNOUNCEMENT_THREAD === 10);
|
||||
|
|
@ -267,7 +267,7 @@ export default definePlugin({
|
|||
// color role mentions (unity styling btw lol)
|
||||
if (message.mention_roles.length > 0) {
|
||||
for (const roleId of message.mention_roles) {
|
||||
const role = GuildStore.getRole(channel.guild_id, roleId);
|
||||
const role = GuildRoleStore.getRole(channel.guild_id, roleId);
|
||||
if (!role) continue;
|
||||
const roleColor = role.colorString ?? `#${pingColor}`;
|
||||
finalMsg = finalMsg.replace(`<@&${roleId}>`, `<b><color=${roleColor}>@${role.name}</color></b>`);
|
||||
|
|
|
|||
|
|
@ -17,29 +17,38 @@
|
|||
*/
|
||||
|
||||
export const enum IpcEvents {
|
||||
QUICK_CSS_UPDATE = "VencordQuickCssUpdate",
|
||||
THEME_UPDATE = "VencordThemeUpdate",
|
||||
OPEN_QUICKCSS = "VencordOpenQuickCss",
|
||||
GET_QUICK_CSS = "VencordGetQuickCss",
|
||||
SET_QUICK_CSS = "VencordSetQuickCss",
|
||||
UPLOAD_THEME = "VencordUploadTheme",
|
||||
DELETE_THEME = "VencordDeleteTheme",
|
||||
GET_THEMES_DIR = "VencordGetThemesDir",
|
||||
QUICK_CSS_UPDATE = "VencordQuickCssUpdate",
|
||||
|
||||
GET_SETTINGS = "VencordGetSettings",
|
||||
SET_SETTINGS = "VencordSetSettings",
|
||||
|
||||
GET_THEMES_LIST = "VencordGetThemesList",
|
||||
GET_THEME_DATA = "VencordGetThemeData",
|
||||
GET_THEME_SYSTEM_VALUES = "VencordGetThemeSystemValues",
|
||||
GET_SETTINGS_DIR = "VencordGetSettingsDir",
|
||||
GET_SETTINGS = "VencordGetSettings",
|
||||
SET_SETTINGS = "VencordSetSettings",
|
||||
UPLOAD_THEME = "VencordUploadTheme",
|
||||
DELETE_THEME = "VencordDeleteTheme",
|
||||
THEME_UPDATE = "VencordThemeUpdate",
|
||||
|
||||
OPEN_EXTERNAL = "VencordOpenExternal",
|
||||
OPEN_QUICKCSS = "VencordOpenQuickCss",
|
||||
OPEN_THEMES_FOLDER = "VencordOpenThemesFolder",
|
||||
OPEN_SETTINGS_FOLDER = "VencordOpenSettingsFolder",
|
||||
|
||||
GET_UPDATES = "VencordGetUpdates",
|
||||
GET_REPO = "VencordGetRepo",
|
||||
UPDATE = "VencordUpdate",
|
||||
BUILD = "VencordBuild",
|
||||
|
||||
OPEN_MONACO_EDITOR = "VencordOpenMonacoEditor",
|
||||
|
||||
GET_PLUGIN_IPC_METHOD_MAP = "VencordGetPluginIpcMethodMap",
|
||||
|
||||
OPEN_IN_APP__RESOLVE_REDIRECT = "VencordOIAResolveRedirect",
|
||||
VOICE_MESSAGES_READ_RECORDING = "VencordVMReadRecording",
|
||||
|
||||
CSP_IS_DOMAIN_ALLOWED = "VencordCspIsDomainAllowed",
|
||||
CSP_REMOVE_OVERRIDE = "VencordCspRemoveOverride",
|
||||
CSP_REQUEST_ADD_OVERRIDE = "VencordCspRequestAddOverride",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export class SettingsStore<T extends object> {
|
|||
// So, we need to extract the top-level setting path (plugins.pluginName.settingName),
|
||||
// to be able to notify globalListeners and top-level setting name listeners (let { settingName } = settings.use(["settingName"]),
|
||||
// with the new value
|
||||
if (paths.length > 2 && paths[0] === "plugins") {
|
||||
if (paths.length > 3 && paths[0] === "plugins") {
|
||||
const settingPath = paths.slice(0, 3);
|
||||
const settingPathStr = settingPath.join(".");
|
||||
const settingValue = settingPath.reduce((acc, curr) => acc[curr], root);
|
||||
|
|
|
|||
|
|
@ -19,15 +19,40 @@
|
|||
import * as DataStore from "@api/DataStore";
|
||||
import { showNotification } from "@api/Notifications";
|
||||
import { Settings } from "@api/Settings";
|
||||
import { OAuth2AuthorizeModal, UserStore } from "@webpack/common";
|
||||
import { Alerts, OAuth2AuthorizeModal, UserStore } from "@webpack/common";
|
||||
|
||||
import { Logger } from "./Logger";
|
||||
import { openModal } from "./modal";
|
||||
import { relaunch } from "./native";
|
||||
|
||||
export const cloudLogger = new Logger("Cloud", "#39b7e0");
|
||||
export const getCloudUrl = () => new URL(Settings.cloud.url);
|
||||
|
||||
const cloudUrlOrigin = () => getCloudUrl().origin;
|
||||
export const getCloudUrl = () => new URL(Settings.cloud.url);
|
||||
const getCloudUrlOrigin = () => getCloudUrl().origin;
|
||||
|
||||
export async function checkCloudUrlCsp() {
|
||||
if (IS_WEB) return true;
|
||||
|
||||
const { host } = getCloudUrl();
|
||||
if (host === "api.vencord.dev") return true;
|
||||
|
||||
if (await VencordNative.csp.isDomainAllowed(Settings.cloud.url, ["connect-src"])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const res = await VencordNative.csp.requestAddOverride(Settings.cloud.url, ["connect-src"], "Cloud Sync");
|
||||
if (res === "ok") {
|
||||
Alerts.show({
|
||||
title: "Cloud Integration enabled",
|
||||
body: `${host} has been added to the whitelist. Please restart the app for the changes to take effect.`,
|
||||
confirmText: "Restart now",
|
||||
cancelText: "Later!",
|
||||
onConfirm: relaunch
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const getUserId = () => {
|
||||
const id = UserStore.getCurrentUser()?.id;
|
||||
if (!id) throw new Error("User not yet logged in");
|
||||
|
|
@ -37,7 +62,7 @@ const getUserId = () => {
|
|||
export async function getAuthorization() {
|
||||
const secrets = await DataStore.get<Record<string, string>>("Vencord_cloudSecret") ?? {};
|
||||
|
||||
const origin = cloudUrlOrigin();
|
||||
const origin = getCloudUrlOrigin();
|
||||
|
||||
// we need to migrate from the old format here
|
||||
if (secrets[origin]) {
|
||||
|
|
@ -59,7 +84,7 @@ export async function getAuthorization() {
|
|||
async function setAuthorization(secret: string) {
|
||||
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
||||
secrets ??= {};
|
||||
secrets[`${cloudUrlOrigin()}:${getUserId()}`] = secret;
|
||||
secrets[`${getCloudUrlOrigin()}:${getUserId()}`] = secret;
|
||||
return secrets;
|
||||
});
|
||||
}
|
||||
|
|
@ -67,7 +92,7 @@ async function setAuthorization(secret: string) {
|
|||
export async function deauthorizeCloud() {
|
||||
await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
|
||||
secrets ??= {};
|
||||
delete secrets[`${cloudUrlOrigin()}:${getUserId()}`];
|
||||
delete secrets[`${getCloudUrlOrigin()}:${getUserId()}`];
|
||||
return secrets;
|
||||
});
|
||||
}
|
||||
|
|
@ -78,6 +103,8 @@ export async function authorizeCloud() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!await checkCloudUrlCsp()) return;
|
||||
|
||||
try {
|
||||
const oauthConfiguration = await fetch(new URL("/v1/oauth/settings", getCloudUrl()));
|
||||
var { clientId, redirectUri } = await oauthConfiguration.json();
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export interface Dev {
|
|||
*/
|
||||
export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
Ven: {
|
||||
name: "Vee",
|
||||
name: "V",
|
||||
id: 343383572805058560n
|
||||
},
|
||||
Arjix: {
|
||||
|
|
@ -194,7 +194,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
},
|
||||
axyie: {
|
||||
name: "'ax",
|
||||
id: 273562710745284628n,
|
||||
id: 929877747151548487n,
|
||||
},
|
||||
pointy: {
|
||||
name: "pointy",
|
||||
|
|
@ -587,7 +587,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
},
|
||||
samsam: {
|
||||
name: "samsam",
|
||||
id: 836452332387565589n,
|
||||
id: 400482410279469056n,
|
||||
},
|
||||
Cootshk: {
|
||||
name: "Cootshk",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue