Compare commits

...

39 Commits

Author SHA1 Message Date
Vendicated
4aa7a052d0 Bump to v1.1.5 2023-04-04 21:29:39 +02:00
Vendicated
f088f17a0a Remove accidently introduced patch 2023-04-04 21:28:38 +02:00
Vendicated
a55c758b0e Fix SpotifyControls 2023-04-04 21:27:44 +02:00
Vendicated
f092f434fe Fix Vencord 2023-04-04 21:14:55 +02:00
Remty
2e6dfaa879 FakeProfileThemes: add usage guide (#778)
Co-authored-by: V <vendicated@riseup.net>
2023-04-04 13:28:41 +00:00
Nuckyz
96dc2e12d0 Fix Web & Game Activity Toggle (#777) 2023-04-04 15:26:53 +02:00
Đỗ Văn Hoài Tuân
d931790ed0 BetterFolders: Fix unread indicator & read all buttons being duplicated (#776) 2023-04-04 05:33:11 +02:00
V
6b26c12bfa Add additional build flavours for Vencord Desktop (#765) 2023-04-04 01:16:29 +02:00
Vendicated
5bb08bdb64 SpotifyControls: Fix crashing on canary
Vencord is still pretty broken on Canary and likely will be for a bit,
but this should at least fix instantly crashing
2023-04-03 21:25:14 +02:00
Vendicated
405be7ef13 Fix weird style on username sheet 2023-04-03 03:13:54 +02:00
Vendicated
a7e2fb48ba fix oopsie 2023-04-03 02:36:54 +02:00
Nuckyz
ae80749dd8 Game Activity Toggle and SettingsStoreAPI (#587) 2023-04-03 02:13:44 +02:00
Vendicated
8c47b7080d QuickReply & Up Key: Do not attempt to edit/reply to logged deleted message 2023-04-02 22:14:58 +02:00
Juby210
8378638ee4 BetterFolders: fix mentions display (#761)
closes #759
2023-04-02 20:31:10 +02:00
V
7c563471f6 Fix typo 2023-04-02 18:31:23 +02:00
Juby210
29382d2781 Add BetterFolders plugin (#530)
Co-authored-by: Ven <vendicated@riseup.net>
2023-04-02 17:43:06 +02:00
Vendicated
6226672ee8 Web: Update extension icon from trolley to Vencord logo 2023-04-02 16:55:36 +02:00
V
5b5ee82f27 Update Contributor Badge to new logo 2023-04-02 16:16:15 +02:00
Remty
62f74f5917 feat(plugin): FakeProfileThemes (#710) 2023-04-02 16:12:19 +02:00
V
265c7a18a7 Delete corruptMp4s.ts
Discord/Electron fixed this bug, so mp4s created by this plugin just look normal on Electron 22, not fixable
2023-04-02 04:33:17 +02:00
Vendicated
462f191051 Bump to v1.1.4 2023-04-02 04:26:05 +02:00
V
6960a439c9 Add Notification log (#745) 2023-04-01 02:47:49 +02:00
Vendicated
4dff1c5bd5 RelationShipNotifier: Delay by 5s to fix false positives 2023-03-31 17:17:50 +02:00
nick
2c8ebdce7d feat(plugin): RelationshipNotifier (#450)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-31 05:07:35 +00:00
Nuckyz
dae7cb67ef Fix IgnoreActivities broken patch (#743) 2023-03-31 04:11:15 +00:00
Berlin
081b01b667 feat(plugin): Wikisearch (#585)
Co-authored-by: Ven <vendicated@riseup.net>
2023-03-31 04:09:19 +00:00
Vendicated
5340ea7ba0 Add back window transparency with temporary unsafe settings key 2023-03-31 05:59:45 +02:00
Vendicated
84a649a671 docs: fix ToC 2023-03-31 05:56:08 +02:00
Vendicated
efd9927696 Fix broken plugins 2023-03-31 05:55:25 +02:00
V
c86a34a15d Update 1_INSTALLING.md 2023-03-31 05:30:45 +02:00
Vendicated
ff16513f21 Fix onHeadersReceived clashes when using OpenAsar (fix github raw styles) 2023-03-31 01:18:57 +02:00
Vendicated
906c265aea FakeNitro: Fix fake emote rendering incorrectly in thread previews 2023-03-31 00:15:51 +02:00
Vendicated
708c16176b Remove transparency feature
This not only causes incredibly confusion among users because they
expect it to work without themes, it also causes freezes/whitescreens
for some users. Thus, this feature is disabled for now until someone
contributes a fix!
2023-03-30 23:48:26 +02:00
whqwert
035d1e24b2 feat(SpotifyControls): Fix background color for built-in themes (#731)
Co-authored-by: V <vendicated@riseup.net>
2023-03-30 17:09:04 +02:00
Vendicated
48e9b1be7a new Plugin: GifPaste - Insert Gif links instead of sending 2023-03-30 15:58:20 +02:00
Vendicated
6acdaf207d NoTrack: Update description & authors 2023-03-30 01:41:18 +02:00
Vendicated
9d41b360c9 Fix NoTrack 2023-03-30 01:35:42 +02:00
Vendicated
12cbd73e7f SpotifyControls: Add right click menus to title/album/artists 2023-03-30 01:29:34 +02:00
Phil
420b068094 Fix makeProxy returning stale proxies after assigning objects (#722) 2023-03-28 18:26:57 +00:00
85 changed files with 2014 additions and 543 deletions

View File

@ -42,7 +42,7 @@ jobs:
- name: Clean up obsolete files
run: |
rm -rf dist/extension* Vencord.user.css
rm -rf dist/extension* Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
- name: Get some values needed for the release
id: release_values

View File

@ -50,7 +50,7 @@ jobs:
export CHROMIUM_BIN=$(which chromium-browser)
export USE_CANARY=true
esbuild test/generateReport.ts > dist/report.mjs
esbuild scripts/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}

View File

@ -6,7 +6,7 @@ The cutest Discord client mod
- Super easy to install (Download Installer, open, click install button, done)
- 100+ plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
- Some highlights: SpotifyControls, GameActivityToggle, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, CustomCommands, ShowHiddenChannels, PronounDB
- 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!)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -13,12 +13,6 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
- [Installing Vencord](#installing-vencord)
- [Updating Vencord](#updating-vencord)
- [Uninstalling Vencord](#uninstalling-vencord)
- [Manually Installing Vencord](#manually-installing-vencord)
- [On Windows](#on-windows)
- [On Linux](#on-linux)
- [On MacOS](#on-macos)
- [Manual Patching](#manual-patching)
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
## Dependencies
@ -27,11 +21,9 @@ Welcome to Megu's Installation Guide! In this file, you will learn about how to
## Installing Vencord
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
Install `pnpm`:
> :exclamation: This next command may need to be run as admin/sudo depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
> :exclamation: This next command may need to be run as admin/root depending on your system, and you may need to close and reopen your terminal for pnpm to be in your PATH.
```shell
npm i -g pnpm
@ -103,102 +95,4 @@ Simply run:
pnpm uninject
```
The above command may ask you to also run:
```shell
pnpm install --frozen-lockfile
pnpm uninject
```
## Manually Installing Vencord
- [Windows](#on-windows)
- [Linux](#on-linux)
- [MacOS](#on-macos)
### On Windows
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
Now follow the instructions at [Manual Patching](#manual-patching)
### On Linux
The Discord folder is usually in one of the following paths:
- /usr/share
- /usr/lib64
- /opt
- /home/$USER/.local/share
If you use flatpak, it will usually be in one of the following paths:
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
You will need to give flatpak access to vencord with one of the following commands:
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
If Discord flatpak install is in /home/:
```shell
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
```
If Discord flatpak install not in /home/:
```shell
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
```
Now follow the instructions at [Manual Patching](#manual-patching)
### On MacOS
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
Go to the `Contents/Resources` folder.
Now follow the instructions at [Manual Patching](#manual-patching)
### Manual Patching
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
Make a new folder in `resources` called `app`. In here, we will make two files:
`package.json` and `index.js`
In `index.js`:
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
```js
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
```
And in `package.json`:
```json
{ "name": "discord", "main": "index.js" }
```
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
### Manually Uninstalling Vencord
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

View File

@ -1,9 +1,8 @@
{
"name": "vencord",
"private": "true",
"version": "1.1.3",
"version": "1.1.5",
"description": "The cutest Discord client mod",
"keywords": [ ],
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
"url": "https://github.com/Vendicated/Vencord/issues"
@ -34,7 +33,9 @@
"dependencies": {
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.3",
"fflate": "^0.7.4"
"fflate": "^0.7.4",
"nanoid": "^4.0.2",
"virtual-merge": "^1.0.1"
},
"devDependencies": {
"@types/diff": "^5.0.2",

14
pnpm-lock.yaml generated
View File

@ -31,6 +31,7 @@ specifiers:
fflate: ^0.7.4
highlight.js: 10.6.0
moment: ^2.29.4
nanoid: ^4.0.2
puppeteer-core: ^19.6.0
standalone-electron-types: ^1.0.0
stylelint: ^14.16.1
@ -38,11 +39,14 @@ specifiers:
tsx: ^3.12.6
type-fest: ^3.5.3
typescript: ^4.9.4
virtual-merge: ^1.0.1
dependencies:
'@vap/core': 0.0.12
'@vap/shiki': 0.10.3
fflate: 0.7.4
nanoid: 4.0.2
virtual-merge: 1.0.1
devDependencies:
'@types/diff': 5.0.2
@ -2245,6 +2249,12 @@ packages:
hasBin: true
dev: true
/nanoid/4.0.2:
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
engines: {node: ^14 || ^16 || >=18}
hasBin: true
dev: false
/nanomatch/1.2.13:
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
engines: {node: '>=0.10.0'}
@ -3123,6 +3133,10 @@ packages:
spdx-expression-parse: 3.0.1
dev: true
/virtual-merge/1.0.1:
resolution: {integrity: sha512-h7rzV6n5fZJbDu2lP4iu+IOtsZ00uqECFUxFePK1uY0pz/S5B7FNDJpmdDVfyGL7poyJECEHfTaIpJaknNkU0Q==}
dev: false
/vscode-oniguruma/1.7.0:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
dev: false

View File

@ -48,6 +48,7 @@ const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.j
const sourcemap = watch ? "inline" : "external";
await Promise.all([
// common preload
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
@ -55,12 +56,19 @@ await Promise.all([
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
}),
// Discord Desktop main & renderer
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/patcher.ts"],
entryPoints: ["src/main/index.ts"],
outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: true,
IS_VENCORD_DESKTOP: false
}
}),
esbuild.build({
...commonOpts,
@ -77,7 +85,43 @@ await Promise.all([
],
define: {
...defines,
IS_WEB: false
IS_WEB: false,
IS_DISCORD_DESKTOP: true,
IS_VENCORD_DESKTOP: false
}
}),
// Vencord Desktop main & renderer
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/main/index.ts"],
outfile: "dist/vencordDesktopMain.js",
footer: { js: "//# sourceURL=VencordDesktopMain\n" + sourceMapFooter("vencordDesktopMain") },
sourcemap,
define: {
...defines,
IS_DISCORD_DESKTOP: false,
IS_VENCORD_DESKTOP: true
}
}),
esbuild.build({
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/vencordDesktopRenderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordDesktopRenderer\n" + sourceMapFooter("vencordDesktopRenderer") },
globalName: "Vencord",
sourcemap,
plugins: [
globPlugins,
...commonOpts.plugins
],
define: {
...defines,
IS_WEB: false,
IS_DISCORD_DESKTOP: false,
IS_VENCORD_DESKTOP: true
}
}),
]).catch(err => {

View File

@ -36,7 +36,7 @@ const commonOptions = {
entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
format: "iife",
external: ["plugins", "git-hash"],
external: ["plugins", "git-hash", "/assets/*"],
plugins: [
globPlugins,
...commonOpts.plugins,
@ -45,7 +45,9 @@ const commonOptions = {
define: {
IS_WEB: "true",
IS_STANDALONE: "true",
IS_DEV: JSON.stringify(watch)
IS_DEV: JSON.stringify(watch),
IS_DISCORD_DESKTOP: "false",
IS_VENCORD_DESKTOP: "false"
}
};

View File

@ -193,7 +193,7 @@ export const commonOpts = {
legalComments: "linked",
banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin, stylePlugin],
external: ["~plugins", "~git-hash", "~git-remote"],
external: ["~plugins", "~git-hash", "~git-remote", "/assets/*"],
inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment",

View File

@ -30,7 +30,7 @@ import "./webpack/patchWebpack";
import { showNotification } from "./api/Notifications";
import { PlainSettings, Settings } from "./api/settings";
import { patches, PMLogger, startAllPlugins } from "./plugins";
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
import { checkForUpdates, rebuild, update,UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common";
@ -54,9 +54,14 @@ async function init() {
title: "Vencord has been updated!",
body: "Click here to restart",
permanent: true,
noPersist: true,
onClick() {
if (needsFullRestart)
window.DiscordNative.app.relaunch();
if (needsFullRestart) {
if (IS_DISCORD_DESKTOP)
window.DiscordNative.app.relaunch();
else
window.VencordDesktop.app.relaunch();
}
else
location.reload();
}
@ -69,6 +74,7 @@ async function init() {
title: "A Vencord update is available!",
body: "Click here to view the update",
permanent: true,
noPersist: true,
onClick() {
SettingsRouter.open("VencordUpdater");
}
@ -94,7 +100,7 @@ async function init() {
init();
if (!IS_WEB && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
document.addEventListener("DOMContentLoaded", () => {
document.head.append(Object.assign(document.createElement("style"), {
id: "vencord-native-titlebar-style",

View File

@ -20,6 +20,7 @@ import "./styles.css";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { classes } from "@utils/misc";
import { React, useEffect, useMemo, useState, useStateFromStores, WindowStore } from "@webpack/common";
import { NotificationData } from "./Notifications";
@ -33,8 +34,10 @@ export default ErrorBoundary.wrap(function NotificationComponent({
onClick,
onClose,
image,
permanent
}: NotificationData) {
permanent,
className,
dismissOnClick
}: NotificationData & { className?: string; }) {
const { timeout, position } = useSettings(["notifications.timeout", "notifications.position"]).notifications;
const hasFocus = useStateFromStores([WindowStore], () => WindowStore.isFocused());
@ -61,11 +64,12 @@ export default ErrorBoundary.wrap(function NotificationComponent({
return (
<button
className="vc-notification-root"
className={classes("vc-notification-root", className)}
style={position === "bottom-right" ? { bottom: "1rem" } : { top: "3rem" }}
onClick={() => {
onClose!();
onClick?.();
if (dismissOnClick !== false)
onClose!();
}}
onContextMenu={e => {
e.preventDefault();

View File

@ -23,6 +23,7 @@ import type { ReactNode } from "react";
import type { Root } from "react-dom/client";
import NotificationComponent from "./NotificationComponent";
import { persistNotification } from "./notificationLog";
const NotificationQueue = new Queue();
@ -56,6 +57,10 @@ export interface NotificationData {
color?: string;
/** Whether this notification should not have a timeout */
permanent?: boolean;
/** Whether this notification should not be persisted in the Notification Log */
noPersist?: boolean;
/** Whether this notification should be dismissed when clicked (defaults to true) */
dismissOnClick?: boolean;
}
function _showNotification(notification: NotificationData, id: number) {
@ -86,6 +91,8 @@ export async function requestPermission() {
}
export async function showNotification(data: NotificationData) {
persistNotification(data);
if (shouldBeNative() && await requestPermission()) {
const { title, body, icon, image, onClick = null, onClose = null } = data;
const n = new Notification(title, {

View File

@ -0,0 +1,203 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 * as DataStore from "@api/DataStore";
import { Settings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import { useAwaiter } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Alerts, Button, Forms, moment, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { nanoid } from "nanoid";
import type { DispatchWithoutAction } from "react";
import NotificationComponent from "./NotificationComponent";
import type { NotificationData } from "./Notifications";
interface PersistentNotificationData extends Pick<NotificationData, "title" | "body" | "image" | "icon" | "color"> {
timestamp: number;
id: string;
}
const KEY = "notification-log";
const getLog = async () => {
const log = await DataStore.get(KEY) as PersistentNotificationData[] | undefined;
return log ?? [];
};
const cl = classNameFactory("vc-notification-log-");
const signals = new Set<DispatchWithoutAction>();
export async function persistNotification(notification: NotificationData) {
if (notification.noPersist) return;
const limit = Settings.notifications.logLimit;
if (limit === 0) return;
await DataStore.update(KEY, (old: PersistentNotificationData[] | undefined) => {
const log = old ?? [];
// Omit stuff we don't need
const {
onClick, onClose, richBody, permanent, noPersist, dismissOnClick,
...pureNotification
} = notification;
log.unshift({
...pureNotification,
timestamp: Date.now(),
id: nanoid()
});
if (log.length > limit && limit !== 200)
log.length = limit;
return log;
});
signals.forEach(x => x());
}
export async function deleteNotification(timestamp: number) {
const log = await getLog();
const index = log.findIndex(x => x.timestamp === timestamp);
if (index === -1) return;
log.splice(index, 1);
await DataStore.set(KEY, log);
signals.forEach(x => x());
}
export function useLogs() {
const [signal, setSignal] = useReducer(x => x + 1, 0);
useEffect(() => {
signals.add(setSignal);
return () => void signals.delete(setSignal);
}, []);
const [log, _, pending] = useAwaiter(getLog, {
fallbackValue: [],
deps: [signal]
});
return [log, pending] as const;
}
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
const [removing, setRemoving] = useState(false);
const ref = React.useRef<HTMLDivElement>(null);
useEffect(() => {
const div = ref.current!;
const setHeight = () => {
if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
div.style.height = `${div.clientHeight}px`;
};
setHeight();
}, []);
return (
<div className={cl("wrapper", { removing })} ref={ref}>
<NotificationComponent
{...data}
permanent={true}
dismissOnClick={false}
onClose={() => {
if (removing) return;
setRemoving(true);
setTimeout(() => deleteNotification(data.timestamp), 200);
}}
richBody={
<div className={cl("body")}>
{data.body}
<Timestamp timestamp={moment(data.timestamp)} className={cl("timestamp")} />
</div>
}
/>
</div>
);
}
export function NotificationLog({ log, pending }: { log: PersistentNotificationData[], pending: boolean; }) {
if (!log.length && !pending)
return (
<div className={cl("container")}>
<div className={cl("empty")} />
<Forms.FormText style={{ textAlign: "center" }}>
No notifications yet
</Forms.FormText>
</div>
);
return (
<div className={cl("container")}>
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
</div>
);
}
function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void; }) {
const [log, pending] = useLogs();
return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
<ModalCloseButton onClick={close} />
</ModalHeader>
<ModalContent>
<NotificationLog log={log} pending={pending} />
</ModalContent>
<ModalFooter>
<Button
disabled={log.length === 0}
onClick={() => {
Alerts.show({
title: "Are you sure?",
body: `This will permanently remove ${log.length} notification${log.length === 1 ? "" : "s"}. This action cannot be undone.`,
async onConfirm() {
await DataStore.set(KEY, []);
signals.forEach(x => x());
},
confirmText: "Do it!",
confirmColor: "vc-notification-log-danger-btn",
cancelText: "Nevermind"
});
}}
>
Clear Notification Log
</Button>
</ModalFooter>
</ModalRoot>
);
}
export function openNotificationLogModal() {
const key = openModal(modalProps => (
<LogModal
modalProps={modalProps}
close={() => closeModal(key)}
/>
));
}

View File

@ -3,16 +3,20 @@
all: unset;
display: flex;
flex-direction: column;
width: 25vw;
min-height: 10vh;
color: var(--text-normal);
background-color: var(--background-secondary-alt);
position: absolute;
z-index: 2147483647;
right: 1rem;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
width: 100%;
}
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
position: absolute;
z-index: 2147483647;
right: 1rem;
width: 25vw;
min-height: 10vh;
}
.vc-notification {
@ -72,3 +76,47 @@
.vc-notification-img {
width: 100%;
}
.vc-notification-log-empty {
height: 218px;
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
margin-bottom: 40px;
}
.vc-notification-log-container {
display: flex;
flex-direction: column;
padding: 1em;
overflow: hidden;
}
.vc-notification-log-wrapper {
transition: 200ms ease;
transition-property: height, opacity;
}
.vc-notification-log-wrapper:not(:last-child) {
margin-bottom: 1em;
}
.vc-notification-log-removing {
height: 0 !important;
opacity: 0;
margin-bottom: 1em;
}
.vc-notification-log-body {
display: flex;
flex-direction: column;
}
.vc-notification-log-timestamp {
margin-left: auto;
font-size: 0.8em;
font-weight: lighter;
}
.vc-notification-log-danger-btn {
color: var(--white-500);
background-color: var(--button-danger-background);
}

69
src/api/SettingsStore.ts Normal file
View File

@ -0,0 +1,69 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 Logger from "@utils/Logger";
import { proxyLazy } from "@utils/proxyLazy";
import { findModuleId, wreq } from "@webpack";
import { Settings } from "./settings";
interface Setting<T> {
/**
* Get the setting value
*/
getSetting(): T;
/**
* Update the setting value
* @param value The new value
*/
updateSetting(value: T | ((old: T) => T)): Promise<void>;
/**
* React hook for automatically updating components when the setting is updated
*/
useSetting(): T;
settingsStoreApiGroup: string;
settingsStoreApiName: string;
}
const SettingsStores: Array<Setting<any>> | undefined = proxyLazy(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"');
if (modId == null) return new Logger("SettingsStoreAPI").error("Didn't find stores module.");
const mod = wreq(modId);
if (mod == null) return;
return Object.values(mod).filter((s: any) => s?.settingsStoreApiGroup) as any;
});
/**
* Get the store for a setting
* @param group The setting group
* @param name The name of the setting
*/
export function getSettingStore<T = any>(group: string, name: string): Setting<T> | undefined {
if (!Settings.plugins.SettingsStoreAPI.enabled) throw new Error("Cannot use SettingsStoreAPI without setting as dependency.");
return SettingsStores?.find(s => s?.settingsStoreApiGroup === group && s?.settingsStoreApiName === name);
}
/**
* getSettingStore but lazy
*/
export function getSettingStoreLazy<T = any>(group: string, name: string) {
return proxyLazy(() => getSettingStore<T>(group, name));
}

View File

@ -28,6 +28,7 @@ import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
import * as $SettingsStore from "./SettingsStore";
import * as $Styles from "./Styles";
/**
@ -85,6 +86,10 @@ export const MessageDecorations = $MessageDecorations;
* An API allowing you to add components to member list users, in both DM's and servers
*/
export const MemberListDecorators = $MemberListDecorators;
/**
* An API allowing you to read, manipulate and automatically update components based on Discord settings
*/
export const SettingsStore = $SettingsStore;
/**
* An API allowing you to dynamically load styles
* a

View File

@ -47,6 +47,7 @@ export interface Settings {
timeout: number;
position: "top-right" | "bottom-right";
useNative: "always" | "never" | "not-focused";
logLimit: number;
};
}
@ -66,7 +67,8 @@ const DefaultSettings: Settings = {
notifications: {
timeout: 5000,
position: "bottom-right",
useNative: "not-focused"
useNative: "not-focused",
logLimit: 50
}
};
@ -133,6 +135,7 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
target[p] = v;
// Call any listeners that are listening to a setting of this path
const setPath = `${path}${path && "."}${p}`;
delete proxyCache[setPath];
for (const subscription of subscriptions) {
if (!subscription._path || subscription._path === setPath) {
subscription(v, setPath);

View File

@ -46,6 +46,7 @@ const cl = classNameFactory("vc-plugins-");
const logger = new Logger("PluginSettings", "#a6d189");
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
const ButtonClasses = findByPropsLazy("button", "disabled", "enabled");
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
@ -154,7 +155,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
<Text variant="text-md/bold" className={cl("name")}>
{plugin.name}{isNew && <Badge text="NEW" color="#ED4245" />}
</Text>
<button role="switch" onClick={() => openModal()} className={classes("button-12Fmur", cl("info-button"))}>
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
{plugin.options
? <CogWheel />
: <InfoIcon width="24" height="24" />}

View File

@ -24,6 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { classes, useAwaiter } from "@utils/misc";
import { relaunch } from "@utils/native";
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
@ -133,7 +134,7 @@ function Updatable(props: CommonProps) {
cancelText: "Not now!",
onConfirm() {
if (needFullRestart)
window.DiscordNative.app.relaunch();
relaunch();
else
location.reload();
r();

View File

@ -17,6 +17,7 @@
*/
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
@ -25,6 +26,7 @@ import { ErrorCard } from "@components/ErrorCard";
import IpcEvents from "@utils/IpcEvents";
import { Margins } from "@utils/margins";
import { identity, useAwaiter } from "@utils/misc";
import { relaunch } from "@utils/native";
import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common";
const cl = classNameFactory("vc-settings-");
@ -72,7 +74,7 @@ function VencordSettings() {
title: "Use Windows' native title bar instead of Discord's custom one",
note: "Requires a full restart"
}),
!IS_WEB && {
!IS_WEB && false /* This causes electron to freeze / white screen for some people */ && {
key: "transparent",
title: "Enable window transparency",
note: "Requires a full restart"
@ -99,7 +101,7 @@ function VencordSettings() {
) : (
<React.Fragment>
<Button
onClick={() => window.DiscordNative.app.relaunch()}
onClick={relaunch}
size={Button.Sizes.SMALL}>
Restart Client
</Button>
@ -110,6 +112,7 @@ function VencordSettings() {
Open QuickCSS File
</Button>
<Button
// FIXME: Vencord Desktop support
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
size={Button.Sizes.SMALL}
disabled={settingsDirPending}>
@ -165,7 +168,7 @@ function VencordSettings() {
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" },
]satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
] satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>}
closeOnSelect={true}
select={v => notifSettings.useNative = v}
isSelected={v => v === notifSettings.useNative}
@ -179,7 +182,7 @@ function VencordSettings() {
options={[
{ label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" },
]satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
] satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>}
select={v => notifSettings.position = v}
isSelected={v => v === notifSettings.position}
serialize={identity}
@ -198,6 +201,29 @@ function VencordSettings() {
onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false}
/>
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Log Limit</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>
The amount of notifications to save in the log until old ones are removed.
Set to <code>0</code> to disable Notification log and <code></code> to never automatically remove old Notifications
</Forms.FormText>
<Slider
markers={[0, 25, 50, 75, 100, 200]}
minValue={0}
maxValue={200}
stickToMarkers={true}
initialValue={notifSettings.logLimit}
onValueChange={v => notifSettings.logLimit = v}
onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v}
/>
<Button
onClick={openNotificationLogModal}
disabled={notifSettings.logLimit === 0}
>
Open Notification Log
</Button>
</React.Fragment>
);
}

View File

@ -21,8 +21,7 @@ import "./settingsStyles.css";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { findByCodeLazy } from "@webpack";
import { Forms, SettingsRouter, Text } from "@webpack/common";
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
import BackupRestoreTab from "./BackupRestoreTab";
import PluginsTab from "./PluginsTab";
@ -32,8 +31,6 @@ import VencordSettings from "./VencordTab";
const cl = classNameFactory("vc-settings-");
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
interface SettingsProps {
tab: string;
}

3
src/globals.d.ts vendored
View File

@ -35,6 +35,8 @@ declare global {
export var IS_WEB: boolean;
export var IS_DEV: boolean;
export var IS_STANDALONE: boolean;
export var IS_DISCORD_DESKTOP: boolean;
export var IS_VENCORD_DESKTOP: boolean;
export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord");
@ -54,6 +56,7 @@ declare global {
* If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x)
*/
export var DiscordNative: any;
export var VencordDesktop: any;
interface Window {
webpackChunkdiscord_app: {

109
src/main/index.ts Normal file
View File

@ -0,0 +1,109 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { app, protocol, session } from "electron";
import { join } from "path";
import { getSettings } from "./ipcMain";
import { IS_VANILLA } from "./utils/constants";
import { installExt } from "./utils/extensions";
if (IS_VENCORD_DESKTOP || !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);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map": // doubt
case "main.js.map":
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
}
});
try {
if (getSettings().enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
// 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("; ");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] = ["*", "blob:", "data:", "'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://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
}
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["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 = () => { };
});
}
if (IS_DISCORD_DESKTOP) {
require("./patcher");
}

View File

@ -28,7 +28,7 @@ import { join } from "path";
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants";
mkdirSync(SETTINGS_DIR, { recursive: true });
@ -44,6 +44,14 @@ export function readSettings() {
}
}
export function getSettings(): typeof import("@api/settings").Settings {
try {
return JSON.parse(readSettings());
} catch {
return {} as any;
}
}
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {

View File

@ -20,9 +20,8 @@ import { onceDefined } from "@utils/onceDefined";
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
import { dirname, join } from "path";
import { initIpc } from "./ipcMain";
import { installExt } from "./ipcMain/extensions";
import { readSettings } from "./ipcMain/index";
import { getSettings, initIpc } from "./ipcMain";
import { IS_VANILLA } from "./utils/constants";
console.log("[Vencord] Starting up...");
@ -41,11 +40,8 @@ require.main!.filename = join(asarPath, discordPkg.main);
// @ts-ignore Untyped method? Dies from cringe
app.setAppPath(asarPath);
if (!process.argv.includes("--vanilla")) {
let settings: typeof import("@api/settings").Settings = {} as any;
try {
settings = JSON.parse(readSettings());
} catch { }
if (!IS_VANILLA) {
const settings = getSettings();
// Repatch after host updates on Windows
if (process.platform === "win32") {
@ -83,7 +79,8 @@ if (!process.argv.includes("--vanilla")) {
delete options.frame;
}
if (settings.transparent) {
// This causes electron to freeze / white screen for some people
if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) {
options.transparent = true;
options.backgroundColor = "#00000000";
}
@ -115,79 +112,6 @@ if (!process.argv.includes("--vanilla")) {
);
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
electron.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
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map": // doubt
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
}
});
try {
if (settings?.enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
// 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("; ");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] = ["*", "blob:", "data:", "'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://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
}
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["text/css"];
}
cb({ cancel: false, responseHeaders });
});
});
} else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
}

View File

@ -25,7 +25,7 @@ import { join } from "path";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
import { get } from "../simpleGet";
import { get } from "../utils/simpleGet";
import { calculateHashes, serializeErrors } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
@ -57,6 +57,13 @@ async function calculateGitChanges() {
}));
}
const FILES_TO_DOWNLOAD = [
IS_DISCORD_DESKTOP ? "patcher.js" : "vencordDesktopMain.js",
"preload.js",
IS_DISCORD_DESKTOP ? "renderer.js" : "vencordDesktopRenderer.js",
"renderer.css"
];
async function fetchUpdates() {
const release = await githubGet("/releases/latest");
@ -66,7 +73,7 @@ async function fetchUpdates() {
return false;
data.assets.forEach(({ name, browser_download_url }) => {
if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) {
if (FILES_TO_DOWNLOAD.some(s => name.startsWith(s))) {
PendingUpdates.push([name, browser_download_url]);
}
});
@ -75,8 +82,17 @@ async function fetchUpdates() {
async function applyUpdates() {
await Promise.all(PendingUpdates.map(
async ([name, data]) => writeFile(join(__dirname, name), await get(data)))
);
async ([name, data]) => writeFile(
join(
__dirname,
IS_VENCORD_DESKTOP
// vencordDesktopRenderer.js -> renderer.js
? name.replace(/vencordDesktop(\w)/, (_, c) => c.toLowerCase())
: name
),
await get(data)
)
));
PendingUpdates = [];
return true;
}

View File

@ -33,3 +33,5 @@ export const ALLOWED_PROTOCOLS = [
"steam:",
"spotify:"
];
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");

View File

@ -29,7 +29,7 @@ import { closeModal, Modals, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Forms } from "@webpack/common";
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/1033680203433660458/1092089947126780035/favicon.png";
/** List of vencord contributor IDs */
const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString());

View File

@ -1,82 +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 { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
// duplicate values have multiple branches with different types. Just include all to be safe
const nameMap = {
radio: "MenuRadioItem",
separator: "MenuSeparator",
checkbox: "MenuCheckboxItem",
groupstart: "MenuGroup",
control: "MenuControlItem",
compositecontrol: "MenuControlItem",
item: "MenuItem",
customitem: "MenuItem",
};
migratePluginSettings("MenuItemDeobfuscatorAPI", "MenuItemDeobfuscatorApi");
export default definePlugin({
name: "MenuItemDeobfuscatorAPI",
description: "Deobfuscates Discord's Menu Item module",
authors: [Devs.Ven],
patches: [
{
find: '"Menu API',
replacement: {
match: /function.{0,80}type===(\i)\).{0,50}navigable:.+?Menu API/s,
replace: (m, mod) => {
let nicenNames = "";
const redefines = [] as string[];
// if (t.type === m.MenuItem)
const typeCheckRe = /\(.{1,3}\.type===(.{1,5})\)/g;
// push({type:"item"})
const pushTypeRe = /type:"(\w+)"/g;
let typeMatch: RegExpExecArray | null;
// for each if (t.type === ...)
while ((typeMatch = typeCheckRe.exec(m)) !== null) {
// extract the current menu item
const item = typeMatch[1];
// Set the starting index of the second regex to that of the first to start
// matching from after the if
pushTypeRe.lastIndex = typeCheckRe.lastIndex;
// extract the first type: "..."
const type = pushTypeRe.exec(m)?.[1];
if (type && type in nameMap) {
const name = nameMap[type];
nicenNames += `Object.defineProperty(${item},"name",{value:"${name}"});`;
redefines.push(`${name}:${item}`);
}
}
if (redefines.length < 6) {
console.warn("[ApiMenuItemDeobfuscator] Expected to at least remap 6 items, only remapped", redefines.length);
}
// Merge all our redefines with the actual module
return `${nicenNames}Object.assign(${mod},{${redefines.join(",")}});${m}`;
},
},
},
],
});

View File

@ -22,12 +22,12 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.",
authors: [Devs.KingFish, Devs.Ven],
authors: [Devs.KingFish, Devs.Ven, Devs.Nuckyz],
patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
// foo && !bar ? createElement(blah,...makeElement(addReactionData))
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderPopout:.{0,300}?(\i)\(.{3,20}\{key:"add-reaction".+?\}/,
// foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"/,
replace: (m, makeElement) => {
const msg = m.match(/message:(.{1,3}),/)?.[1];
if (!msg) throw new Error("Could not find message variable");

View File

@ -0,0 +1,38 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "SettingsStoreAPI",
description: "Patches Discord's SettingsStores to expose their group and name",
authors: [Devs.Nuckyz],
patches: [
{
find: '"textAndImages","renderSpoilers"',
replacement: [
{
match: /(?<=INFREQUENT_USER_ACTION.{0,20}),useSetting:function/,
replace: ",settingsStoreApiGroup:arguments[0],settingsStoreApiName:arguments[1]$&"
}
]
}
]
});

View File

@ -0,0 +1,84 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { Settings } from "@api/settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { i18n, React, useStateFromStores } from "@webpack/common";
const cl = classNameFactory("vc-bf-");
const classes = findByPropsLazy("sidebar", "guilds");
const Animations = findByPropsLazy("a", "animated", "useTransition");
const ChannelRTCStore = findStoreLazy("ChannelRTCStore");
const ExpandedGuildFolderStore = findStoreLazy("ExpandedGuildFolderStore");
function Guilds(props: {
className: string;
bfGuildFolders: any[];
}) {
// @ts-expect-error
const res = Vencord.Plugins.plugins.BetterFolders.Guilds(props);
const scrollerProps = res.props.children?.props?.children?.[1]?.props;
if (scrollerProps?.children) {
const servers = scrollerProps.children.find(c => c?.props?.["aria-label"] === i18n.Messages.SERVERS);
if (servers) scrollerProps.children = servers;
}
return res;
}
export default ErrorBoundary.wrap(() => {
const expandedFolders = useStateFromStores([ExpandedGuildFolderStore], () => ExpandedGuildFolderStore.getExpandedFolders());
const fullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext());
const guilds = document.querySelector(`.${classes.guilds}`);
const visible = !!expandedFolders.size;
const className = cl("folder-sidebar", { fullscreen });
const Sidebar = (
<Guilds
className={classes.guilds}
bfGuildFolders={Array.from(expandedFolders)}
/>
);
if (!guilds || !Settings.plugins.BetterFolders.sidebarAnim)
return visible
? <div className={className}>{Sidebar}</div>
: null;
return (
<Animations.Transition
items={visible}
from={{ width: 0 }}
enter={{ width: guilds.getBoundingClientRect().width }}
leave={{ width: 0 }}
config={{ duration: 200 }}
>
{(style, show) => show && (
<Animations.animated.div style={style} className={className}>
{Sidebar}
</Animations.animated.div>
)}
</Animations.Transition>
);
}, { noop: true });

View File

@ -0,0 +1,17 @@
.vc-bf-folder-sidebar [class*="wrapper-"] > [class*="listItem-"]:first-of-type,
.vc-bf-folder-sidebar [class*="unreadMentionsIndicator"] {
display: none;
}
.vc-bf-folder-sidebar [class*="expandedFolderBackground-"] {
background-color: transparent;
}
.vc-bf-folder-sidebar {
display: flex;
}
.vc-bf-fullscreen {
width: 0 !important;
visibility: hidden;
}

View File

@ -0,0 +1,177 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 "./betterFolders.css";
import { definePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher } from "@webpack/common";
import FolderSideBar from "./FolderSideBar";
const GuildsTree = findLazy(m => m.prototype?.convertToFolder);
const GuildFolderStore = findStoreLazy("SortedGuildStore");
const ExpandedFolderStore = findStoreLazy("ExpandedGuildFolderStore");
const FolderUtils = findByPropsLazy("move", "toggleGuildFolderExpand");
const settings = definePluginSettings({
sidebar: {
type: OptionType.BOOLEAN,
description: "Display servers from folder on dedicated sidebar",
default: true,
},
sidebarAnim: {
type: OptionType.BOOLEAN,
description: "Animate opening the folder sidebar",
default: true,
},
closeAllFolders: {
type: OptionType.BOOLEAN,
description: "Close all folders when selecting a server not in a folder",
default: false,
},
closeAllHomeButton: {
type: OptionType.BOOLEAN,
description: "Close all folders when clicking on the home button",
default: false,
},
closeOthers: {
type: OptionType.BOOLEAN,
description: "Close other folders when opening a folder",
default: false,
},
forceOpen: {
type: OptionType.BOOLEAN,
description: "Force a folder to open when switching to a server of that folder",
default: false,
},
});
export default definePlugin({
name: "BetterFolders",
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
authors: [Devs.juby],
patches: [
{
find: '("guildsnav")',
predicate: () => settings.store.sidebar,
replacement: [
{
match: /(\i)\(\){return \i\(\(0,\i\.jsx\)\("div",{className:\i\(\)\.guildSeparator}\)\)}/,
replace: "$&$self.Separator=$1;"
},
// Folder component patch
{
match: /\i\(\(function\(\i,\i,\i\){var \i=\i\.key;return.+\(\i\)},\i\)}\)\)/,
replace: "arguments[0].bfHideServers?null:$&"
},
// BEGIN Guilds component patch
{
match: /(\i)\.themeOverride,(.{15,25}\(function\(\){var \i=)(\i\.\i\.getGuildsTree\(\))/,
replace: "$1.themeOverride,bfPatch=$1.bfGuildFolders,$2bfPatch?$self.getGuildsTree(bfPatch,$3):$3"
},
{
match: /return(\(0,\i\.jsx\))(\(\i,{)(folderNode:\i,setNodeRef:\i\.setNodeRef,draggable:!0,.+},\i\.id\));case/,
replace: "var bfHideServers=typeof bfPatch==='undefined',folder=$1$2bfHideServers,$3;return !bfHideServers&&arguments[1]?[$1($self.Separator,{}),folder]:folder;case"
},
// END
{
match: /\("guildsnav"\);return\(0,\i\.jsx\)\(.{1,6},{navigator:\i,children:\(0,\i\.jsx\)\(/,
replace: "$&$self.Guilds="
}
]
},
{
find: "APPLICATION_LIBRARY,render",
predicate: () => settings.store.sidebar,
replacement: {
match: /(\(0,\i\.jsx\))\(\i\..,{className:\i\(\)\.guilds,themeOverride:\i}\)/,
replace: "$&,$1($self.FolderSideBar,{})"
}
},
{
find: '("guildsnav")',
predicate: () => settings.store.closeAllHomeButton,
replacement: {
match: ",onClick:function(){if(!__OVERLAY__){",
replace: "$&$self.closeFolders();"
}
}
],
settings,
start() {
const getGuildFolder = (id: string) => GuildFolderStore.guildFolders.find(f => f.guildIds.includes(id));
FluxDispatcher.subscribe("CHANNEL_SELECT", this.onSwitch = data => {
if (!settings.store.closeAllFolders && !settings.store.forceOpen)
return;
if (this.lastGuildId !== data.guildId) {
this.lastGuildId = data.guildId;
const guildFolder = getGuildFolder(data.guildId);
if (guildFolder?.folderId) {
if (settings.store.forceOpen && !ExpandedFolderStore.isFolderExpanded(guildFolder.folderId))
FolderUtils.toggleGuildFolderExpand(guildFolder.folderId);
} else if (settings.store.closeAllFolders)
this.closeFolders();
}
});
FluxDispatcher.subscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder = e => {
if (settings.store.closeOthers && !this.dispatching)
FluxDispatcher.wait(() => {
const expandedFolders = ExpandedFolderStore.getExpandedFolders();
if (expandedFolders.size > 1) {
this.dispatching = true;
for (const id of expandedFolders) if (id !== e.folderId)
FolderUtils.toggleGuildFolderExpand(id);
this.dispatching = false;
}
});
});
},
stop() {
FluxDispatcher.unsubscribe("CHANNEL_SELECT", this.onSwitch);
FluxDispatcher.unsubscribe("TOGGLE_GUILD_FOLDER_EXPAND", this.onToggleFolder);
},
FolderSideBar,
getGuildsTree(folders, oldTree) {
const tree = new GuildsTree();
tree.root.children = oldTree.root.children.filter(e => folders.includes(e.id));
tree.nodes = folders.map(id => oldTree.nodes[id]);
return tree;
},
closeFolders() {
for (const id of ExpandedFolderStore.getExpandedFolders())
FolderUtils.toggleGuildFolderExpand(id);
},
});

View File

@ -44,7 +44,7 @@ export default definePlugin({
match: /"(?:username|dot)"===\i(?!\.\i)/g,
replace: "true",
},
},
}
],
options: {

View File

@ -17,6 +17,7 @@
*/
import { Devs } from "@utils/constants";
import { relaunch } from "@utils/native";
import definePlugin from "@utils/types";
import * as Webpack from "@webpack";
import { extract, filters, findAll, search } from "@webpack";
@ -71,13 +72,14 @@ export default definePlugin({
findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)),
findByCode: newFindWrapper(filters.byCode),
findAllByCode: (code: string) => findAll(filters.byCode(code)),
findStore: newFindWrapper(filters.byStoreName),
PluginsApi: Vencord.Plugins,
plugins: Vencord.Plugins.plugins,
React,
Settings: Vencord.Settings,
Api: Vencord.Api,
reload: () => location.reload(),
restart: IS_WEB ? WEB_ONLY("restart") : window.DiscordNative.app.relaunch
restart: IS_WEB ? WEB_ONLY("restart") : relaunch
};
},

View File

@ -1,105 +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 { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands";
import { findOption } from "@api/Commands/commandHelpers";
import { ApplicationCommandInputType } from "@api/Commands/types";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByCode, findByProps } from "@webpack";
const DRAFT_TYPE = 0;
export default definePlugin({
name: "CorruptMp4s",
description: "Create corrupt mp4s with extremely high or negative duration",
authors: [Devs.Ven],
dependencies: ["CommandsAPI"],
commands: [{
name: "corrupt",
description: "Create a corrupt mp4 with extremely high or negative duration",
inputType: ApplicationCommandInputType.BUILT_IN,
options: [
{
name: "mp4",
description: "the video to corrupt",
type: ApplicationCommandOptionType.ATTACHMENT,
required: true
},
{
name: "kind",
description: "the kind of corruption",
type: ApplicationCommandOptionType.STRING,
choices: [
{
name: "infinite",
value: "infinite",
label: "Very high duration"
},
{
name: "negative",
value: "negative",
label: "Negative duration"
}
]
}
],
execute: async (args, ctx) => {
const UploadStore = findByProps("getUploads");
const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0];
const video = upload?.item?.file as File | undefined;
if (video?.type !== "video/mp4")
return void sendBotMessage(ctx.channel.id, {
content: "Please upload a mp4 file"
});
const corruption = findOption<string>(args, "kind", "infinite");
const buf = new Uint8Array(await video.arrayBuffer());
let found = false;
// adapted from https://github.com/GeopJr/exorcism/blob/c9a12d77ccbcb49c987b385eafae250906efc297/src/App.svelte#L41-L48
for (let i = 0; i < buf.length; i++) {
if (buf[i] === 0x6d && buf[i + 1] === 0x76 && buf[i + 2] === 0x68 && buf[i + 3] === 0x64) {
let start = i + 18;
buf[start++] = 0x00;
buf[start++] = 0x01;
buf[start++] = corruption === "negative" ? 0xff : 0x7f;
buf[start++] = 0xff;
buf[start++] = 0xff;
buf[start++] = corruption === "negative" ? 0xf0 : 0xff;
found = true;
break;
}
}
if (!found) {
return void sendBotMessage(ctx.channel.id, {
content: "Could not find signature. Is this even a mp4?"
});
}
const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4");
const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR");
const file = new File([buf], newName, { type: "video/mp4" });
setTimeout(() => promptToUpload([file], ctx.channel, DRAFT_TYPE), 10);
}
}]
});

View File

@ -78,6 +78,7 @@ export default definePlugin({
color: "#eed202",
title: "Discord has crashed!",
body: "Awn :( Discord has crashed more than five times, not attempting to recover.",
noPersist: true,
});
} catch { }
@ -111,6 +112,7 @@ export default definePlugin({
color: "#eed202",
title: "Discord has crashed!",
body: "Attempting to recover...",
noPersist: true,
});
} catch { }
}

View File

@ -116,7 +116,8 @@ function initWs(isManual = false) {
showNotification({
title: "Dev Companion Error",
body: (e as ErrorEvent).message || "No Error Message",
color: "var(--status-danger, red)"
color: "var(--status-danger, red)",
noPersist: true,
});
});
@ -128,7 +129,8 @@ function initWs(isManual = false) {
showNotification({
title: "Dev Companion Disconnected",
body: e.reason || "No Reason provided",
color: "var(--status-danger, red)"
color: "var(--status-danger, red)",
noPersist: true,
});
});
@ -159,7 +161,11 @@ function initWs(isManual = false) {
return reply("Expected exactly one 'find' matches, found " + keys.length);
const mod = candidates[keys[0]];
let src = String(mod.original ?? mod);
let src = String(mod.original ?? mod).replaceAll("\n", "");
if (src.startsWith("function(")) {
src = "0," + src;
}
let i = 0;

View File

@ -238,7 +238,7 @@ export default definePlugin({
name: "EmoteCloner",
description: "Adds a Clone context menu item to emotes to clone them your own server",
authors: [Devs.Ven, Devs.Nuckyz],
dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
dependencies: ["ContextMenuAPI"],
start() {
addContextMenuPatch("message", messageContextMenuPatch);

View File

@ -204,7 +204,7 @@ export default definePlugin({
},
{
match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/,
replace: (_, content) => `${content}=$self.patchFakeNitroEmojis(${content});`
replace: (_, content) => `${content}=$self.patchFakeNitroEmojis(${content},arguments[2]?.formatInline);`
}
]
},
@ -333,7 +333,7 @@ export default definePlugin({
EmojiComponent: null as any,
patchFakeNitroEmojis(content: Array<any>) {
patchFakeNitroEmojis(content: Array<any>, inline: boolean) {
if (!this.EmojiComponent) return content;
const newContent: Array<any> = [];
@ -353,7 +353,7 @@ export default definePlugin({
newContent.push((
<this.EmojiComponent node={{
type: "customEmoji",
jumboable: content.length === 1,
jumboable: !inline && content.length === 1,
animated: fakeNitroMatch[2] === "gif",
name: ":FakeNitroEmoji:",
emojiId: fakeNitroMatch[1]

View File

@ -0,0 +1,145 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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/>.
*/
// This plugin is a port from Alyxia's Vendetta plugin
import { definePluginSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { Button, Forms } from "@webpack/common";
import { User } from "discord-types/general";
import virtualMerge from "virtual-merge";
interface UserProfile extends User {
themeColors?: Array<number>;
}
interface Colors {
primary: number;
accent: number;
}
function encode(primary: number, accent: number): string {
const message = `[#${primary.toString(16).padStart(6, "0")},#${accent.toString(16).padStart(6, "0")}]`;
const padding = "";
const encoded = Array.from(message)
.map(x => x.codePointAt(0))
.filter(x => x! >= 0x20 && x! <= 0x7f)
.map(x => String.fromCodePoint(x! + 0xe0000))
.join("");
return (padding || "") + " " + encoded;
}
// Courtesy of Cynthia.
function decode(bio: string): Array<number> | null {
if (bio == null) return null;
const colorString = bio.match(
/\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u,
);
if (colorString != null) {
const parsed = [...colorString[0]]
.map(x => String.fromCodePoint(x.codePointAt(0)! - 0xe0000))
.join("");
const colors = parsed
.substring(1, parsed.length - 1)
.split(",")
.map(x => parseInt(x.replace("#", "0x"), 16));
return colors;
} else {
return null;
}
}
const settings = definePluginSettings({
nitroFirst: {
description: "Default color source if both are present",
type: OptionType.SELECT,
options: [
{ label: "Nitro colors", value: true, default: true },
{ label: "Fake colors", value: false },
]
}
});
export default definePlugin({
name: "FakeProfileThemes",
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding.",
authors: [Devs.Alyxia, Devs.Remty],
patches: [
{
find: "getUserProfile=",
replacement: {
match: /(?<=getUserProfile=function\(\i\){return )(\i\[\i\])/,
replace: "$self.colorDecodeHook($1)"
}
}, {
find: ".USER_SETTINGS_PROFILE_THEME_ACCENT",
replacement: {
match: /RESET_PROFILE_THEME}\)(?<=},color:(\i).+?},color:(\i).+?)/,
replace: "$&,$self.addCopy3y3Button({primary:$1,accent:$2})"
}
}
],
settingsAboutComponent: () => (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Usage</Forms.FormTitle>
<Forms.FormText>
After enabling this plugin, you will see custom colors in the profiles of other people using compatible plugins. <br />
To set your own colors:
<ul>
<li> go to your profile settings</li>
<li> choose your own colors in the Nitro preview</li>
<li> click the "Copy 3y3" button</li>
<li> paste the invisible text anywhere in your bio</li>
</ul><br />
<b>Please note:</b> if you are using a theme which hides nitro upsells, you should disable it temporarily to set colors.
</Forms.FormText>
</Forms.FormSection>),
settings,
colorDecodeHook(user: UserProfile) {
if (user) {
// don't replace colors if already set with nitro
if (settings.store.nitroFirst && user.themeColors) return user;
const colors = decode(user.bio);
if (colors) {
return virtualMerge(user, {
premiumType: 2,
themeColors: colors
});
}
}
return user;
},
addCopy3y3Button: ErrorBoundary.wrap(function ({ primary, accent }: Colors) {
return <Button
onClick={() => {
const colorString = encode(primary, accent);
copyWithToast(colorString);
}}
color={Button.Colors.PRIMARY}
size={Button.Sizes.XLARGE}
className={Margins.left16}
>Copy 3y3
</Button >;
}, { noop: true }),
});

View File

@ -0,0 +1,85 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { getSettingStoreLazy } from "@api/SettingsStore";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByCodeLazy } from "@webpack";
import style from "./style.css?managed";
const ShowCurrentGame = getSettingStoreLazy<boolean>("status", "showCurrentGame");
const Button = findByCodeLazy("Button.Sizes.NONE,disabled:");
function makeIcon(showCurrentGame?: boolean) {
return function () {
return (
<svg
width="24"
height="24"
viewBox="0 96 960 960"
>
<path fill="currentColor" d="M182 856q-51 0-79-35.5T82 734l42-300q9-60 53.5-99T282 296h396q60 0 104.5 39t53.5 99l42 300q7 51-21 86.5T778 856q-21 0-39-7.5T706 826l-90-90H344l-90 90q-15 15-33 22.5t-39 7.5Zm498-240q17 0 28.5-11.5T720 576q0-17-11.5-28.5T680 536q-17 0-28.5 11.5T640 576q0 17 11.5 28.5T680 616Zm-80-120q17 0 28.5-11.5T640 456q0-17-11.5-28.5T600 416q-17 0-28.5 11.5T560 456q0 17 11.5 28.5T600 496ZM310 616h60v-70h70v-60h-70v-70h-60v70h-70v60h70v70Z" />
{!showCurrentGame && <line x1="920" y1="280" x2="40" y2="880" stroke="var(--status-danger)" stroke-width="80" />}
</svg>
);
};
}
function GameActivityToggleButton() {
const showCurrentGame = ShowCurrentGame?.useSetting();
return (
<Button
tooltipText="Toggle Game Activity"
icon={makeIcon(showCurrentGame)}
role="switch"
aria-checked={!showCurrentGame}
onClick={() => ShowCurrentGame?.updateSetting(old => !old)}
/>
);
}
export default definePlugin({
name: "GameActivityToggle",
description: "Adds a button next to the mic and deafen button to toggle game activity.",
authors: [Devs.Nuckyz],
dependencies: ["SettingsStoreAPI"],
patches: [
{
find: ".Messages.ACCOUNT_SPEAKING_WHILE_MUTED",
replacement: {
match: /this\.renderNameZone\(\).+?children:\[/,
replace: "$&$self.GameActivityToggleButton(),"
}
}
],
GameActivityToggleButton: ErrorBoundary.wrap(GameActivityToggleButton, { noop: true }),
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
}
});

View File

@ -0,0 +1,3 @@
[class*="withTagAsButton"] {
min-width: 88px;
}

47
src/plugins/gifPaste.ts Normal file
View File

@ -0,0 +1,47 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { filters, findLazy, mapMangledModuleLazy } from "@webpack";
const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', {
close: filters.byCode("activeView:null", "setState")
});
const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
export default definePlugin({
name: "GifPaste",
description: "Makes picking a gif in the gif picker insert a link into the chatbox instead of instantly sending it",
authors: [Devs.Ven],
patches: [{
find: ".handleSelectGIF=",
replacement: {
match: /\.handleSelectGIF=function.+?\{/,
replace: ".handleSelectGIF=function(gif){return $self.handleSelect(gif);"
}
}],
handleSelect(gif?: { url: string; }) {
if (gif) {
ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: gif.url + " " });
ExpressionPickerState.close();
}
}
});

View File

@ -147,8 +147,8 @@ export default definePlugin({
{
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
replacement: {
match: /!(\i)\|\|(null==\i\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => ""
match: /!(\i)(\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "false"
+ `${restWithoutPlatformCheck}`
+ `(${platformCheck}?${children}:[])`
+ `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))`

View File

@ -76,7 +76,7 @@ export default definePlugin({
name: "MessageLogger",
description: "Temporarily logs deleted and edited messages.",
authors: [Devs.rushii, Devs.Ven],
dependencies: ["ContextMenuAPI", "MenuItemDeobfuscatorAPI"],
dependencies: ["ContextMenuAPI"],
start() {
addDeleteStyle();
@ -209,6 +209,11 @@ export default definePlugin({
" m" +
")" +
".update($3"
},
{
// fix up key (edit last message) attempting to edit a deleted message
match: /(?<=getLastEditableMessage=.{0,200}\.find\(\(function\((\i)\)\{)return/,
replace: "return !$1.deleted &&"
}
]
},

View File

@ -21,8 +21,8 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "NoTrack",
description: "Disable Discord's tracking and crash reporting",
authors: [Devs.Cyn],
description: "Disable Discord's tracking ('science'), metrics and Sentry crash reporting",
authors: [Devs.Cyn, Devs.Ven, Devs.Nuckyz],
required: true,
patches: [
{
@ -35,8 +35,8 @@ export default definePlugin({
{
find: "window.DiscordSentry=",
replacement: {
match: /window\.DiscordSentry=function.+\}\(\)/,
replace: "",
match: /^.+$/,
replace: "()=>{}",
}
},
{

View File

@ -63,8 +63,8 @@ export default definePlugin({
{
find: ".Messages.USER_POPOUT_PRONOUNS",
replacement: {
match: /\i\.\i\.useExperiment\({}\)\.showPronouns/,
replace: "true"
match: /\.showPronouns/,
replace: ".showPronouns||true"
}
}
],

View File

@ -111,7 +111,7 @@ function jumpIfOffScreen(channelId: string, messageId: string) {
}
function getNextMessage(isUp: boolean, isReply: boolean) {
let messages: Message[] = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
let messages: Array<Message & { deleted?: boolean; }> = MessageStore.getMessages(SelectedChannelStore.getChannelId())._array;
if (!isReply) { // we are editing so only include own
const meId = UserStore.getCurrentUser().id;
messages = messages.filter(m => m.author.id === meId);
@ -121,11 +121,18 @@ function getNextMessage(isUp: boolean, isReply: boolean) {
? Math.min(messages.length - 1, i + 1)
: Math.max(-1, i - 1);
const findNextNonDeleted = (i: number) => {
do {
i = mutate(i);
} while (i !== -1 && messages[messages.length - i - 1]?.deleted === true);
return i;
};
let i: number;
if (isReply)
replyIdx = i = mutate(replyIdx);
replyIdx = i = findNextNonDeleted(replyIdx);
else
editIdx = i = mutate(editIdx);
editIdx = i = findNextNonDeleted(editIdx);
return i === - 1 ? undefined : messages[messages.length - i - 1];
}

View File

@ -62,10 +62,10 @@ export default definePlugin({
renderReadAllButton: () => <ReadAllButton />,
start() {
addServerListElement(ServerListRenderPosition.In, this.renderReadAllButton);
addServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
},
stop() {
removeServerListElement(ServerListRenderPosition.In, this.renderReadAllButton);
removeServerListElement(ServerListRenderPosition.Above, this.renderReadAllButton);
}
});

View File

@ -0,0 +1,40 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { FluxEvents } from "@webpack/types";
import { onChannelDelete, onGuildDelete, onRelationshipRemove } from "./functions";
import { syncFriends, syncGroups, syncGuilds } from "./utils";
export const FluxHandlers: Partial<Record<FluxEvents, Array<(data: any) => void>>> = {
GUILD_CREATE: [syncGuilds],
GUILD_DELETE: [onGuildDelete],
CHANNEL_CREATE: [syncGroups],
CHANNEL_DELETE: [onChannelDelete],
RELATIONSHIP_ADD: [syncFriends],
RELATIONSHIP_UPDATE: [syncFriends],
RELATIONSHIP_REMOVE: [syncFriends, onRelationshipRemove]
};
export function forEachEvent(fn: (event: FluxEvents, handler: (data: any) => void) => void) {
for (const event in FluxHandlers) {
for (const cb of FluxHandlers[event]) {
fn(event as FluxEvents, cb);
}
}
}

View File

@ -0,0 +1,87 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { UserUtils } from "@webpack/common";
import settings from "./settings";
import { ChannelDelete, ChannelType, GuildDelete, RelationshipRemove, RelationshipType } from "./types";
import { deleteGroup, deleteGuild, getGroup, getGuild, notify } from "./utils";
let manuallyRemovedFriend: string | undefined;
let manuallyRemovedGuild: string | undefined;
let manuallyRemovedGroup: string | undefined;
export const removeFriend = (id: string) => manuallyRemovedFriend = id;
export const removeGuild = (id: string) => manuallyRemovedGuild = id;
export const removeGroup = (id: string) => manuallyRemovedGroup = id;
export async function onRelationshipRemove({ relationship: { type, id } }: RelationshipRemove) {
if (manuallyRemovedFriend === id) {
manuallyRemovedFriend = undefined;
return;
}
const user = await UserUtils.fetchUser(id)
.catch(() => null);
if (!user) return;
switch (type) {
case RelationshipType.FRIEND:
if (settings.store.friends)
notify(`${user.tag} removed you as a friend.`, user.getAvatarURL(undefined, undefined, false));
break;
case RelationshipType.FRIEND_REQUEST:
if (settings.store.friendRequestCancels)
notify(`A friend request from ${user.tag} has been removed.`, user.getAvatarURL(undefined, undefined, false));
break;
}
}
export function onGuildDelete({ guild: { id, unavailable } }: GuildDelete) {
if (!settings.store.servers) return;
if (unavailable) return;
if (manuallyRemovedGuild === id) {
deleteGuild(id);
manuallyRemovedGuild = undefined;
return;
}
const guild = getGuild(id);
if (guild) {
deleteGuild(id);
notify(`You were removed from the server ${guild.name}.`, guild.iconURL);
}
}
export function onChannelDelete({ channel: { id, type } }: ChannelDelete) {
if (!settings.store.groups) return;
if (type !== ChannelType.GROUP_DM) return;
if (manuallyRemovedGroup === id) {
deleteGroup(id);
manuallyRemovedGroup = undefined;
return;
}
const group = getGroup(id);
if (group) {
deleteGroup(id);
notify(`You were removed from the group ${group.name}.`, group.iconURL);
}
}

View File

@ -0,0 +1,72 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { forEachEvent } from "./events";
import { removeFriend, removeGroup, removeGuild } from "./functions";
import settings from "./settings";
import { syncAndRunChecks } from "./utils";
export default definePlugin({
name: "RelationshipNotifier",
description: "Notifies you when a friend, group chat, or server removes you.",
authors: [Devs.nick],
settings,
patches: [
{
find: "removeRelationship:function(",
replacement: {
match: /(removeRelationship:function\((\i),\i,\i\){)/,
replace: "$1$self.removeFriend($2);"
}
},
{
find: "leaveGuild:function(",
replacement: {
match: /(leaveGuild:function\((\i)\){)/,
replace: "$1$self.removeGuild($2);"
}
},
{
find: "closePrivateChannel:function(",
replacement: {
match: /(closePrivateChannel:function\((\i)\){)/,
replace: "$1$self.removeGroup($2);"
}
}
],
async start() {
setTimeout(() => {
syncAndRunChecks();
}, 5000);
forEachEvent((ev, cb) => FluxDispatcher.subscribe(ev, cb));
},
stop() {
forEachEvent((ev, cb) => FluxDispatcher.unsubscribe(ev, cb));
},
removeFriend,
removeGroup,
removeGuild
});

View File

@ -0,0 +1,53 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { OptionType } from "@utils/types";
export default definePluginSettings({
notices: {
type: OptionType.BOOLEAN,
description: "Also show a notice at the top of your screen when removed (use this if you don't want to miss any notifications).",
default: false
},
offlineRemovals: {
type: OptionType.BOOLEAN,
description: "Notify you when starting discord if you were removed while offline.",
default: true
},
friends: {
type: OptionType.BOOLEAN,
description: "Notify when a friend removes you",
default: true
},
friendRequestCancels: {
type: OptionType.BOOLEAN,
description: "Notify when a friend request is cancelled",
default: true
},
servers: {
type: OptionType.BOOLEAN,
description: "Notify when removed from a server",
default: true
},
groups: {
type: OptionType.BOOLEAN,
description: "Notify when removed from a group chat",
default: true
}
});

View File

@ -0,0 +1,62 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { Channel } from "discord-types/general";
export interface ChannelDelete {
type: "CHANNEL_DELETE";
channel: Channel;
}
export interface GuildDelete {
type: "GUILD_DELETE";
guild: {
id: string;
unavailable?: boolean;
};
}
export interface RelationshipRemove {
type: "RELATIONSHIP_REMOVE";
relationship: {
id: string;
nickname: string;
type: number;
};
}
export interface SimpleGroupChannel {
id: string;
name: string;
iconURL?: string;
}
export interface SimpleGuild {
id: string;
name: string;
iconURL?: string;
}
export const enum ChannelType {
GROUP_DM = 3,
}
export const enum RelationshipType {
FRIEND = 1,
FRIEND_REQUEST = 3,
}

View File

@ -0,0 +1,149 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { DataStore, Notices } from "@api/index";
import { showNotification } from "@api/Notifications";
import { ChannelStore, GuildStore, RelationshipStore, UserUtils } from "@webpack/common";
import settings from "./settings";
import { ChannelType, RelationshipType, SimpleGroupChannel, SimpleGuild } from "./types";
const guilds = new Map<string, SimpleGuild>();
const groups = new Map<string, SimpleGroupChannel>();
const friends = {
friends: [] as string[],
requests: [] as string[]
};
export async function syncAndRunChecks() {
const [oldGuilds, oldGroups, oldFriends] = await DataStore.getMany([
"relationship-notifier-guilds",
"relationship-notifier-groups",
"relationship-notifier-friends"
]) as [Map<string, SimpleGuild> | undefined, Map<string, SimpleGroupChannel> | undefined, Record<"friends" | "requests", string[]> | undefined];
await Promise.all([syncGuilds(), syncGroups(), syncFriends()]);
if (settings.store.offlineRemovals) {
if (settings.store.groups && oldGroups?.size) {
for (const [id, group] of oldGroups) {
if (!groups.has(id))
notify(`You are no longer in the group ${group.name}.`, group.iconURL);
}
}
if (settings.store.servers && oldGuilds?.size) {
for (const [id, guild] of oldGuilds) {
if (!guilds.has(id))
notify(`You are no longer in the server ${guild.name}.`, guild.iconURL);
}
}
if (settings.store.friends && oldFriends?.friends.length) {
for (const id of oldFriends.friends) {
if (friends.friends.includes(id)) continue;
const user = await UserUtils.fetchUser(id).catch(() => void 0);
if (user)
notify(`You are no longer friends with ${user.tag}.`, user.getAvatarURL(undefined, undefined, false));
}
}
if (settings.store.friendRequestCancels && oldFriends?.requests?.length) {
for (const id of oldFriends.requests) {
if (friends.requests.includes(id)) continue;
const user = await UserUtils.fetchUser(id).catch(() => void 0);
if (user)
notify(`Friend request from ${user.tag} has been revoked.`, user.getAvatarURL(undefined, undefined, false));
}
}
}
}
export function notify(text: string, icon?: string) {
if (settings.store.notices)
Notices.showNotice(text, "OK", () => Notices.popNotice());
showNotification({
title: "Relationship Notifier",
body: text,
icon
});
}
export function getGuild(id: string) {
return guilds.get(id);
}
export function deleteGuild(id: string) {
guilds.delete(id);
syncGuilds();
}
export async function syncGuilds() {
for (const [id, { name, icon }] of Object.entries(GuildStore.getGuilds())) {
guilds.set(id, {
id,
name,
iconURL: icon && `https://cdn.discordapp.com/icons/${id}/${icon}.png`
});
}
await DataStore.set("relationship-notifier-guilds", guilds);
}
export function getGroup(id: string) {
return groups.get(id);
}
export function deleteGroup(id: string) {
groups.delete(id);
syncGroups();
}
export async function syncGroups() {
for (const { type, id, name, rawRecipients, icon } of ChannelStore.getSortedPrivateChannels()) {
if (type === ChannelType.GROUP_DM)
groups.set(id, {
id,
name: name || rawRecipients.map(r => r.username).join(", "),
iconURL: icon && `https://cdn.discordapp.com/channel-icons/${id}/${icon}.png`
});
}
await DataStore.set("relationship-notifier-groups", groups);
}
export async function syncFriends() {
friends.friends = [];
friends.requests = [];
const relationShips = RelationshipStore.getRelationships();
for (const id in relationShips) {
switch (relationShips[id]) {
case RelationshipType.FRIEND:
friends.friends.push(id);
break;
case RelationshipType.FRIEND_REQUEST:
friends.requests.push(id);
break;
}
}
await DataStore.set("relationship-notifier-friends", friends);
}

View File

@ -76,7 +76,7 @@ export default definePlugin({
name: "ReverseImageSearch",
description: "Adds ImageSearch to image context menus",
authors: [Devs.Ven, Devs.Nuckyz],
dependencies: ["MenuItemDeobfuscatorAPI", "ContextMenuAPI"],
dependencies: ["ContextMenuAPI"],
patches: [
{
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",

View File

@ -168,6 +168,7 @@ export default definePlugin({
get additionalInfo() {
if (IS_DEV) return " (Dev)";
if (IS_WEB) return " (Web)";
if (IS_VENCORD_DESKTOP) return " (Vencord Desktop)";
if (IS_STANDALONE) return " (Standalone)";
return "";
},

View File

@ -22,8 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { debounce } from "@utils/debounce";
import { classes, LazyComponent } from "@utils/misc";
import { filters, find } from "@webpack";
import { classes, copyWithToast } from "@utils/misc";
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
import { SpotifyStore, Track } from "./SpotifyStore";
@ -74,6 +73,37 @@ function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
);
}
function CopyContextMenu({ name, path }: { name: string; path: string; }) {
const copyId = `spotify-copy-${name}`;
const openId = `spotify-open-${name}`;
return (
<Menu.Menu
navId={`spotify-${name}-menu`}
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label={`Spotify ${name} Menu`}
>
<Menu.MenuItem
key={copyId}
id={copyId}
label={`Copy ${name} Link`}
action={() => copyWithToast("https://open.spotify.com" + path)}
/>
<Menu.MenuItem
key={openId}
id={openId}
label={`Open ${name} in Spotify`}
action={() => SpotifyStore.openExternal(path)}
/>
</Menu.Menu>
);
}
function makeContextMenu(name: string, path: string) {
return (e: React.MouseEvent<HTMLElement, MouseEvent>) =>
ContextMenu.open(e, () => <CopyContextMenu name={name} path={path} />);
}
function Controls() {
const [isPlaying, shuffle, repeat] = useStateFromStores(
[SpotifyStore],
@ -123,11 +153,6 @@ const seek = debounce((v: number) => {
SpotifyStore.seek(v);
});
const Slider = LazyComponent(() => {
const filter = filters.byCode("sliderContainer");
return find(m => m.render && filter(m.render));
});
function SeekBar() {
const { duration } = SpotifyStore.track!;
@ -159,7 +184,7 @@ function SeekBar() {
>
{msToHuman(position)}
</Forms.FormText>
<Slider
<Menu.MenuSliderControl
minValue={0}
maxValue={duration}
value={position}
@ -186,7 +211,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
const volume = useStateFromStores([SpotifyStore], () => SpotifyStore.volume);
return (
<Menu.ContextMenu
<Menu.Menu
navId="spotify-album-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label="Spotify Album Menu"
@ -209,7 +234,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
key="spotify-volume"
label="Volume"
control={(props, ref) => (
<Slider
<Menu.MenuSliderControl
{...props}
ref={ref}
value={volume}
@ -219,7 +244,7 @@ function AlbumContextMenu({ track }: { track: Track; }) {
/>
)}
/>
</Menu.ContextMenu>
</Menu.Menu>
);
}
@ -263,6 +288,7 @@ function Info({ track }: { track: Track; }) {
onClick={track.id ? () => {
SpotifyStore.openExternal(`/track/${track.id}`);
} : void 0}
onContextMenu={track.id ? makeContextMenu("Song", `/track/${track.id}`) : void 0}
>
{track.name}
</Forms.FormText>
@ -277,6 +303,7 @@ function Info({ track }: { track: Track; }) {
href={`https://open.spotify.com/artist/${a.id}`}
style={{ fontSize: "inherit" }}
title={a.name}
onContextMenu={makeContextMenu("Artist", `/artist/${a.id}`)}
>
{a.name}
</Link>
@ -295,6 +322,7 @@ function Info({ track }: { track: Track; }) {
disabled={!track.album.id}
style={{ fontSize: "inherit" }}
title={track.album.name}
onContextMenu={makeContextMenu("Album", `/album/${track.album.id}`)}
>
{track.album.name}
</Link>
@ -338,10 +366,10 @@ export function Player() {
return (
<ErrorBoundary fallback={() => (
<>
<Forms.FormText>Failed to render Spotify Modal :(</Forms.FormText>
<Forms.FormText>Check the console for errors</Forms.FormText>
</>
<div className="vc-spotify-fallback">
<p>Failed to render Spotify Modal :(</p>
<p >Check the console for errors</p>
</div>
)}>
<div id={cl("player")}>
<Info track={track} />

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/settings";
import IpcEvents from "@utils/IpcEvents";
import { proxyLazy } from "@utils/proxyLazy";
import { findByPropsLazy } from "@webpack";
@ -89,7 +90,11 @@ export const SpotifyStore = proxyLazy(() => {
public isSettingPosition = false;
public openExternal(path: string) {
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://open.spotify.com" + path);
const url = Settings.plugins.SpotifyControls.useSpotifyUris
? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":")
: "https://open.spotify.com" + path;
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, url);
}
// Need to keep track of this manually

View File

@ -39,7 +39,6 @@ export default definePlugin({
name: "SpotifyControls",
description: "Spotify Controls",
authors: [Devs.Ven, Devs.afn, Devs.KraXen72],
dependencies: ["MenuItemDeobfuscatorAPI"],
options: {
hoverControls: {
description: "Show controls on hover",
@ -47,6 +46,11 @@ export default definePlugin({
default: false,
onChange: v => toggleHoverControls(v)
},
useSpotifyUris: {
type: OptionType.BOOLEAN,
description: "Open Spotify URIs instead of Spotify URLs. Will only work if you have Spotify installed and might not work on all platforms",
default: false
}
},
patches: [
{

View File

@ -5,6 +5,14 @@
--vc-spotify-green: #1db954; /* so cusotm themes can easily change it */
}
.theme-light #vc-spotify-player {
background: var(--bg-overlay-3, var(--background-secondary-alt));
}
.theme-dark #vc-spotify-player {
background: var(--bg-overlay-1, var(--background-secondary-alt));
}
.vc-spotify-button {
background: none;
color: var(--interactive-normal);
@ -183,3 +191,8 @@
.vc-spotify-time-right {
right: 0;
}
.vc-spotify-fallback {
padding: 0.5em;
color: var(--text-normal);
}

View File

@ -33,7 +33,7 @@ const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
export default definePlugin({
name: "SupportHelper",
required: true,
description: "Helps me provide support to you",
description: "Helps us provide support to you",
authors: [Devs.Ven],
commands: [{

View File

@ -35,8 +35,6 @@ export default definePlugin({
authors: [Devs.Ven],
description: "Makes Avatars/Banners in user profiles clickable, and adds Guild Context Menu Entries to View Banner/Icon.",
dependencies: ["MenuItemDeobfuscatorAPI"],
openImage(url: string) {
const u = new URL(url);
u.searchParams.set("size", "512");

110
src/plugins/wikisearch.ts Normal file
View File

@ -0,0 +1,110 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "Wikisearch",
description: "Searches Wikipedia for your requested query. (/wikisearch)",
authors: [Devs.Samu],
dependencies: ["CommandsAPI"],
commands: [
{
name: "wikisearch",
description: "Searches Wikipedia for your request.",
inputType: ApplicationCommandInputType.BUILT_IN,
options: [
{
name: "search",
description: "Word to search for",
type: ApplicationCommandOptionType.STRING,
required: true
},
],
execute: async (_, ctx) => {
const word = findOption(_, "search", "");
if (!word) {
return sendBotMessage(ctx.channel.id, {
content: "No word was defined!"
});
}
const dataSearchParams = new URLSearchParams({
action: "query",
format: "json",
list: "search",
formatversion: "2",
origin: "*",
srsearch: word
});
const data = await fetch("https://en.wikipedia.org/w/api.php?" + dataSearchParams).then(response => response.json())
.catch(err => {
console.log(err);
sendBotMessage(ctx.channel.id, { content: "There was an error. Check the console for more info" });
return null;
});
if (!data) return;
if (!data.query?.search?.length) {
console.log(data);
return sendBotMessage(ctx.channel.id, { content: "No results given" });
}
const altData = await fetch(`https://en.wikipedia.org/w/api.php?action=query&format=json&prop=info%7Cdescription%7Cimages%7Cimageinfo%7Cpageimages&list=&meta=&indexpageids=1&pageids=${data.query.search[0].pageid}&formatversion=2&origin=*`)
.then(res => res.json())
.then(data => data.query.pages[0])
.catch(err => {
console.log(err);
sendBotMessage(ctx.channel.id, { content: "There was an error. Check the console for more info" });
return null;
});
if (!altData) return;
const thumbnailData = altData.thumbnail;
const thumbnail = thumbnailData && {
url: thumbnailData.source.replace(/(50px-)/ig, "1000px-"),
height: thumbnailData.height * 100,
width: thumbnailData.width * 100
};
sendBotMessage(ctx.channel.id, {
embeds: [
{
type: "rich",
title: data.query.search[0].title,
url: `https://wikipedia.org/w/index.php?curid=${data.query.search[0].pageid}`,
color: "0x8663BE",
description: data.query.search[0].snippet.replace(/(&nbsp;|<([^>]+)>)/ig, "").replace(/(&quot;)/ig, "\"") + "...",
image: thumbnail,
footer: {
text: "Powered by the Wikimedia API",
},
}
] as any
});
}
}
]
});

View File

@ -54,7 +54,9 @@ if (location.protocol !== "data:") {
document.getElementById("vencord-css-core")!.textContent = readFileSync(rendererCss, "utf-8");
});
}
require(process.env.DISCORD_PRELOAD!);
if (process.env.DISCORD_PRELOAD)
require(process.env.DISCORD_PRELOAD);
} else {
// Monaco Popout
contextBridge.exposeInMainWorld("setCss", debounce(s => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, s)));

View File

@ -194,6 +194,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Captain",
id: 347366054806159360n
},
nick: {
name: "nick",
id: 347884694408265729n
},
whqwert: {
name: "whqwert",
id: 586239091520176128n
@ -222,6 +226,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "TheKodeToad",
id: 706152404072267788n
},
juby: {
name: "Juby210",
id: 324622488644616195n
},
Alyxia: {
name: "Alyxia Sother",
id: 952185386350829688n
},
Remty: {
name: "Remty",
id: 335055032204656642n
},
skyevg: {
name: "skyevg",
id: 1090310844283363348n

24
src/utils/native.ts Normal file
View File

@ -0,0 +1,24 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 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/>.
*/
export function relaunch() {
if (IS_DISCORD_DESKTOP)
window.DiscordNative.app.relaunch();
else
window.VencordDesktop.app.relaunch();
}

View File

@ -47,7 +47,9 @@ export async function downloadSettingsBackup() {
const backup = await exportSettings();
const data = new TextEncoder().encode(backup);
if (IS_WEB) {
if (IS_DISCORD_DESKTOP) {
DiscordNative.fileManager.saveWithDialog(data, filename);
} else {
const file = new File([data], filename, { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(file);
@ -59,8 +61,6 @@ export async function downloadSettingsBackup() {
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
});
} else {
DiscordNative.fileManager.saveWithDialog(data, filename);
}
}
@ -77,7 +77,24 @@ const toastFailure = (err: any) => Toasts.show({
});
export async function uploadSettingsBackup(showToast = true): Promise<void> {
if (IS_WEB) {
if (IS_DISCORD_DESKTOP) {
const [file] = await DiscordNative.fileManager.openFiles({
filters: [
{ name: "Vencord Settings Backup", extensions: ["json"] },
{ name: "all", extensions: ["*"] }
]
});
if (file) {
try {
await importSettings(new TextDecoder().decode(file.data));
if (showToast) toastSuccess();
} catch (err) {
new Logger("SettingsSync").error(err);
if (showToast) toastFailure(err);
}
}
} else {
const input = document.createElement("input");
input.type = "file";
input.style.display = "none";
@ -102,22 +119,5 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> {
document.body.appendChild(input);
input.click();
setImmediate(() => document.body.removeChild(input));
} else {
const [file] = await DiscordNative.fileManager.openFiles({
filters: [
{ name: "Vencord Settings Backup", extensions: ["json"] },
{ name: "all", extensions: ["*"] }
]
});
if (file) {
try {
await importSettings(new TextDecoder().decode(file.data));
if (showToast) toastSuccess();
} catch (err) {
new Logger("SettingsSync").error(err);
if (showToast) toastFailure(err);
}
}
}
}

View File

@ -20,6 +20,7 @@ import gitHash from "~git-hash";
import IpcEvents from "./IpcEvents";
import Logger from "./Logger";
import { relaunch } from "./native";
import { IpcRes } from "./types";
export const UpdateLogger = /* #__PURE__*/ new Logger("Updater", "white");
@ -90,8 +91,10 @@ export async function maybePromptToUpdate(confirmMessage: string, checkForDev =
if (wantsUpdate) {
await update();
const needFullRestart = await rebuild();
if (needFullRestart) DiscordNative.app.relaunch();
else location.reload();
if (needFullRestart)
relaunch();
else
location.reload();
}
}
} catch (err) {

View File

@ -17,40 +17,37 @@
*/
// eslint-disable-next-line path-alias/no-relative
import { filters, findByPropsLazy } from "../webpack";
import { filters, findByPropsLazy, waitFor } from "@webpack";
import { waitForComponent } from "./internal";
import * as t from "./types/components";
export const Forms = {
FormTitle: waitForComponent<t.FormTitle>("FormTitle", filters.byCode("errorSeparator")),
FormSection: waitForComponent<t.FormSection>("FormSection", filters.byCode("titleClassName", "sectionTitle")),
FormDivider: waitForComponent<t.FormDivider>("FormDivider", m => {
if (typeof m !== "function") return false;
const s = m.toString();
return s.length < 200 && s.includes(".divider");
}),
FormText: waitForComponent<t.FormText>("FormText", m => m.Types?.INPUT_PLACEHOLDER),
export let Forms = {} as {
FormTitle: t.FormTitle,
FormSection: t.FormSection,
FormDivider: t.FormDivider,
FormText: t.FormText,
};
export const Card = waitForComponent<t.Card>("Card", m => m.Types?.PRIMARY && m.defaultProps);
export const Button = waitForComponent<t.Button>("Button", ["Hovers", "Looks", "Sizes"]);
export const Switch = waitForComponent<t.Switch>("Switch", filters.byCode("tooltipNote", "ringTarget"));
export const Tooltip = waitForComponent<t.Tooltip>("Tooltip", filters.byCode("shouldShowTooltip:!1", "clickableOnMobile||"));
export let Card: t.Card;
export let Button: t.Button;
export let Switch: t.Switch;
export let Tooltip: t.Tooltip;
export let TextInput: t.TextInput;
export let TextArea: t.TextArea;
export let Text: t.Text;
export let Select: t.Select;
export let SearchableSelect: t.SearchableSelect;
export let Slider: t.Slider;
export let ButtonLooks: t.ButtonLooks;
export let TabBar: any;
export const Timestamp = waitForComponent<t.Timestamp>("Timestamp", filters.byCode(".Messages.MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL.format"));
export const TextInput = waitForComponent<t.TextInput>("TextInput", ["defaultProps", "Sizes", "contextType"]);
export const TextArea = waitForComponent<t.TextArea>("TextArea", filters.byCode("handleSetRef", "textArea"));
export const Text = waitForComponent<t.Text>("Text", m => {
if (typeof m !== "function") return false;
const s = m.toString();
return (s.length < 1500 && s.includes("data-text-variant") && s.includes("always-white"));
});
export const Select = waitForComponent<t.Select>("Select", filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
const searchableSelectFilter = filters.byCode("autoFocus", ".Messages.SELECT");
export const SearchableSelect = waitForComponent<t.SearchableSelect>("SearchableSelect", m =>
m.render && searchableSelectFilter(m.render)
);
export const Slider = waitForComponent<t.Slider>("Slider", filters.byCode("closestMarkerIndex", "stickToMarkers"));
export const Flex = waitForComponent<t.Flex>("Flex", ["Justify", "Align", "Wrap"]);
export const ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent") as Record<string, string>;
export const ButtonLooks: t.ButtonLooks = findByPropsLazy("BLANK", "FILLED", "INVERTED");
waitFor("FormItem", m => {
({ Card, Button, FormSwitch: Switch, Tooltip, TextInput, TextArea, Text, Select, SearchableSelect, Slider, ButtonLooks, TabBar } = m);
Forms = m;
});

View File

@ -16,32 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { proxyLazy } from "@utils/proxyLazy";
// eslint-disable-next-line path-alias/no-relative
import { filters, mapMangledModule, mapMangledModuleLazy } from "../webpack";
import { filters, mapMangledModuleLazy, waitFor } from "../webpack";
import type * as t from "./types/menu";
export const Menu: t.Menu = proxyLazy(() => {
const hasDeobfuscator = Vencord.Settings.plugins.MenuItemDeobfuscatorAPI.enabled;
const menuItems = ["MenuSeparator", "MenuGroup", "MenuItem", "MenuCheckboxItem", "MenuRadioItem", "MenuControlItem"];
export let Menu = {} as t.Menu;
const map = mapMangledModule("♫ ⊂(。◕‿‿◕。⊂) ♪", {
ContextMenu: filters.byCode("getContainerProps"),
...Object.fromEntries((hasDeobfuscator ? menuItems : []).map(s => [s, (m: any) => m.name === s]))
}) as t.Menu;
if (!hasDeobfuscator) {
for (const m of menuItems)
Object.defineProperty(map, m, {
get() {
throw new Error("MenuItemDeobfuscator must be enabled to use this.");
}
});
}
return map;
});
waitFor("MenuItem", m => Menu = m);
export const ContextMenu: t.ContextMenuApi = mapMangledModuleLazy('type:"CONTEXT_MENU_OPEN"', {
open: filters.byCode("stopPropagation"),

View File

@ -24,10 +24,12 @@ export let useState: typeof React.useState;
export let useEffect: typeof React.useEffect;
export let useMemo: typeof React.useMemo;
export let useRef: typeof React.useRef;
export let useReducer: typeof React.useReducer;
export let useCallback: typeof React.useCallback;
export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/client") = findByPropsLazy("createPortal", "render");
waitFor("useState", m => {
React = m;
({ useEffect, useState, useMemo, useRef } = React);
({ useEffect, useState, useMemo, useRef, useReducer, useCallback } = React);
});

View File

@ -21,7 +21,7 @@ import type { ComponentType, CSSProperties, PropsWithChildren, UIEvent } from "r
type RC<C> = ComponentType<PropsWithChildren<C & Record<string, any>>>;
export interface Menu {
ContextMenu: RC<{
Menu: RC<{
navId: string;
onClose(): void;
className?: string;
@ -49,19 +49,21 @@ export interface Menu {
id: string;
interactive?: boolean;
}>;
// TODO: Type me
MenuSliderControl: RC<any>;
}
export interface ContextMenuApi {
close(): void;
open(
event: UIEvent,
render?: Menu["ContextMenu"],
render?: Menu["Menu"],
options?: { enableSpellCheck?: boolean; },
renderLazy?: () => Promise<Menu["ContextMenu"]>
renderLazy?: () => Promise<Menu["Menu"]>
): void;
openLazy(
event: UIEvent,
renderLazy?: () => Promise<Menu["ContextMenu"]>,
renderLazy?: () => Promise<Menu["Menu"]>,
options?: { enableSpellCheck?: boolean; }
): void;
}

View File

@ -30,6 +30,7 @@ export interface FluxDispatcher {
isDispatching(): boolean;
subscribe(event: FluxEvents, callback: (data: any) => void): void;
unsubscribe(event: FluxEvents, callback: (data: any) => void): void;
wait(callback: () => void): void;
}
export type Parser = Record<

View File

@ -67,7 +67,7 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) {
instance.pop();
}
if (IS_DEV && !IS_WEB) {
if (IS_DEV && IS_DISCORD_DESKTOP) {
var devToolsOpen = false;
// At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed
setTimeout(() => {
@ -109,6 +109,8 @@ export const find = traceFunction("find", function find(filter: FilterFn, getDef
if (!isWaitFor) {
const err = new Error("Didn't find module matching this filter");
if (IS_DEV) {
logger.error(err);
logger.error(filter);
if (!devToolsOpen)
// Strict behaviour in DevBuilds to fail early and make sure the issue is found
throw err;