Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
462f191051 | ||
|
6960a439c9 | ||
|
4dff1c5bd5 | ||
|
2c8ebdce7d | ||
|
dae7cb67ef | ||
|
081b01b667 | ||
|
5340ea7ba0 | ||
|
84a649a671 | ||
|
efd9927696 | ||
|
c86a34a15d | ||
|
ff16513f21 | ||
|
906c265aea | ||
|
708c16176b | ||
|
035d1e24b2 | ||
|
48e9b1be7a | ||
|
6acdaf207d | ||
|
9d41b360c9 | ||
|
12cbd73e7f | ||
|
420b068094 |
@ -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).
|
||||
|
@ -1,9 +1,8 @@
|
||||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"description": "The cutest Discord client mod",
|
||||
"keywords": [ ],
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Vendicated/Vencord/issues"
|
||||
@ -34,11 +33,13 @@
|
||||
"dependencies": {
|
||||
"@vap/core": "0.0.12",
|
||||
"@vap/shiki": "0.10.3",
|
||||
"fflate": "^0.7.4"
|
||||
"fflate": "^0.7.4",
|
||||
"nanoid": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^5.0.2",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/nanoid": "^3.0.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
|
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@ -11,6 +11,7 @@ patchedDependencies:
|
||||
specifiers:
|
||||
'@types/diff': ^5.0.2
|
||||
'@types/lodash': ^4.14.191
|
||||
'@types/nanoid': ^3.0.0
|
||||
'@types/node': ^18.11.18
|
||||
'@types/react': ^18.0.27
|
||||
'@types/react-dom': ^18.0.10
|
||||
@ -31,6 +32,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
|
||||
@ -43,10 +45,12 @@ dependencies:
|
||||
'@vap/core': 0.0.12
|
||||
'@vap/shiki': 0.10.3
|
||||
fflate: 0.7.4
|
||||
nanoid: 4.0.2
|
||||
|
||||
devDependencies:
|
||||
'@types/diff': 5.0.2
|
||||
'@types/lodash': 4.14.191
|
||||
'@types/nanoid': 3.0.0
|
||||
'@types/node': 18.11.18
|
||||
'@types/react': 18.0.27
|
||||
'@types/react-dom': 18.0.10
|
||||
@ -417,6 +421,13 @@ packages:
|
||||
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
|
||||
dev: true
|
||||
|
||||
/@types/nanoid/3.0.0:
|
||||
resolution: {integrity: sha512-UXitWSmXCwhDmAKe7D3hNQtQaHeHt5L8LO1CB8GF8jlYVzOv5cBWDNqiJ+oPEWrWei3i3dkZtHY/bUtd0R/uOQ==}
|
||||
deprecated: This is a stub types definition. nanoid provides its own type definitions, so you do not need this installed.
|
||||
dependencies:
|
||||
nanoid: 4.0.2
|
||||
dev: true
|
||||
|
||||
/@types/node/18.11.18:
|
||||
resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==}
|
||||
dev: true
|
||||
@ -2245,6 +2256,11 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/nanoid/4.0.2:
|
||||
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
||||
engines: {node: ^14 || ^16 || >=18}
|
||||
hasBin: true
|
||||
|
||||
/nanomatch/1.2.13:
|
||||
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -54,6 +54,7 @@ async function init() {
|
||||
title: "Vencord has been updated!",
|
||||
body: "Click here to restart",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick() {
|
||||
if (needsFullRestart)
|
||||
window.DiscordNative.app.relaunch();
|
||||
@ -69,6 +70,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");
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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, {
|
||||
|
203
src/api/Notifications/notificationLog.tsx
Normal file
203
src/api/Notifications/notificationLog.tsx
Normal 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)}
|
||||
/>
|
||||
));
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
@ -72,7 +73,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"
|
||||
@ -165,7 +166,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 +180,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 +199,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>
|
||||
);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
import { onceDefined } from "@utils/onceDefined";
|
||||
import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron";
|
||||
import electron, { app, BrowserWindowConstructorOptions, Menu, protocol, session } from "electron";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
import { initIpc } from "./ipcMain";
|
||||
@ -83,7 +83,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";
|
||||
}
|
||||
@ -116,10 +117,10 @@ if (!process.argv.includes("--vanilla")) {
|
||||
|
||||
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
|
||||
|
||||
electron.app.whenReady().then(() => {
|
||||
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) => {
|
||||
protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
|
||||
let url = unsafeUrl.slice("vencord://".length);
|
||||
if (url.endsWith("/")) url = url.slice(0, -1);
|
||||
switch (url) {
|
||||
@ -175,7 +176,7 @@ if (!process.argv.includes("--vanilla")) {
|
||||
}
|
||||
}
|
||||
|
||||
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
||||
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
|
||||
if (responseHeaders) {
|
||||
if (resourceType === "mainFrame")
|
||||
patchCsp(responseHeaders, "content-security-policy");
|
||||
@ -187,6 +188,11 @@ if (!process.argv.includes("--vanilla")) {
|
||||
}
|
||||
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 = () => { };
|
||||
});
|
||||
} else {
|
||||
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
|
||||
|
@ -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");
|
||||
|
@ -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 { }
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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]
|
||||
|
47
src/plugins/gifPaste.ts
Normal file
47
src/plugins/gifPaste.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
@ -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}))`
|
||||
|
@ -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: "()=>{}",
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
40
src/plugins/relationshipNotifier/events.ts
Normal file
40
src/plugins/relationshipNotifier/events.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
87
src/plugins/relationshipNotifier/functions.ts
Normal file
87
src/plugins/relationshipNotifier/functions.ts
Normal 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);
|
||||
}
|
||||
}
|
72
src/plugins/relationshipNotifier/index.ts
Normal file
72
src/plugins/relationshipNotifier/index.ts
Normal 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
|
||||
});
|
53
src/plugins/relationshipNotifier/settings.ts
Normal file
53
src/plugins/relationshipNotifier/settings.ts
Normal 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
|
||||
}
|
||||
});
|
62
src/plugins/relationshipNotifier/types.ts
Normal file
62
src/plugins/relationshipNotifier/types.ts
Normal 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,
|
||||
}
|
149
src/plugins/relationshipNotifier/utils.ts
Normal file
149
src/plugins/relationshipNotifier/utils.ts
Normal 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);
|
||||
}
|
@ -22,7 +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 { classes, copyWithToast, LazyComponent } from "@utils/misc";
|
||||
import { filters, find } from "@webpack";
|
||||
import { ContextMenu, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
@ -74,6 +74,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.ContextMenu
|
||||
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.ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
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],
|
||||
@ -263,6 +294,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 +309,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 +328,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>
|
||||
|
@ -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
|
||||
|
@ -47,6 +47,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: [
|
||||
{
|
||||
|
@ -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);
|
||||
|
110
src/plugins/wikisearch.ts
Normal file
110
src/plugins/wikisearch.ts
Normal 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(/( |<([^>]+)>)/ig, "").replace(/(")/ig, "\"") + "...",
|
||||
image: thumbnail,
|
||||
footer: {
|
||||
text: "Powered by the Wikimedia API",
|
||||
},
|
||||
}
|
||||
] as any
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
@ -194,6 +194,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
name: "Captain",
|
||||
id: 347366054806159360n
|
||||
},
|
||||
nick: {
|
||||
name: "nick",
|
||||
id: 347884694408265729n
|
||||
},
|
||||
whqwert: {
|
||||
name: "whqwert",
|
||||
id: 586239091520176128n
|
||||
|
@ -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);
|
||||
});
|
||||
|
Reference in New Issue
Block a user