Compare commits

...

55 Commits

Author SHA1 Message Date
V
6b10947d06 Bump to v1.5.1 2023-09-21 18:58:43 +02:00
V
97b6699afe Fuck you Mozilla 2023-09-21 18:56:58 +02:00
V
7e91edc757 browser: unhardcode rnnoise 2023-09-21 18:19:59 +02:00
V
a82544e93e Delete FixInbox: Discord fixed this issue themselves now 2023-09-21 17:57:14 +02:00
AutumnVN
d8c8b74ed7 BetterRoleDot: Setting for copying role color in profile popout (#1698) 2023-09-21 17:40:37 +02:00
Daniel Foster
fadd1598f5 Settings: use nearest-neighbour for shiggy (#1739)
Co-authored-by: V <vendicated@riseup.net>
2023-09-21 17:18:41 +02:00
Manti
e5c0898dd6 [ReviewDB] add emojis, discord markdown & notifications (#1718)
Co-authored-by: V <vendicated@riseup.net>
2023-09-21 17:16:15 +02:00
Nuckyz
9550b74b2a Fix broken FakeNitro patch 2023-09-20 02:44:31 -03:00
V
6cfb67a52e fix ci 2023-09-19 04:15:34 +02:00
V
12fe367385 bump to v1.5.0 2023-09-19 04:12:54 +02:00
AutumnVN
fc0417eb80 moreUserTags: fix user tag in member list (#1730) 2023-09-19 04:11:30 +02:00
AutumnVN
1f87d14ab2 textReplace: pad space only in string rules (#1738) 2023-09-19 04:11:30 +02:00
XAlboX
7d8f3508a8 ClearURLs: Add si parameter on youtube.com (#1733)
Co-authored-by: V <vendicated@riseup.net>
2023-09-19 04:11:30 +02:00
V
41f5d71e38 Bundle dependencies with extensions for webstore rule compliance (#1740) 2023-09-19 04:11:27 +02:00
AutumnVN
efb88a4df8 favGifSearch: fix search bar (#1732) 2023-09-19 04:10:15 +02:00
Syncx
a73d09a2f0 PreviewMessage: Add attachments (& misc changes) (#1715) 2023-09-12 23:14:17 +02:00
AutumnVN
cf7c4d63b6 pictureInPicture: don't show PiP button on normal file (#1725) 2023-09-12 23:11:53 +02:00
Archer
a95311ef2c lastfm: Add setting for artist name and song title only (#1726)
Co-authored-by: V <vendicated@riseup.net>
2023-09-12 23:11:25 +02:00
lovenginx
dd23f9802c InvisibleChat: fixup decryption modal (#1720) 2023-09-12 23:04:50 +02:00
Nuckyz
f23ddf4cae oopsies 2023-09-12 05:10:44 -03:00
Nuckyz
4222c7fd9f Fix broken FakeNitro patch on canary 2023-09-12 05:01:18 -03:00
Nuckyz
09f65b401e Fix broken MessageLogger patches 2023-09-12 04:28:55 -03:00
V
7364776715 Dearrow: Fix button part 2 2023-09-09 19:51:30 +02:00
V
3a5b70d410 Dearrow: Fix button 2023-09-09 19:49:11 +02:00
V
e08d49edac New Plugin: Dearrow (#1723) 2023-09-09 19:17:50 +02:00
Mushrrom
17abbd3e3e LastFM: Add setting for using name + artist as activity name (#1713)
Co-authored-by: V <vendicated@riseup.net>
2023-09-09 04:22:41 +02:00
V
25a1d934c6 bump to v1.4.7 2023-09-09 00:22:24 +02:00
Luna
620319a4bc blurnsfw: fix crash (#31)
Reviewed-on: https://codeberg.org/Ven/cord/pulls/31
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-08 21:55:27 +00:00
Hugo C
4c9996d620 feat(plugin): PictureInPicture (#1697)
Co-authored-by: V <vendicated@riseup.net>
2023-09-08 03:57:44 +02:00
V
885c75fdaa add custom plugin author popouts (#1712) 2023-09-08 03:42:20 +02:00
Lewis Crichton
f2a22c5e57 feat: crash info in /vencord-debug (#1714)
Co-authored-by: V <vendicated@riseup.net>
2023-09-08 03:41:30 +02:00
V
5e3a485edc ci: generate plugin readme map 2023-09-08 02:26:47 +02:00
Luna
452bf72e56 rnnoise: fix error on webcam (#29)
errored because there was no audio stream, luckily the patched function had a boolean indicating if it was audio or not so just ignore it if it isnt

Reviewed-on: https://codeberg.org/Ven/cord/pulls/29
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-06 17:10:33 +00:00
Luna
afa47addd7 usrbg: export the data to allow other plugins to access (#30)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: Ven <ven@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/Ven/cord/pulls/30
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-06 17:10:04 +00:00
V
8ecee3d09f fix ViewRaw & VoiceMessages context menu style 2023-09-06 19:08:48 +02:00
luk1337
d2aac5edc5 ClearURLs: Add share_id parameter on reddit.com (#1706) 2023-09-06 18:53:28 +02:00
lovenginx
fc45510053 LoadingQuotes: add a new quote (#1710) 2023-09-06 18:52:14 +02:00
AutumnVN
77bfaf38a5 HideAttachments: fix broken css (#1707) 2023-09-06 18:51:48 +02:00
AutumnVN
a4cadc03d8 viewRaw: dont add context menu to guild folders (#1708) 2023-09-06 18:51:18 +02:00
AutumnVN
315fcf1972 messageLogger: make ignoreChannels also ignore threads (#1709) 2023-09-06 18:50:20 +02:00
Nuckyz
f1b3b3c0a3 Uncanonicalize reported patches 2023-09-06 18:47:54 +02:00
V
4c805d08be [skip ci] PatchHelper: Fix copy button style 2023-09-05 21:37:39 +02:00
V
9b987d1e56 Bump to v1.4.6 2023-09-05 21:25:16 +02:00
V
774318d583 ViewRaw: Update description 2023-09-05 21:24:43 +02:00
V
7d954f9ade ViewRaw: Fix ugly copy icon & context menu position 2023-09-05 21:20:13 +02:00
Luna
860d6edc7b HideAttachments: allow hiding stickers (#13)
Self explanatory

Reviewed-on: https://codeberg.org/Ven/cord/pulls/13
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-05 18:56:28 +00:00
Rini
223b0366c6 pinDMs: fix alt+shift+up (#25)
applies the same fix as alt+up/down to the jump to unread one

confirmed non-explosion of shc

Reviewed-on: https://codeberg.org/Ven/cord/pulls/25
Co-authored-by: Rini <rini@rinici.de>
Co-committed-by: Rini <rini@rinici.de>
2023-09-05 18:51:22 +00:00
Luna
69cb7593eb View Raw on more objects (#27)
Shows view raw on guilds, channels, and users

Reviewed-on: https://codeberg.org/Ven/cord/pulls/27
Co-authored-by: Luna <imlvnaa@gmail.com>
Co-committed-by: Luna <imlvnaa@gmail.com>
2023-09-05 18:49:57 +00:00
V
8614e17633 Fix some plugins wrongly displaying the settings cog 2023-09-05 20:10:42 +02:00
Ryan Cao
aecd9d8fda feat(memberCount): format count according to user locale (#1679) 2023-09-05 19:46:33 +02:00
Hugo C
faeb4fb585 previewMessage: Hide the button once a message is sent (#1692)
Co-authored-by: V <vendicated@riseup.net>
2023-09-05 19:45:44 +02:00
dolfies
0d18b44ba7 feat(plugin): ShowTimeouts (#1687)
Co-authored-by: TheKodeToad <TheKodeToad@proton.me>
Co-authored-by: V <vendicated@riseup.net>
2023-09-05 19:42:35 +02:00
omahs
d671bd65ac fix typos (#1703) 2023-09-05 19:40:32 +02:00
AutumnVN
8e952c630b MessageLogger: fix ignore by id doesn't ignore edited (#1705) 2023-09-05 19:39:22 +02:00
V
2c758ccdf8 new plugin: ServerProfile (#1704)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2023-09-05 18:36:14 +02:00
110 changed files with 3182 additions and 430 deletions

View File

@ -38,11 +38,11 @@ jobs:
run: pnpm build --standalone
- name: Generate plugin list
run: pnpm generatePluginJson dist/plugins.json
run: pnpm generatePluginJson dist/plugins.json dist/plugin-readmes.json
- name: Clean up obsolete files
run: |
rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
rm -rf dist/*-unpacked dist/monaco Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map
- name: Get some values needed for the release
id: release_values

View File

@ -65,7 +65,7 @@ Also pay attention to the following:
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
`var .{1,2}=([^;]+);`
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
- Additionally, as you might have noticed, all of the appove approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
- Additionally, as you might have noticed, all of the above approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
#### "replace"

View File

@ -19,9 +19,11 @@
/// <reference path="../src/modules.d.ts" />
/// <reference path="../src/globals.d.ts" />
import monacoHtml from "~fileContent/../src/components/monacoWin.html";
import monacoHtmlLocal from "~fileContent/monacoWin.html";
import monacoHtmlCdn from "~fileContent/../src/main/monacoWin.html";
import * as DataStore from "../src/api/DataStore";
import { debounce } from "../src/utils";
import { EXTENSION_BASE_URL } from "../src/utils/web-metadata";
import { getTheme, Theme } from "../src/utils/discord";
import { getThemeInfo } from "../src/main/themes";
@ -80,6 +82,7 @@ window.VencordNative = {
return;
}
win.baseUrl = EXTENSION_BASE_URL;
win.setCss = setCssDebounced;
win.getCurrentCss = () => VencordNative.quickCss.get();
win.getTheme = () =>
@ -87,7 +90,7 @@ window.VencordNative = {
? "vs-light"
: "vs-dark";
win.document.write(monacoHtml);
win.document.write(IS_EXTENSION ? monacoHtmlLocal : monacoHtmlCdn);
},
},

View File

@ -1,32 +0,0 @@
/**
* @template T
* @param {T[]} arr
* @param {(v: T) => boolean} predicate
*/
function removeFirst(arr, predicate) {
const idx = arr.findIndex(predicate);
if (idx !== -1) arr.splice(idx, 1);
}
chrome.webRequest.onHeadersReceived.addListener(
({ responseHeaders, type, url }) => {
if (!responseHeaders) return;
if (type === "main_frame") {
// In main frame requests, the CSP needs to be removed to enable fetching of custom css
// as desired by the user
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy");
} else if (type === "stylesheet" && url.startsWith("https://raw.githubusercontent.com/")) {
// Most users will load css from GitHub, but GitHub doesn't set the correct content type,
// so we fix it here
removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-type");
responseHeaders.push({
name: "Content-Type",
value: "text/css"
});
}
return { responseHeaders };
},
{ urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"], types: ["main_frame", "stylesheet"] },
["blocking", "responseHeaders"]
);

View File

@ -4,6 +4,11 @@ if (typeof browser === "undefined") {
const script = document.createElement("script");
script.src = browser.runtime.getURL("dist/Vencord.js");
script.id = "vencord-script";
Object.assign(script.dataset, {
extensionBaseUrl: browser.runtime.getURL(""),
version: browser.runtime.getManifest().version
});
const style = document.createElement("link");
style.type = "text/css";

View File

@ -28,7 +28,7 @@
"web_accessible_resources": [
{
"resources": ["dist/Vencord.js", "dist/Vencord.css"],
"resources": ["dist/*", "third-party/*"],
"matches": ["*://*.discord.com/*"]
}
],

View File

@ -26,11 +26,7 @@
}
],
"background": {
"scripts": ["background.js"]
},
"web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
"web_accessible_resources": ["dist/*", "third-party/*"],
"browser_specific_settings": {
"gecko": {

43
browser/monaco.ts Normal file
View File

@ -0,0 +1,43 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./patch-worker";
import * as monaco from "monaco-editor/esm/vs/editor/editor.main.js";
declare global {
const baseUrl: string;
const getCurrentCss: () => Promise<string>;
const setCss: (css: string) => void;
const getTheme: () => string;
}
const BASE = "/dist/monaco/vs";
self.MonacoEnvironment = {
getWorkerUrl(_moduleId: unknown, label: string) {
const path = label === "css" ? "/language/css/css.worker.js" : "/editor/editor.worker.js";
return new URL(BASE + path, baseUrl).toString();
}
};
getCurrentCss().then(css => {
const editor = monaco.editor.create(
document.getElementById("container")!,
{
value: css,
language: "css",
theme: getTheme(),
}
);
editor.onDidChangeModelContent(() =>
setCss(editor.getValue())
);
window.addEventListener("resize", () => {
// make monaco re-layout
editor.layout();
});
});

37
browser/monacoWin.html Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Vencord QuickCSS Editor</title>
<style>
html,
body,
#container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
const script = document.createElement("script");
script.src = new URL("/dist/monaco/index.js", baseUrl);
const style = document.createElement("link");
style.type = "text/css";
style.rel = "stylesheet";
style.href = new URL("/dist/monaco/index.css", baseUrl);
document.body.append(style, script);
</script>
</body>
</html>

135
browser/patch-worker.js Normal file
View File

@ -0,0 +1,135 @@
/*
Copyright 2013 Rob Wu <gwnRob@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Target: Chrome 20+
// W3-compliant Worker proxy.
// This module replaces the global Worker object.
// When invoked, the default Worker object is called.
// If this call fails with SECURITY_ERR, the script is fetched
// using async XHR, and transparently proxies all calls and
// setters/getters to the new Worker object.
// Note: This script does not magically circumvent the Same origin policy.
(function () {
'use strict';
var Worker_ = window.Worker;
var URL = window.URL || window.webkitURL;
// Create dummy worker for the following purposes:
// 1. Don't override the global Worker object if the fallback isn't
// going to work (future API changes?)
// 2. Use it to trigger early validation of postMessage calls
// Note: Blob constructor is supported since Chrome 20, but since
// some of the used Chrome APIs are only supported as of Chrome 20,
// I don't bother adding a BlobBuilder fallback.
var dummyWorker = new Worker_(
URL.createObjectURL(new Blob([], { type: 'text/javascript' })));
window.Worker = function Worker(scriptURL) {
if (arguments.length === 0) {
throw new TypeError('Not enough arguments');
}
try {
return new Worker_(scriptURL);
} catch (e) {
if (e.code === 18/*DOMException.SECURITY_ERR*/) {
return new WorkerXHR(scriptURL);
} else {
throw e;
}
}
};
// Bind events and replay queued messages
function bindWorker(worker, workerURL) {
if (worker._terminated) {
return;
}
worker.Worker = new Worker_(workerURL);
worker.Worker.onerror = worker._onerror;
worker.Worker.onmessage = worker._onmessage;
var o;
while ((o = worker._replayQueue.shift())) {
worker.Worker[o.method].apply(worker.Worker, o.arguments);
}
while ((o = worker._messageQueue.shift())) {
worker.Worker.postMessage.apply(worker.Worker, o);
}
}
function WorkerXHR(scriptURL) {
var worker = this;
var x = new XMLHttpRequest();
x.responseType = 'blob';
x.onload = function () {
// http://stackoverflow.com/a/10372280/938089
var workerURL = URL.createObjectURL(x.response);
bindWorker(worker, workerURL);
};
x.open('GET', scriptURL);
x.send();
worker._replayQueue = [];
worker._messageQueue = [];
}
WorkerXHR.prototype = {
constructor: Worker_,
terminate: function () {
if (!this._terminated) {
this._terminated = true;
if (this.Worker)
this.Worker.terminate();
}
},
postMessage: function (message, transfer) {
if (!(this instanceof WorkerXHR))
throw new TypeError('Illegal invocation');
if (this.Worker) {
this.Worker.postMessage.apply(this.Worker, arguments);
} else {
// Trigger validation:
dummyWorker.postMessage(message);
// Alright, push the valid message to the queue.
this._messageQueue.push(arguments);
}
}
};
// Implement the EventTarget interface
[
'addEventListener',
'removeEventListener',
'dispatchEvent'
].forEach(function (method) {
WorkerXHR.prototype[method] = function () {
if (!(this instanceof WorkerXHR)) {
throw new TypeError('Illegal invocation');
}
if (this.Worker) {
this.Worker[method].apply(this.Worker, arguments);
} else {
this._replayQueue.push({ method: method, arguments: arguments });
}
};
});
Object.defineProperties(WorkerXHR.prototype, {
onmessage: {
get: function () { return this._onmessage || null; },
set: function (func) {
this._onmessage = typeof func === 'function' ? func : null;
}
},
onerror: {
get: function () { return this._onerror || null; },
set: function (func) {
this._onerror = typeof func === 'function' ? func : null;
}
}
});
})();

View File

@ -63,7 +63,7 @@ Then fully close Discord from your taskbar or task manager, and restart it. Venc
If you're using Discord already, go into the `Updater` tab in settings.
Sometimes it may be neccessary to manually update if the GUI updater fails.
Sometimes it may be necessary to manually update if the GUI updater fails.
To pull latest changes:

View File

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.4.5",
"version": "1.5.1",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -36,10 +36,13 @@
"@vap/shiki": "0.10.5",
"eslint-plugin-simple-header": "^1.0.2",
"fflate": "^0.7.4",
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
"monaco-editor": "^0.43.0",
"nanoid": "^4.0.2",
"virtual-merge": "^1.0.1"
},
"devDependencies": {
"@types/chrome": "^0.0.246",
"@types/diff": "^5.0.3",
"@types/lodash": "^4.14.194",
"@types/node": "^18.16.3",
@ -64,7 +67,8 @@
"stylelint-config-standard": "^33.0.0",
"tsx": "^3.12.7",
"type-fest": "^3.9.0",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"zip-local": "^0.3.5"
},
"packageManager": "pnpm@8.1.1",
"pnpm": {

81
pnpm-lock.yaml generated
View File

@ -1,5 +1,9 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
eslint-plugin-path-alias@1.0.0:
hash: m6sma4g6bh67km3q6igf6uxaja
@ -24,6 +28,12 @@ dependencies:
fflate:
specifier: ^0.7.4
version: 0.7.4
gifenc:
specifier: github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3
version: github.com/mattdesl/gifenc/64842fca317b112a8590f8fef2bf3825da8f6fe3
monaco-editor:
specifier: ^0.43.0
version: 0.43.0
nanoid:
specifier: ^4.0.2
version: 4.0.2
@ -32,6 +42,9 @@ dependencies:
version: 1.0.1
devDependencies:
'@types/chrome':
specifier: ^0.0.246
version: 0.0.246
'@types/diff':
specifier: ^5.0.3
version: 5.0.3
@ -107,6 +120,9 @@ devDependencies:
typescript:
specifier: ^5.0.4
version: 5.0.4
zip-local:
specifier: ^0.3.5
version: 0.3.5
packages:
@ -520,10 +536,31 @@ packages:
resolution: {integrity: sha512-gAC33DCXYwNTI/k1PxOVHmbbzakUSMbb/DHpoV6rn4pKZtPI1dduULSmAAm/y1ipgIlArnk2JcnQzw4n2tCZHw==}
dev: false
/@types/chrome@0.0.246:
resolution: {integrity: sha512-MxGxEomGxsJiL9xe/7ZwVgwdn8XVKWbPvxpVQl3nWOjrS0Ce63JsfzxUc4aU3GvRcUPYsfufHmJ17BFyKxeA4g==}
dependencies:
'@types/filesystem': 0.0.33
'@types/har-format': 1.2.13
dev: true
/@types/diff@5.0.3:
resolution: {integrity: sha512-amrLbRqTU9bXMCc6uX0sWpxsQzRIo9z6MJPkH1pkez/qOxuqSZVuryJAWoBRq94CeG8JxY+VK4Le9HtjQR5T9A==}
dev: true
/@types/filesystem@0.0.33:
resolution: {integrity: sha512-2KedRPzwu2K528vFkoXnnWdsG0MtUwPjuA7pRy4vKxlxHEe8qUDZibYHXJKZZr2Cl/ELdCWYqyb/MKwsUuzBWw==}
dependencies:
'@types/filewriter': 0.0.30
dev: true
/@types/filewriter@0.0.30:
resolution: {integrity: sha512-lB98tui0uxc7erbj0serZfJlHKLNJHwBltPnbmO1WRpL5T325GOHRiQfr2E29V2q+S1brDO63Fpdt6vb3bES9Q==}
dev: true
/@types/har-format@1.2.13:
resolution: {integrity: sha512-PwBsCBD3lDODn4xpje3Y1di0aDJp4Ww7aSfMRVw6ysnxD4I7Wmq2mBkSKaDtN403hqH5sp6c9xQUvFYY3+lkBg==}
dev: true
/@types/json-schema@7.0.11:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
@ -843,6 +880,10 @@ packages:
engines: {node: '>=8'}
dev: true
/async@1.5.2:
resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==}
dev: true
/atob@2.1.2:
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
engines: {node: '>= 4.5.0'}
@ -1867,6 +1908,10 @@ packages:
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
dev: true
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
dev: true
/grapheme-splitter@1.0.4:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
dev: true
@ -2184,6 +2229,12 @@ packages:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: false
/jszip@2.7.0:
resolution: {integrity: sha512-JIsRKRVC3gTRo2vM4Wy9WBC3TRcfnIZU8k65Phi3izkvPH975FowRYtKGT6PxevA0XnJ/yO8b0QwV0ydVyQwfw==}
dependencies:
pako: 1.0.11
dev: true
/kind-of@3.2.2:
resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
engines: {node: '>=0.10.0'}
@ -2354,6 +2405,10 @@ packages:
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
dev: true
/monaco-editor@0.43.0:
resolution: {integrity: sha512-cnoqwQi/9fml2Szamv1XbSJieGJ1Dc8tENVMD26Kcfl7xGQWp7OBKMjlwKVGYFJ3/AXJjSOGvcqK7Ry/j9BM1Q==}
dev: false
/ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
dev: true
@ -2511,6 +2566,10 @@ packages:
engines: {node: '>=6'}
dev: true
/pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
dev: true
/parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@ -2662,6 +2721,11 @@ packages:
- utf-8-validate
dev: true
/q@1.5.1:
resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==}
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
dev: true
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
@ -3377,6 +3441,17 @@ packages:
engines: {node: '>=10'}
dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
/zip-local@0.3.5:
resolution: {integrity: sha512-GRV3D5TJY+/PqyeRm5CYBs7xVrKTKzljBoEXvocZu0HJ7tPEcgpSOYa2zFIsCZWgKWMuc4U3yMFgFkERGFIB9w==}
dependencies:
async: 1.5.2
graceful-fs: 4.2.11
jszip: 2.7.0
q: 1.5.1
dev: true
github.com/mattdesl/gifenc/64842fca317b112a8590f8fef2bf3825da8f6fe3:
resolution: {tarball: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3}
name: gifenc
version: 1.0.3
dev: false

View File

@ -78,6 +78,7 @@ await Promise.all([
define: {
...defines,
IS_WEB: false,
IS_EXTENSION: false,
IS_DISCORD_DESKTOP: true,
IS_VESKTOP: false
}
@ -124,6 +125,7 @@ await Promise.all([
define: {
...defines,
IS_WEB: false,
IS_EXTENSION: false,
IS_DISCORD_DESKTOP: false,
IS_VESKTOP: true
}

View File

@ -17,12 +17,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import esbuild from "esbuild";
import { zip } from "fflate";
import { readFileSync } from "fs";
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
import { join } from "path";
import Zip from "zip-local";
import { BUILD_TIMESTAMP, commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
@ -42,6 +41,7 @@ const commonOptions = {
target: ["esnext"],
define: {
IS_WEB: "true",
IS_EXTENSION: "false",
IS_STANDALONE: "true",
IS_DEV: JSON.stringify(watch),
IS_DISCORD_DESKTOP: "false",
@ -52,19 +52,58 @@ const commonOptions = {
}
};
const MonacoWorkerEntryPoints = [
"vs/language/css/css.worker.js",
"vs/editor/editor.worker.js"
];
const RnNoiseFiles = [
"dist/rnnoise.wasm",
"dist/rnnoise_simd.wasm",
"dist/rnnoise/workletProcessor.js",
"LICENSE"
];
await Promise.all(
[
esbuild.build({
entryPoints: MonacoWorkerEntryPoints.map(entry => `node_modules/monaco-editor/esm/${entry}`),
bundle: true,
minify: true,
format: "iife",
outbase: "node_modules/monaco-editor/esm/",
outdir: "dist/monaco"
}),
esbuild.build({
entryPoints: ["browser/monaco.ts"],
bundle: true,
minify: true,
format: "iife",
outfile: "dist/monaco/index.js",
loader: {
".ttf": "file"
}
}),
esbuild.build({
...commonOptions,
outfile: "dist/browser.js",
footer: { js: "//# sourceURL=VencordWeb" },
}),
esbuild.build({
...commonOptions,
outfile: "dist/extension.js",
define: {
...commonOptions?.define,
IS_EXTENSION: "true",
},
footer: { js: "//# sourceURL=VencordWeb" },
}),
esbuild.build({
...commonOptions,
inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])],
define: {
"window": "unsafeWindow",
...(commonOptions?.define)
...(commonOptions?.define),
window: "unsafeWindow",
},
outfile: "dist/Vencord.user.js",
banner: {
@ -79,12 +118,41 @@ await Promise.all(
);
/**
* @type {(target: string, files: string[], shouldZip: boolean) => Promise<void>}
* @type {(dir: string) => Promise<string[]>}
*/
async function buildPluginZip(target, files, shouldZip) {
async function globDir(dir) {
const files = [];
for (const child of await readdir(dir, { withFileTypes: true })) {
const p = join(dir, child.name);
if (child.isDirectory())
files.push(...await globDir(p));
else
files.push(p);
}
return files;
}
/**
* @type {(dir: string, basePath?: string) => Promise<Record<string, string>>}
*/
async function loadDir(dir, basePath = "") {
const files = await globDir(dir);
return Object.fromEntries(await Promise.all(files.map(async f => [f.slice(basePath.length), await readFile(f)])));
}
/**
* @type {(target: string, files: string[]) => Promise<void>}
*/
async function buildExtension(target, files, noMonaco = false) {
const entries = {
"dist/Vencord.js": await readFile("dist/browser.js"),
"dist/Vencord.css": await readFile("dist/browser.css"),
"dist/Vencord.js": await readFile("dist/extension.js"),
"dist/Vencord.css": await readFile("dist/extension.css"),
...(noMonaco ? {} : await loadDir("dist/monaco")),
...Object.fromEntries(await Promise.all(RnNoiseFiles.map(async file =>
[`third-party/rnnoise/${file.replace(/^dist\//, "")}`, await readFile(`node_modules/@sapphi-red/web-noise-suppressor/${file}`)]
))),
...Object.fromEntries(await Promise.all(files.map(async f => {
let content = await readFile(join("browser", f));
if (f.startsWith("manifest")) {
@ -100,31 +168,15 @@ async function buildPluginZip(target, files, shouldZip) {
}))),
};
if (shouldZip) {
return new Promise((resolve, reject) => {
zip(entries, {}, (err, data) => {
if (err) {
reject(err);
} else {
const out = join("dist", target);
writeFile(out, data).then(() => {
console.info("Extension written to " + out);
resolve();
}).catch(reject);
}
});
});
} else {
await rm(target, { recursive: true, force: true });
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
const dest = join("dist", target, file);
const parentDirectory = join(dest, "..");
await mkdir(parentDirectory, { recursive: true });
await writeFile(dest, content);
}));
await rm(target, { recursive: true, force: true });
await Promise.all(Object.entries(entries).map(async ([file, content]) => {
const dest = join("dist", target, file);
const parentDirectory = join(dest, "..");
await mkdir(parentDirectory, { recursive: true });
await writeFile(dest, content);
}));
console.info("Unpacked Extension written to dist/" + target);
}
console.info("Unpacked Extension written to dist/" + target);
}
const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content => {
@ -142,8 +194,9 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
await Promise.all([
appendCssRuntime,
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
buildPluginZip("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
buildPluginZip("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"], false),
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
buildExtension("firefox-unpacked", ["content.js", "manifestv2.json", "icon.png"], true),
]);
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension.zip");
console.info("Packed Chromium Extension written to dist/extension.zip");

View File

@ -165,7 +165,11 @@ async function parseFile(fileName: string) {
data.target = target as any;
}
return data;
let readme = "";
try {
readme = readFileSync(join(fileName, "..", "README.md"), "utf-8");
} catch { }
return [data, readme] as const;
}
throw fail("no default export called 'definePlugin' found");
@ -194,18 +198,24 @@ function isPluginFile({ name }: { name: string; }) {
(async () => {
parseDevs();
const plugins = ["src/plugins", "src/plugins/_core"].flatMap(dir =>
const plugins = [] as PluginData[];
const readmes = {} as Record<string, string>;
await Promise.all(["src/plugins", "src/plugins/_core"].flatMap(dir =>
readdirSync(dir, { withFileTypes: true })
.filter(isPluginFile)
.map(async dirent =>
parseFile(await getEntryPoint(dir, dirent))
)
);
.map(async dirent => {
const [data, readme] = await parseFile(await getEntryPoint(dir, dirent));
plugins.push(data);
if (readme) readmes[data.name] = readme;
})
));
const data = JSON.stringify(await Promise.all(plugins));
const data = JSON.stringify(plugins);
if (process.argv.length > 2) {
if (process.argv.length > 3) {
writeFileSync(process.argv[2], data);
writeFileSync(process.argv[3], JSON.stringify(readmes));
} else {
console.log(data);
}

View File

@ -171,7 +171,7 @@ page.on("console", async e => {
plugin,
type,
id,
match: regex,
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
error: cause
});
break;

View File

@ -0,0 +1,21 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findByPropsLazy } from "@webpack";
import { Parser } from "@webpack/common";
const CodeContainerClasses = findByPropsLazy("markup", "codeContainer");
/**
* Renders code in a Discord codeblock
*/
export function CodeBlock(props: { content?: string, lang: string; }) {
return (
<div className={CodeContainerClasses.markup}>
{Parser.defaultRules.codeBlock.react(props, null, {})}
</div>
);
}

View File

@ -0,0 +1,113 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./contributorModal.css";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { DevsById } from "@utils/constants";
import { fetchUserProfile, getTheme, Theme } from "@utils/discord";
import { ModalContent, ModalRoot, openModal } from "@utils/modal";
import { Forms, MaskedLink, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common";
import { User } from "discord-types/general";
import Plugins from "~plugins";
import { PluginCard } from ".";
const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg";
const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg";
const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg";
const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg";
const cl = classNameFactory("vc-author-modal-");
export function openContributorModal(user: User) {
openModal(modalProps =>
<ModalRoot {...modalProps}>
<ErrorBoundary>
<ModalContent className={cl("root")}>
<ContributorModal user={user} />
</ModalContent>
</ErrorBoundary>
</ModalRoot>
);
}
function GithubIcon() {
const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark;
return <img src={src} alt="GitHub" />;
}
function WebsiteIcon() {
const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark;
return <img src={src} alt="Website" />;
}
function ContributorModal({ user }: { user: User; }) {
useSettings();
const profile = useStateFromStores([UserProfileStore], () => UserProfileStore.getUserProfile(user.id));
useEffect(() => {
if (!profile && !user.bot && user.id)
fetchUserProfile(user.id);
}, [user.id]);
const githubName = profile?.connectedAccounts?.find(a => a.type === "github")?.name;
const website = profile?.connectedAccounts?.find(a => a.type === "domain")?.name;
const plugins = useMemo(() => {
const allPlugins = Object.values(Plugins);
const pluginsByAuthor = DevsById[user.id]
? allPlugins.filter(p => p.authors.includes(DevsById[user.id]))
: allPlugins.filter(p => p.authors.some(a => a.name === user.username));
return pluginsByAuthor
.filter(p => !p.name.endsWith("API"))
.sort((a, b) => Number(a.required ?? false) - Number(b.required ?? false));
}, [user.id, user.username]);
return (
<>
<div className={cl("header")}>
<img
className={cl("avatar")}
src={user.getAvatarURL(void 0, 512, true)}
alt=""
/>
<Forms.FormTitle tag="h2" className={cl("name")}>{user.username}</Forms.FormTitle>
<div className={cl("links")}>
{website && (
<MaskedLink
href={"https://" + website}
>
<WebsiteIcon />
</MaskedLink>
)}
{githubName && (
<MaskedLink href={`https://github.com/${githubName}`}>
<GithubIcon />
</MaskedLink>
)}
</div>
</div>
<div className={cl("plugins")}>
{plugins.map(p =>
<PluginCard
key={p.name}
plugin={p}
disabled={p.required ?? false}
onRestartNeeded={() => showToast("Restart to apply changes!")}
/>
)}
</div>
</>
);
}

View File

@ -18,17 +18,16 @@
import { generateId } from "@api/Commands";
import { useSettings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { proxyLazy } from "@utils/lazy";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { classes, isObjectEmpty } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { LazyComponent } from "@utils/react";
import { OptionType, Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
import { User } from "discord-types/general";
import { Constructor } from "type-fest";
@ -41,7 +40,7 @@ import {
SettingSliderComponent,
SettingTextComponent
} from "./components";
import hideBotTagStyle from "./userPopoutHideBotTag.css?managed";
import { openContributorModal } from "./ContributorModal";
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
@ -89,30 +88,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
const canSubmit = () => Object.values(errors).every(e => !e);
const hasSettings = Boolean(pluginSettings && plugin.options);
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
React.useEffect(() => {
enableStyle(hideBotTagStyle);
let originalUser: User;
(async () => {
for (const user of plugin.authors.slice(0, 6)) {
const author = user.id
? await UserUtils.fetchUser(`${user.id}`)
// only show name & pfp and no actions so users cannot harass plugin devs for support (send dms, add as friend, etc)
.then(u => (originalUser = u, makeDummyUser(u)))
.catch(() => makeDummyUser({ username: user.name }))
: makeDummyUser({ username: user.name });
setAuthors(a => [...a, author]);
}
})();
return () => {
disableStyle(hideBotTagStyle);
if (originalUser)
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: originalUser });
};
}, []);
async function saveAndClose() {
@ -214,6 +202,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
showDefaultAvatarsForNullUsers
showUserPopout
renderMoreUsers={renderMoreUsers}
renderUser={(user: User) => (
<Clickable
className={AvatarStyles.clickableAvatar}
onClick={() => openContributorModal(user)}
>
<img
className={AvatarStyles.avatar}
src={user.getAvatarURL(void 0, 80, true)}
alt={user.username}
title={user.username}
/>
</Clickable>
)}
/>
</div>
</Forms.FormSection>

View File

@ -0,0 +1,57 @@
.vc-author-modal-root {
padding: 1em;
}
.vc-author-modal-header {
display: flex;
align-items: center;
margin-bottom: 1em;
}
.vc-author-modal-name {
text-transform: none;
flex-grow: 0;
background: var(--background-tertiary);
border-radius: 0 9999px 9999px 0;
padding: 6px 0.8em 6px 0.5em;
font-size: 20px;
height: 20px;
position: relative;
}
.vc-author-modal-name::before {
content: "";
display: block;
position: absolute;
height: 100%;
width: 16px;
background: var(--background-tertiary);
z-index: -1;
left: -16px;
top: 0;
}
.vc-author-modal-avatar {
height: 32px;
width: 32px;
border-radius: 50%;
}
.vc-author-modal-links {
margin-left: auto;
display: flex;
gap: 0.2em;
}
.vc-author-modal-links img {
height: 32px;
width: 32px;
border-radius: 50%;
border: 4px solid var(--background-tertiary);
box-sizing: border-box
}
.vc-author-modal-plugins {
display: grid;
gap: 0.5em;
}

View File

@ -28,7 +28,7 @@ import { SettingsTab } from "@components/VencordSettings/shared";
import { ChangeList } from "@utils/ChangeList";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { classes, isObjectEmpty } from "@utils/misc";
import { openModalLazy } from "@utils/modal";
import { LazyComponent, useAwaiter } from "@utils/react";
import { Plugin } from "@utils/types";
@ -91,7 +91,7 @@ interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
isNew?: boolean;
}
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave, isNew }: PluginCardProps) {
const settings = Settings.plugins[plugin.name];
const isEnabled = () => settings.enabled ?? false;
@ -161,7 +161,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
onMouseLeave={onMouseLeave}
infoButton={
<button role="switch" onClick={() => openModal()} className={classes(ButtonClasses.button, cl("info-button"))}>
{plugin.options
{plugin.options && !isObjectEmpty(plugin.options)
? <CogWheel />
: <InfoIcon width="24" height="24" />}
</button>

View File

@ -1,3 +0,0 @@
[class|="userPopoutOuter"] [class*="botTag"] {
display: none;
}

View File

@ -17,6 +17,7 @@
*/
import { CheckedTextInput } from "@components/CheckedTextInput";
import { CodeBlock } from "@components/CodeBlock";
import { debounce } from "@utils/debounce";
import { Margins } from "@utils/margins";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
@ -299,7 +300,7 @@ function PatchHelper() {
{!!(find && match && replacement) && (
<>
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
<CodeBlock lang="js" content={code} />
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
</>
)}

View File

@ -18,8 +18,10 @@
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { IsFirefox } from "@utils/constants";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { showItemInFolder } from "@utils/native";
@ -249,12 +251,14 @@ function ThemesTab() {
>
Load missing Themes
</Button>
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
>
Edit QuickCSS
</Button>
{!IsFirefox && (
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
>
Edit QuickCSS
</Button>
)}
</>
</Card>
@ -316,6 +320,15 @@ function ThemesTab() {
return (
<SettingsTab title="Themes">
{IsFirefox && (
<ErrorCard>
<Forms.FormTitle tag="h5">Warning</Forms.FormTitle>
<Forms.FormText>
You are using Firefox. Expect the vast majority of themes to not work.
If this is a problem, use a chromium browser or Discord Desktop / Vesktop.
</Forms.FormText>
</ErrorCard>
)}
<TabBar
type="top"
look="brand"

View File

@ -21,6 +21,7 @@ import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
import { ErrorCard } from "@components/ErrorCard";
import { IsFirefox } from "@utils/constants";
import { Margins } from "@utils/margins";
import { identity } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native";
@ -109,12 +110,14 @@ function VencordSettings() {
Restart Client
</Button>
)}
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}>
Open QuickCSS File
</Button>
{!IsFirefox && (
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}>
Open QuickCSS File
</Button>
)}
{!IS_WEB && (
<Button
onClick={() => showItemInFolder(settingsDir)}
@ -257,7 +260,11 @@ function DonateCard({ image }: DonateCardProps) {
src={image}
alt=""
height={128}
style={{ marginLeft: "auto", transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : "" }}
style={{
imageRendering: image === SHIGGY_DONATE_IMAGE ? "pixelated" : void 0,
marginLeft: "auto",
transform: image === DEFAULT_DONATE_IMAGE ? "rotate(10deg)" : void 0
}}
/>
</Card>
);

View File

@ -8,6 +8,7 @@
width: 100%;
transition: 0.1s ease-out;
transition-property: box-shadow, transform, background, opacity;
box-sizing: border-box;
}
.vc-addon-card-disabled {

1
src/globals.d.ts vendored
View File

@ -33,6 +33,7 @@ declare global {
* replace: `${IS_WEB}?foo:bar`
*/
export var IS_WEB: boolean;
export var IS_EXTENSION: boolean;
export var IS_DEV: boolean;
export var IS_STANDALONE: boolean;
export var IS_UPDATER_DISABLED: boolean;

View File

@ -27,7 +27,7 @@ import { mkdirSync, readFileSync, watch } from "fs";
import { open, readdir, readFile, writeFile } from "fs/promises";
import { join, normalize } from "path";
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
import monacoHtml from "~fileContent/monacoWin.html;base64";
import { getThemeInfo, stripBOM, UserThemeHeader } from "./themes";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE, THEMES_DIR } from "./utils/constants";

View File

@ -17,7 +17,7 @@
*/
import { DataStore } from "@api/index";
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { Devs, IsFirefox, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { isPluginDev } from "@utils/misc";
import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
@ -27,9 +27,10 @@ import { Alerts, Forms, UserStore } from "@webpack/common";
import gitHash from "~git-hash";
import plugins from "~plugins";
import settings from "./_core/settings";
import settings from "./settings";
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
const FIREFOX_DISMISS_KEY = "Vencord-Firefox-Warning-Dismiss";
const AllowedChannelIds = [
SUPPORT_CHANNEL_ID,
@ -48,7 +49,7 @@ export default definePlugin({
name: "vencord-debug",
description: "Send Vencord Debug info",
predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
execute() {
async execute() {
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
const client = (() => {
@ -75,6 +76,10 @@ export default definePlugin({
OpenAsar: "openasar" in window,
};
if (IS_DISCORD_DESKTOP) {
info["Last Crash Reason"] = (await DiscordNative.processUtils.getLastCrash())?.rendererCrashReason ?? "N/A";
}
const debugInfo = `
**Vencord Debug Info**
>>> ${Object.entries(info).map(([k, v]) => `${k}: ${v}`).join("\n")}
@ -111,6 +116,22 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
onConfirm: rememberDismiss
});
}
if (IsFirefox) {
const rememberDismiss = () => DataStore.set(FIREFOX_DISMISS_KEY, true);
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>You are using Firefox.</Forms.FormText>
<Forms.FormText>Due to Firefox's stupid extension guidelines, most themes and many plugins will not function correctly.</Forms.FormText>
<Forms.FormText>Do not report bugs. Do not ask for help with broken plugins.</Forms.FormText>
<Forms.FormText>Instead, use a chromium browser, Discord Desktop, or Vesktop.</Forms.FormText>
</div>,
onCancel: rememberDismiss,
onConfirm: rememberDismiss
});
}
}
}
});

View File

@ -23,7 +23,7 @@ import { Clipboard, Toasts } from "@webpack/common";
export default definePlugin({
name: "BetterRoleDot",
authors: [Devs.Ven],
authors: [Devs.Ven, Devs.AutumnVN],
description:
"Copy role colour on RoleDot (accessibility setting) click. Also allows using both RoleDot and coloured names simultaneously",
@ -43,6 +43,23 @@ export default definePlugin({
match: /"(?:username|dot)"===\i(?!\.\i)/g,
replace: "true",
},
},
{
find: ".ADD_ROLE_A11Y_LABEL",
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout,
replacement: {
match: /"dot"===\i/,
replace: "true"
}
},
{
find: ".roleVerifiedIcon",
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout,
replacement: {
match: /"dot"===\i/,
replace: "true"
}
}
],
@ -50,7 +67,14 @@ export default definePlugin({
bothStyles: {
type: OptionType.BOOLEAN,
description: "Show both role dot and coloured names",
restartNeeded: true,
default: false,
},
copyRoleColorInProfilePopout: {
type: OptionType.BOOLEAN,
description: "Allow click on role dot in profile popout to copy role color",
restartNeeded: true,
default: false
}
},

View File

@ -49,7 +49,7 @@ export default definePlugin({
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
}, {
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\.embedWrapper)/g,
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
replace: "$1,vcProps=$2$3+(vcProps.nsfw?' vc-nsfw-img':'')"
}]
}
],

View File

@ -127,6 +127,8 @@ export const defaultRules = [
"redircnt@yandex.*",
"feature@youtube.com",
"kw@youtube.com",
"si@youtube.com",
"pp@youtube.com",
"si@youtu.be",
"wt_zmc",
"utm_source",
@ -136,4 +138,5 @@ export const defaultRules = [
"utm_term",
"si@open.spotify.com",
"igshid",
"share_id@reddit.com",
];

View File

@ -0,0 +1,5 @@
# Dearrow
Makes YouTube embed titles and thumbnails less sensationalist, powered by [Dearrow](https://dearrow.ajay.app/)
https://github.com/Vendicated/Vencord/assets/45497981/7bf81108-102d-47c5-8ba5-357db4db1283

View File

@ -0,0 +1,161 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin from "@utils/types";
import { Tooltip } from "@webpack/common";
import type { Component } from "react";
interface Props {
embed: {
rawTitle: string;
provider?: {
name: string;
};
thumbnail: {
proxyURL: string;
};
video: {
url: string;
};
dearrow: {
enabled: boolean;
oldTitle?: string;
oldThumb?: string;
};
};
}
const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/;
async function embedDidMount(this: Component<Props>) {
try {
const { embed } = this.props;
if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return;
const videoId = embedUrlRe.exec(embed.video.url)?.[1];
if (!videoId) return;
const res = await fetch(`https://sponsor.ajay.app/api/branding?videoID=${videoId}`);
if (!res.ok) return;
const { titles, thumbnails } = await res.json();
const hasTitle = titles[0]?.votes >= 0;
const hasThumb = thumbnails[0]?.votes >= 0;
if (!hasTitle && !hasThumb) return;
embed.dearrow = {
enabled: true
};
if (titles[0]?.votes >= 0) {
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = titles[0].title;
}
if (thumbnails[0]?.votes >= 0) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`;
}
this.forceUpdate();
} catch (err) {
new Logger("Dearrow").error("Failed to dearrow embed", err);
}
}
function DearrowButton({ component }: { component: Component<Props>; }) {
const { embed } = component.props;
if (!embed?.dearrow) return null;
return (
<Tooltip text={embed.dearrow.enabled ? "This embed has been dearrowed, click to restore" : "Click to dearrow"}>
{({ onMouseEnter, onMouseLeave }) => (
<button
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={"vc-dearrow-toggle-" + (embed.dearrow.enabled ? "on" : "off")}
onClick={() => {
const { enabled, oldThumb, oldTitle } = embed.dearrow;
embed.dearrow.enabled = !enabled;
if (oldTitle) {
embed.dearrow.oldTitle = embed.rawTitle;
embed.rawTitle = oldTitle;
}
if (oldThumb) {
embed.dearrow.oldThumb = embed.thumbnail.proxyURL;
embed.thumbnail.proxyURL = oldThumb;
}
component.forceUpdate();
}}
>
{/* Dearrow Icon, taken from https://dearrow.ajay.app/logo.svg (and optimised) */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24px"
height="24px"
viewBox="0 0 36 36"
aria-label="Toggle Dearrow"
>
<path
fill="#1213BD"
d="M36 18.302c0 4.981-2.46 9.198-5.655 12.462s-7.323 5.152-12.199 5.152s-9.764-1.112-12.959-4.376S0 23.283 0 18.302s2.574-9.38 5.769-12.644S13.271 0 18.146 0s9.394 2.178 12.589 5.442C33.931 8.706 36 13.322 36 18.302z"
/>
<path
fill="#88c9f9"
d="m 30.394282,18.410186 c 0,3.468849 -1.143025,6.865475 -3.416513,9.137917 -2.273489,2.272442 -5.670115,2.92874 -9.137918,2.92874 -3.467803,0 -6.373515,-1.147212 -8.6470033,-3.419654 -2.2734888,-2.272442 -3.5871299,-5.178154 -3.5871299,-8.647003 0,-3.46885 0.9420533,-6.746149 3.2144954,-9.0196379 2.2724418,-2.2734888 5.5507878,-3.9513905 9.0196378,-3.9513905 3.46885,0 6.492841,1.9322561 8.76633,4.204698 2.273489,2.2724424 3.788101,5.2974804 3.788101,8.7663304 z"
/>
<path
fill="#0a62a5"
d="m 23.95823,17.818306 c 0,3.153748 -2.644888,5.808102 -5.798635,5.808102 -3.153748,0 -5.599825,-2.654354 -5.599825,-5.808102 0,-3.153747 2.446077,-5.721714 5.599825,-5.721714 3.153747,0 5.798635,2.567967 5.798635,5.721714 z"
/>
</svg>
</button>
)}
</Tooltip>
);
}
export default definePlugin({
name: "Dearrow",
description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow",
authors: [Devs.Ven],
embedDidMount,
renderButton(component: Component<Props>) {
return (
<ErrorBoundary noop>
<DearrowButton component={component} />
</ErrorBoundary>
);
},
patches: [{
find: "this.renderInlineMediaEmbed",
replacement: [
// patch componentDidMount to replace embed thumbnail and title
{
match: /(\i).render=function.{0,50}\i\.embed/,
replace: "$1.componentDidMount=$self.embedDidMount,$&"
},
// add dearrow button
{
match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/,
replace: "children:[$self.renderButton(this),"
}
]
}],
});

View File

@ -0,0 +1,12 @@
.vc-dearrow-toggle-off svg {
filter: grayscale(1);
}
.vc-dearrow-toggle-on, .vc-dearrow-toggle-off {
all: unset;
display: inline;
cursor: pointer;
position: absolute;
top: 0.75rem;
right: 0.75rem;
}

View File

@ -19,7 +19,7 @@
import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
import { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { ApngBlendOp, ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
import { ApngBlendOp, ApngDisposeOp, importApngJs } from "@utils/dependencies";
import { getCurrentGuild } from "@utils/discord";
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
@ -27,6 +27,7 @@ import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { ChannelStore, EmojiStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common";
import type { Message } from "discord-types/general";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react";
const DRAFT_TYPE = 0;
@ -102,6 +103,11 @@ interface StickerPack {
stickers: Sticker[];
}
const enum FakeNoticeType {
Sticker,
Emoji
}
const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
@ -212,15 +218,15 @@ export default definePlugin({
}
},
{
find: "canStreamHighQuality:function",
find: "canUseHighVideoUploadQuality:function",
predicate: () => settings.store.enableStreamQualityBypass,
replacement: [
"canUseHighVideoUploadQuality",
"canStreamHighQuality",
"canStreamMidQuality"
// TODO: Remove the last two when they get removed from stable
"(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)",
].map(func => {
return {
match: new RegExp(`${func}:function\\(\\i\\){`),
match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"),
replace: "$&return true;"
};
})
@ -305,16 +311,24 @@ export default definePlugin({
},
{
match: /(emojiSection.{0,50}description:)(\i)(?<=(\i)\.sticker,.+?)(?=,)/,
replace: (_, rest, reactNode, props) => `${rest}$self.addFakeNotice("STICKER",${reactNode},!!${props}.renderableSticker?.fake)`
replace: (_, rest, reactNode, props) => `${rest}$self.addFakeNotice(${FakeNoticeType.Sticker},${reactNode},!!${props}.renderableSticker?.fake)`
}
]
},
{
find: ".Messages.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION",
find: ".EMOJI_UPSELL_POPOUT_MORE_EMOJIS_OPENED,",
predicate: () => settings.store.transformEmojis,
replacement: {
match: /((\i)=\i\.node,\i=\i\.expressionSourceGuild)(.+?return )(.{0,450}Messages\.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION.+?}\))/,
replace: (_, rest1, node, rest2, reactNode) => `${rest1},fakeNitroNode=${node}${rest2}$self.addFakeNotice("EMOJI",${reactNode},fakeNitroNode.fake)`
match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<=(\i)=\i\.node.+?)/,
replace: (m, node) => `${m}fakeNitroNode:${node},`
}
},
{
find: ".Messages.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION",
predicate: () => settings.store.transformEmojis,
replacement: {
match: /(?<=\.Messages\.EMOJI_POPOUT_ADDED_PACK_DESCRIPTION.+?return )(.{0,1200}\.Messages\.EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION.+?)(}\({)/,
replace: (_, reactNode, rest) => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!arguments[0]?.fakeNitroNode?.fake)${rest}fakeNitroNode:arguments[0]?.fakeNitroNode,`
}
}
],
@ -610,18 +624,18 @@ export default definePlugin({
return link.target && fakeNitroEmojiRegex.test(link.target);
},
addFakeNotice(type: "STICKER" | "EMOJI", node: Array<ReactNode>, fake: boolean) {
addFakeNotice(type: FakeNoticeType, node: Array<ReactNode>, fake: boolean) {
if (!fake) return node;
node = Array.isArray(node) ? node : [node];
switch (type) {
case "STICKER": {
case FakeNoticeType.Sticker: {
node.push(" This is a FakeNitro sticker and renders like a real sticker only for you. Appears as a link to non-plugin users.");
return node;
}
case "EMOJI": {
case FakeNoticeType.Emoji: {
node.push(" This is a FakeNitro emoji and renders like a real emoji only for you. Appears as a link to non-plugin users.");
return node;
@ -650,15 +664,11 @@ export default definePlugin({
},
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
const [{ parseURL }, {
GIFEncoder,
quantize,
applyPalette
}] = await Promise.all([importApngJs(), getGifEncoder()]);
const { parseURL } = importApngJs();
const { frames, width, height } = await parseURL(stickerLink);
const gif = new GIFEncoder();
const gif = GIFEncoder();
const resolution = Settings.plugins.FakeNitro.stickerSize;
const canvas = document.createElement("canvas");

View File

@ -0,0 +1,6 @@
# FavoriteEmojiFirst
Puts your favorite emoji first in the emoji autocomplete.
![FavEmojis](https://i.imgur.com/mEFCoZG.png)
![Example](https://i.imgur.com/wY3Tc43.png)

View File

@ -0,0 +1,5 @@
# FavoriteGifSearch
Adds a search bar to favorite gifs.
![Screenshot](https://i.imgur.com/Bcgb7PD.png)

View File

@ -60,7 +60,7 @@ interface Instance {
}
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchHeader", "gutterSize");
const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchHeader", "searchInput");
export const settings = definePluginSettings({
searchOption: {
@ -87,7 +87,7 @@ export const settings = definePluginSettings({
export default definePlugin({
name: "FavoriteGifSearch",
authors: [Devs.Aria],
description: "Adds a search bar for favorite gifs",
description: "Adds a search bar to favorite gifs.",
patches: [
{

View File

@ -1,56 +0,0 @@
/*
* 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 { Forms } from "@webpack/common";
export default definePlugin({
name: "FixInbox",
description: "Fixes the Unreads Inbox from crashing Discord when you're in lots of guilds.",
authors: [Devs.Megu],
patches: [{
find: "INBOX_OPEN:function",
replacement: {
// This function normally dispatches a subscribe event to every guild.
// this is badbadbadbadbad so we just get rid of it.
match: /INBOX_OPEN:function.+?\{/,
replace: "$&return true;"
}
}],
settingsAboutComponent() {
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">What's the problem?</Forms.FormTitle>
<Forms.FormText style={{ marginBottom: 8 }}>
By default, Discord emits a GUILD_SUBSCRIPTIONS event for every guild you're in.
When you're in a lot of guilds, this can cause the gateway to ratelimit you.
This causes the client to crash and get stuck in an infinite ratelimit loop as it tries to reconnect.
</Forms.FormText>
<Forms.FormTitle tag="h3">How does it work?</Forms.FormTitle>
<Forms.FormText>
This plugin works by stopping the client from sending GUILD_SUBSCRIPTIONS events to the gateway when you open the unreads inbox.
This means that not all unreads will be shown, instead only already-subscribed guilds' unreads will be shown, but your client won't crash anymore.
</Forms.FormText>
</Forms.FormSection>
);
}
});

View File

@ -49,7 +49,7 @@ export default definePlugin({
await this.buildCss();
addButton("HideAttachments", msg => {
if (!msg.attachments.length && !msg.embeds.length) return null;
if (!msg.attachments.length && !msg.embeds.length && !msg.stickerItems.length) return null;
const isHidden = hiddenMessages.has(msg.id);
@ -72,7 +72,7 @@ export default definePlugin({
async buildCss() {
const elements = [...hiddenMessages].map(id => `#message-accessories-${id}`).join(",");
style.textContent = `
:is(${elements}) [class*="embedWrapper"] {
:is(${elements}) :is([class*="embedWrapper"], [class*="clickableSticker"]) {
/* important is not necessary, but add it to make sure bad themes won't break it */
display: none !important;
}

View File

@ -0,0 +1,6 @@
# ImageZoom
Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size
![Example](https://i.imgur.com/VJdo4aq.png)
![ContextMenu](https://i.imgur.com/0oaRM2s.png)

View File

@ -99,6 +99,15 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
ContextMenu.close();
}}
/>
<Menu.MenuCheckboxItem
id="vc-nearest-neighbour"
label="Nearset Neighbour"
checked={settings.store.nearestNeighbour}
action={() => {
settings.store.nearestNeighbour = !settings.store.nearestNeighbour;
ContextMenu.close();
}}
/>
<Menu.MenuControlItem
id="vc-zoom"
label="Zoom"

View File

@ -28,7 +28,7 @@ import { Button, Forms, React, TextInput } from "@webpack/common";
import { decrypt } from "../index";
export function DecModal(props: any) {
const secret: string = props?.message?.content;
const encryptedMessage: string = props?.message?.content;
const [password, setPassword] = React.useState("password");
return (
@ -38,9 +38,9 @@ export function DecModal(props: any) {
</ModalHeader>
<ModalContent>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Secret</Forms.FormTitle>
<TextInput defaultValue={secret} disabled={true}></TextInput>
<Forms.FormTitle tag="h5">Password</Forms.FormTitle>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Message with Encryption</Forms.FormTitle>
<TextInput defaultValue={encryptedMessage} disabled={true}></TextInput>
<Forms.FormTitle tag="h5" style={{ marginTop: "10px" }}>Password</Forms.FormTitle>
<TextInput
style={{ marginBottom: "20px" }}
onChange={setPassword}
@ -51,7 +51,7 @@ export function DecModal(props: any) {
<Button
color={Button.Colors.GREEN}
onClick={() => {
const toSend = decrypt(secret, password, true);
const toSend = decrypt(encryptedMessage, password, true);
if (!toSend || !props?.message) return;
// @ts-expect-error
Vencord.Plugins.plugins.InvisibleChat.buildEmbed(props?.message, toSend);

View File

@ -225,8 +225,8 @@ export function encrypt(secret: string, password: string, cover: string): string
return steggo.hide(secret + "\u200b", password, cover);
}
export function decrypt(secret: string, password: string, removeIndicator: boolean): string {
const decrypted = steggo.reveal(secret, password);
export function decrypt(encrypted: string, password: string, removeIndicator: boolean): string {
const decrypted = steggo.reveal(encrypted, password);
return removeIndicator ? decrypted.replace("\u200b", "") : decrypted;
}

View File

@ -72,6 +72,14 @@ const enum ActivityFlag {
INSTANCE = 1 << 0,
}
const enum NameFormat {
StatusName = "status-name",
ArtistFirst = "artist-first",
SongFirst = "song-first",
ArtistOnly = "artist",
SongOnly = "song"
}
const applicationId = "1108588077900898414";
const placeholderId = "2a96cbd8b46e442fc41c2b86b821562f";
@ -117,10 +125,37 @@ const settings = definePluginSettings({
default: true,
},
statusName: {
description: "text shown in status",
description: "custom status text",
type: OptionType.STRING,
default: "some music",
},
nameFormat: {
description: "Show name of song and artist in status name",
type: OptionType.SELECT,
options: [
{
label: "Use custom status name",
value: NameFormat.StatusName,
default: true
},
{
label: "Use format 'artist - song'",
value: NameFormat.ArtistFirst
},
{
label: "Use format 'song - artist'",
value: NameFormat.SongFirst
},
{
label: "Use artist name only",
value: NameFormat.ArtistOnly
},
{
label: "Use song name only",
value: NameFormat.SongOnly
}
],
},
useListeningStatus: {
description: 'show "Listening to" status instead of "Playing"',
type: OptionType.BOOLEAN,
@ -140,13 +175,13 @@ const settings = definePluginSettings({
value: "placeholder"
}
],
}
},
});
export default definePlugin({
name: "LastFMRichPresence",
description: "Little plugin for Last.fm rich presence",
authors: [Devs.dzshn, Devs.RuiNtD],
authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu],
settingsAboutComponent: () => (
<>
@ -267,9 +302,24 @@ export default definePlugin({
url: `https://www.last.fm/user/${settings.store.username}`,
});
const statusName = (() => {
switch (settings.store.nameFormat) {
case NameFormat.ArtistFirst:
return trackData.artist + " - " + trackData.name;
case NameFormat.SongFirst:
return trackData.name + " - " + trackData.artist;
case NameFormat.ArtistOnly:
return trackData.artist;
case NameFormat.SongOnly:
return trackData.name;
default:
return settings.store.statusName;
}
})();
return {
application_id: applicationId,
name: settings.store.statusName,
name: statusName,
details: trackData.name,
state: trackData.artist,

View File

@ -56,7 +56,8 @@ const quotes = [
"hd{b${",
"<;vqkijbq33271:56<3799?24944:",
"Thof$lu'ofdn,!qsefc'az*bnrcma+&Om{o+iu\"`khct$)bnrd\"bcdoi&",
"snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno"
"snofplkb{)c'r\"lod'|f*aurv#cpno`abchijklmno",
"Wdn`khc'|f*eghl{%"
];
export default definePlugin({

View File

@ -30,6 +30,9 @@ const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };
};
const sharedIntlNumberFormat = new Intl.NumberFormat();
const numberFormat = (value: number) => sharedIntlNumberFormat.format(value);
function MemberCount() {
const { id: channelId, guild_id: guildId } = useStateFromStores([SelectedChannelStore], () => getCurrentChannel());
const { groups } = useStateFromStores(
@ -57,7 +60,7 @@ function MemberCount() {
alignContent: "center",
gap: 0
}}>
<Tooltip text={`${online} Online in this Channel`} position="bottom">
<Tooltip text={`${numberFormat(online)} online in this channel`} position="bottom">
{props => (
<div {...props}>
<span
@ -70,11 +73,11 @@ function MemberCount() {
marginRight: "0.5em"
}}
/>
<span style={{ color: "var(--green-360)" }}>{online}</span>
<span style={{ color: "var(--green-360)" }}>{numberFormat(online)}</span>
</div>
)}
</Tooltip>
<Tooltip text={`${total} Total Server Members`} position="bottom">
<Tooltip text={`${numberFormat(total)} total server members`} position="bottom">
{props => (
<div {...props}>
<span
@ -88,7 +91,7 @@ function MemberCount() {
marginLeft: "1em"
}}
/>
<span style={{ color: "var(--primary-400)" }}>{total}</span>
<span style={{ color: "var(--primary-400)" }}>{numberFormat(total)}</span>
</div>
)}
</Tooltip>

View File

@ -169,21 +169,14 @@ export default definePlugin({
try {
if (cache == null || (!isBulk && !cache.has(data.id))) return cache;
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id;
function mutate(id: string) {
const mutate = (id: string) => {
const msg = cache.get(id);
if (!msg) return;
const EPHEMERAL = 64;
const shouldIgnore = data.mlDeleted ||
(msg.flags & EPHEMERAL) === EPHEMERAL ||
ignoreBots && msg.author?.bot ||
ignoreSelf && msg.author?.id === myId ||
ignoreUsers.includes(msg.author?.id) ||
ignoreChannels.includes(msg.channel_id) ||
ignoreGuilds.includes(ChannelStore.getChannel(msg.channel_id)?.guild_id);
this.shouldIgnore(msg);
if (shouldIgnore) {
cache = cache.remove(id);
@ -192,7 +185,7 @@ export default definePlugin({
.set("deleted", true)
.set("attachments", m.attachments.map(a => (a.deleted = true, a))));
}
}
};
if (isBulk) {
data.ids.forEach(mutate);
@ -205,6 +198,18 @@ export default definePlugin({
return cache;
},
shouldIgnore(message: any) {
const { ignoreBots, ignoreSelf, ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
const myId = UserStore.getCurrentUser().id;
return ignoreBots && message.author?.bot ||
ignoreSelf && message.author?.id === myId ||
ignoreUsers.includes(message.author?.id) ||
ignoreChannels.includes(message.channel_id) ||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id);
},
// Based on canary 9ab8626bcebceaea6da570b9c586172d02b9c996
patches: [
{
@ -237,7 +242,7 @@ export default definePlugin({
match: /(MESSAGE_UPDATE:function\((\w)\).+?)\.update\((\w)/,
replace: "$1" +
".update($3,m =>" +
" (($2.message.flags & 64) === 64 || (Vencord.Settings.plugins.MessageLogger.ignoreBots && $2.message.author?.bot) || (Vencord.Settings.plugins.MessageLogger.ignoreSelf && $2.message.author?.id === Vencord.Webpack.Common.UserStore.getCurrentUser().id)) ? m :" +
" (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message)) ? m :" +
" $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" +
" m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" +
" m" +
@ -321,14 +326,14 @@ export default definePlugin({
{
// Attachment renderer
// Module 96063
find: "[\"className\",\"attachment\",\"inlineMedia\"",
find: "().removeAttachmentHoverButton",
replacement: [
{
match: /((\w)\.className,\w=\2\.attachment),/,
replace: "$1,deleted=$2.attachment?.deleted,"
},
{
match: /\["className","attachment","inlineMedia".+?className:/,
match: /\["className","attachment".+?className:/,
replace: "$& (deleted ? 'messagelogger-deleted-attachment ' : '') +"
}
]

View File

@ -185,7 +185,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "MoreUserTags",
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias],
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN],
settings,
patches: [
// add tags to the tag list
@ -232,9 +232,8 @@ export default definePlugin({
{
find: ".renderBot=function(){",
replacement: {
match: /this.props.user;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag/,
replace: "this.props.user;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});\
return type!==null?$2.botTag,type"
match: /\.BOT;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag,type:\i/,
replace: ".BOT;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});return type!==null?$2.botTag,type"
}
},
// pass channel id down props to be used in profiles

View File

@ -178,12 +178,12 @@ export default definePlugin({
start() {
addContextMenuPatch("user-context", this.userContextMenuPatch);
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
addContextMenuPatch("guild-context", this.guildContextMenuPatch);
addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
},
stop() {
removeContextMenuPatch("user-context", this.userContextMenuPatch);
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
removeContextMenuPatch("guild-context", this.guildContextMenuPatch);
removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
},
});

View File

@ -18,10 +18,10 @@
import { ApplicationCommandInputType, ApplicationCommandOptionType, Argument, CommandContext, findOption, sendBotMessage } from "@api/Commands";
import { Devs } from "@utils/constants";
import { getGifEncoder } from "@utils/dependencies";
import { makeLazy } from "@utils/lazy";
import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
const DRAFT_TYPE = 0;
const DEFAULT_DELAY = 20;
@ -124,7 +124,6 @@ export default definePlugin({
}
],
execute: async (opts, cmdCtx) => {
const { GIFEncoder, quantize, applyPalette } = await getGifEncoder();
const frames = await getFrames();
const noServerPfp = findOption(opts, "no-server-pfp", false);
@ -143,7 +142,7 @@ export default definePlugin({
const delay = findOption(opts, "delay", DEFAULT_DELAY);
const resolution = findOption(opts, "resolution", DEFAULT_RESOLUTION);
const gif = new GIFEncoder();
const gif = GIFEncoder();
const canvas = document.createElement("canvas");
canvas.width = canvas.height = resolution;

View File

@ -0,0 +1,83 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { React, Tooltip } from "@webpack/common";
const settings = definePluginSettings({
loop: {
description: "Whether to make the PiP video loop or not",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: false
}
});
export default definePlugin({
name: "PictureInPicture",
description: "Adds picture in picture to videos (next to the Download button)",
authors: [Devs.Lumap],
settings,
patches: [
{
find: ".onRemoveAttachment,",
replacement: {
match: /\.nonMediaAttachment,!(\i).{0,7}children:\[(\i),/,
replace: "$&$1&&$2&&$self.renderPiPButton(),"
},
},
],
renderPiPButton: ErrorBoundary.wrap(() => {
return (
<Tooltip text="Toggle Picture in Picture">
{tooltipProps => (
<div
{...tooltipProps}
role="button"
style={{
cursor: "pointer",
paddingTop: "4px",
paddingLeft: "4px",
paddingRight: "4px",
}}
onClick={e => {
const video = e.currentTarget.parentNode!.parentNode!.querySelector("video")!;
const videoClone = document.body.appendChild(video.cloneNode(true)) as HTMLVideoElement;
videoClone.loop = settings.store.loop;
videoClone.style.display = "none";
videoClone.onleavepictureinpicture = () => videoClone.remove();
function launchPiP() {
videoClone.currentTime = video.currentTime;
videoClone.requestPictureInPicture();
video.pause();
videoClone.play();
}
if (videoClone.readyState === 4 /* HAVE_ENOUGH_DATA */)
launchPiP();
else
videoClone.onloadedmetadata = launchPiP;
}}
>
<svg width="24px" height="24px" viewBox="0 0 24 24">
<path
fill="var(--interactive-normal)"
d="M21 3a1 1 0 0 1 1 1v7h-2V5H4v14h6v2H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18zm0 10a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h8zm-1 2h-6v4h6v-4z"
/>
</svg>
</div>
)}
</Tooltip>
);
}, { noop: true })
});

View File

@ -122,6 +122,14 @@ export default definePlugin({
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))"
}
}
},
// fix alt+shift+up/down
{
find: '"alt+shift+down"',
replacement: {
match: /(?<=return \i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
}
},
]
});

View File

@ -0,0 +1,5 @@
# PreviewMessage
Lets you preview your message before sending it.
![Example](https://i.imgur.com/etqbkzu.png)

View File

@ -16,38 +16,96 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { sendBotMessage } from "@api/Commands";
import { generateId, sendBotMessage } from "@api/Commands";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore } from "@webpack/common";
import { findByPropsLazy } from "@webpack";
import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common";
import { MessageAttachment } from "discord-types/general";
interface Props {
type: {
analyticsName: string;
isEmpty: boolean;
attachments: boolean;
};
}
const UploadStore = findByPropsLazy("getUploads");
const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage);
export function PreviewButton(chatBoxProps: Props) {
if (chatBoxProps.type.analyticsName !== "normal") return null;
const channelId = SelectedChannelStore.getChannelId();
const draft = getDraft(channelId);
if (!draft) return null;
const getImageBox = (url: string): Promise<{ width: number, height: number; } | null> =>
new Promise(res => {
const img = new Image();
img.onload = () =>
res({ width: img.width, height: img.height });
img.onerror = () =>
res(null);
img.src = url;
});
const getAttachments = async (channelId: string) =>
await Promise.all(
UploadStore.getUploads(channelId, DraftType.ChannelMessage)
.map(async (upload: any) => {
const { isImage, filename, spoiler, item: { file } } = upload;
const url = URL.createObjectURL(file);
const attachment: MessageAttachment = {
id: generateId(),
filename: spoiler ? "SPOILER_" + filename : filename,
// weird eh? if i give it the normal content type the preview doenst work
content_type: undefined,
size: await upload.getSize(),
spoiler,
// discord adds query params to the url, so we need to add a hash to prevent that
url: url + "#",
proxy_url: url + "#",
};
if (isImage) {
const box = await getImageBox(url);
if (!box) return attachment;
attachment.width = box.width;
attachment.height = box.height;
}
return attachment;
})
);
export function PreviewButton(chatBoxProps: Props) {
const { isEmpty, attachments } = chatBoxProps.type;
const channelId = SelectedChannelStore.getChannelId();
const draft = useStateFromStores([DraftStore], () => getDraft(channelId));
if (chatBoxProps.type.analyticsName !== "normal") return null;
const hasAttachments = attachments && UploadStore.getUploads(channelId, DraftType.ChannelMessage).length > 0;
const hasContent = !isEmpty && draft?.length > 0;
if (!hasContent && !hasAttachments) return null;
return (
<Tooltip text="Preview Message">
{tooltipProps => (
<Button
{...tooltipProps}
onClick={() =>
onClick={async () =>
sendBotMessage(
channelId,
{
content: getDraft(channelId),
author: UserStore.getCurrentUser()
author: UserStore.getCurrentUser(),
attachments: hasAttachments ? await getAttachments(channelId) : undefined,
}
)}
size=""
@ -67,7 +125,7 @@ export function PreviewButton(chatBoxProps: Props) {
export default definePlugin({
name: "PreviewMessage",
description: "Lets you preview your message before sending it",
description: "Lets you preview your message before sending it.",
authors: [Devs.Aria],
patches: [
{

View File

@ -21,14 +21,11 @@ import { VENCORD_USER_AGENT } from "@utils/constants";
import { debounce } from "@utils/debounce";
import { getCurrentChannel } from "@utils/discord";
import { useAwaiter } from "@utils/react";
import { findStoreLazy } from "@webpack";
import { UserStore } from "@webpack/common";
import { UserProfileStore, UserStore } from "@webpack/common";
import { settings } from "./settings";
import { PronounCode, PronounMapping, PronounsResponse } from "./types";
const UserProfileStore = findStoreLazy("UserProfileStore");
type PronounsWithSource = [string | null, string];
const EmptyPronouns: PronounsWithSource = [null, ""];

View File

@ -28,8 +28,8 @@ export default function ReviewBadge(badge: Badge) {
{({ onMouseEnter, onMouseLeave }) => (
<img
className={cl("badge")}
width="24px"
height="24px"
width="22px"
height="22px"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
src={badge.icon}

View File

@ -20,7 +20,7 @@ import { openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import { filters, findBulk } from "@webpack";
import { Alerts, moment, Timestamp, UserStore } from "@webpack/common";
import { Alerts, moment, Parser, Timestamp, UserStore } from "@webpack/common";
import { Review, ReviewType } from "../entities";
import { deleteReview, reportReview } from "../reviewDbApi";
@ -30,12 +30,12 @@ import { DeleteButton, ReportButton } from "./MessageButton";
import ReviewBadge from "./ReviewBadge";
export default LazyComponent(() => {
// this is terrible, blame mantika
// this is terrible, blame ven
const p = filters.byProps;
const [
{ cozyMessage, buttons, message, buttonsInner, groupStart },
{ container, isHeader },
{ avatar, clickable, username, messageContent, wrapper, cozy },
{ avatar, clickable, username, wrapper, cozy },
buttonClasses,
botTag
] = findBulk(
@ -124,12 +124,10 @@ export default LazyComponent(() => {
</Timestamp>)
}
<p
className={classes(messageContent)}
style={{ fontSize: 15, marginTop: 4, color: "var(--text-normal)" }}
>
{review.comment}
</p>
<div className={cl("review-comment")}>
{Parser.parseGuildEventDescription(review.comment)}
</div>
{review.id !== 0 && (
<div className={classes(container, isHeader, buttons)} style={{
padding: "0px",

View File

@ -16,11 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classes } from "@utils/misc";
import { useAwaiter, useForceUpdater } from "@utils/react";
import { findByPropsLazy } from "@webpack";
import { Forms, React, RelationshipStore, UserStore } from "@webpack/common";
import type { KeyboardEvent } from "react";
import { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react";
import { find, findByPropsLazy } from "@webpack";
import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common";
import { Review } from "../entities";
import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
@ -28,7 +26,12 @@ import { settings } from "../settings";
import { authorize, cl, showToast } from "../utils";
import ReviewComponent from "./ReviewComponent";
const Classes = findByPropsLazy("inputDefault", "editable");
const Editor = findByPropsLazy("start", "end", "addMark");
const Transform = findByPropsLazy("unwrapNodes");
const InputTypes = findByPropsLazy("VOICE_CHANNEL_STATUS", "SIDEBAR");
const InputComponent = LazyComponent(() => find(m => m?.type?.render?.toString().includes("CHANNEL_TEXT_AREA).AnalyticsLocationProvider")));
interface UserProps {
discordId: string;
@ -113,48 +116,82 @@ function ReviewList({ refetch, reviews, hideOwnReview }: { refetch(): void; revi
);
}
export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) {
const { token } = settings.store;
const editorRef = useRef<any>(null);
const inputType = InputTypes.FORM;
inputType.disableAutoFocus = true;
function onKeyPress({ key, target }: KeyboardEvent<HTMLTextAreaElement>) {
if (key === "Enter") {
addReview({
userid: discordId,
comment: (target as HTMLInputElement).value,
star: -1
}).then(res => {
if (res?.success) {
(target as HTMLInputElement).value = ""; // clear the input
refetch();
} else if (res?.message) {
showToast(res.message);
}
});
}
}
const channel = {
flags_: 256,
guild_id_: null,
id: "0",
getGuildId: () => null,
isPrivate: () => true,
isActiveThread: () => false,
isArchivedLockedThread: () => false,
isDM: () => true,
roles: { "0": { permissions: 0n } },
getRecipientId: () => "0",
hasFlag: () => false,
};
return (
<textarea
className={classes(Classes.inputDefault, "enter-comment", cl("input"))}
onKeyDownCapture={e => {
if (e.key === "Enter") {
e.preventDefault(); // prevent newlines
}
}}
placeholder={
!token
? "You need to authorize to review users!"
: isAuthor
? `Update review for @${name}`
: `Review @${name}`
}
onKeyDown={onKeyPress}
onClick={() => {
<>
<div onClick={() => {
if (!token) {
showToast("Opening authorization window...");
authorize();
}
}}
/>
}}>
<InputComponent
className={cl("input")}
channel={channel}
placeholder={
!token
? "You need to authorize to review users!"
: isAuthor
? `Update review for @${name}`
: `Review @${name}`
}
type={inputType}
disableThemedBackground={true}
setEditorRef={ref => editorRef.current = ref}
textValue=""
onSubmit={
async res => {
const response = await addReview({
userid: discordId,
comment: res.value,
});
if (response?.success) {
refetch();
const slateEditor = editorRef.current.ref.current.getSlateEditor();
// clear editor
Transform.delete(slateEditor, {
at: {
anchor: Editor.start(slateEditor, []),
focus: Editor.end(slateEditor, []),
}
});
} else if (response?.message) {
showToast(response.message);
}
// even tho we need to return this, it doesnt do anything
return {
shouldClear: false,
shouldRefocus: true,
};
}
}
/>
</div>
</>
);
}

View File

@ -29,6 +29,13 @@ export const enum ReviewType {
System = 3
}
export const enum NotificationType {
Info = 0,
Ban = 1,
Unban = 2,
Warning = 3
}
export interface Badge {
name: string;
description: string;
@ -45,6 +52,13 @@ export interface BanInfo {
banEndDate: number;
}
export interface Notification {
id: number;
title: string;
content: string;
type: NotificationType;
}
export interface ReviewDBUser {
ID: number;
discordID: string;
@ -54,6 +68,7 @@ export interface ReviewDBUser {
warningCount: number;
badges: any[];
banInfo: BanInfo | null;
notification: Notification | null;
lastReviewID: number;
type: UserType;
}

View File

@ -24,13 +24,13 @@ import ExpandableHeader from "@components/ExpandableHeader";
import { OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Alerts, Menu, useState } from "@webpack/common";
import { Alerts, Menu, Parser, useState } from "@webpack/common";
import { Guild, User } from "discord-types/general";
import { openReviewsModal } from "./components/ReviewModal";
import ReviewsView from "./components/ReviewsView";
import { UserType } from "./entities";
import { getCurrentUserInfo } from "./reviewDbApi";
import { NotificationType } from "./entities";
import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings";
import { showToast } from "./utils";
@ -78,40 +78,33 @@ export default definePlugin({
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
if (user.banInfo) {
const endDate = new Date(user.banInfo.banEndDate);
if (endDate.getTime() > Date.now() && (s.user?.banInfo?.banEndDate ?? 0) < endDate.getTime()) {
Alerts.show({
title: "You have been banned from ReviewDB",
body: (
<>
<p>
You are banned from ReviewDB {
user.type === UserType.Banned
? "permanently"
: "until " + endDate.toLocaleString()
}
</p>
{user.banInfo.reviewContent && (
<p>Offending Review: {user.banInfo.reviewContent}</p>
)}
<p>Continued offenses will result in a permanent ban.</p>
</>
),
cancelText: "Appeal",
confirmText: "Ok",
onCancel: () =>
VencordNative.native.openExternal(
"https://reviewdb.mantikafasi.dev/api/redirect?"
+ new URLSearchParams({
token: settings.store.token!,
page: "dashboard/appeal"
})
)
});
}
}
if (user.notification) {
const props = user.notification.type === NotificationType.Ban ? {
cancelText: "Appeal",
confirmText: "Ok",
onCancel: () =>
VencordNative.native.openExternal(
"https://reviewdb.mantikafasi.dev/api/redirect?"
+ new URLSearchParams({
token: settings.store.token!,
page: "dashboard/appeal"
})
)
} : {};
Alerts.show({
title: user.notification.title,
body: (
Parser.parse(
user.notification.content,
false
)
),
...props
});
readNotification(user.notification.id);
}
s.user = user;
}, 4000);
},

View File

@ -140,3 +140,12 @@ export function getCurrentUserInfo(token: string): Promise<ReviewDBUser> {
method: "POST",
}).then(r => r.json());
}
export function readNotification(id: number) {
return fetch(API_URL + `/api/reviewdb/notifications?id=${id}`, {
method: "PATCH",
headers: {
"Authorization": settings.store.token || "",
},
});
}

View File

@ -14,7 +14,16 @@
overflow: hidden;
background: transparent;
border: 1px solid var(--profile-message-input-border-color);
font-size: 14px;
}
.vc-rdb-modal-footer > div {
width: 100%;
margin: 6px 16px;
}
/* When input becomes disabled(while sending review), input adds unneccesary padding to left, this prevents it */
.vc-rdb-input > div > div {
padding-left: 0 !important;
}
.vc-rdb-placeholder {
@ -24,13 +33,12 @@
color: var(--text-muted);
}
.vc-rdb-modal-footer {
padding: 0;
.vc-rdb-input * {
font-size: 14px;
}
.vc-rdb-modal-footer > div {
width: 100%;
margin: 6px 16px;
.vc-rdb-modal-footer {
padding: 0;
}
.vc-rdb-modal-footer .vc-rdb-input {
@ -49,3 +57,20 @@
.vc-rdb-modal-reviews {
margin-top: 16px;
}
.vc-rdb-review {
margin-top: 8px;
margin-bottom: 8px;
}
.vc-rdb-review-comment img {
vertical-align: text-top;
}
.vc-rdb-review-comment {
overflow-y: hidden;
margin-top: 1px;
margin-bottom: 8px;
color: var(--text-normal);
font-size: 15px;
}

View File

@ -145,7 +145,7 @@ export default definePlugin({
find: "window.webkitAudioContext",
replacement: {
match: /(?<=\i\.acquire=function\((\i)\)\{return )navigator\.mediaDevices\.getUserMedia\(\1\)(?=\})/,
replace: m => `${m}.then(stream => $self.connectRnnoise(stream))`
replace: "$&.then(stream => $self.connectRnnoise(stream, $1.audio))"
},
},
{
@ -182,7 +182,8 @@ export default definePlugin({
setEnabled,
isEnabled: () => settings.store.isEnabled,
async connectRnnoise(stream: MediaStream): Promise<MediaStream> {
async connectRnnoise(stream: MediaStream, isAudio: boolean): Promise<MediaStream> {
if (!isAudio) return stream;
if (!settings.store.isEnabled) return stream;
const audioCtx = new AudioContext();

View File

@ -0,0 +1,5 @@
# SearchReply
Adds a reply button to search results.
![Screenshot](https://i.imgur.com/SjIEHpw.png)

View File

@ -0,0 +1,247 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { classNameFactory } from "@api/Styles";
import { openImageModal, openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
import { LazyComponent, useAwaiter } from "@utils/react";
import { findByCode, findByPropsLazy } from "@webpack";
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, moment, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
import { Guild, User } from "discord-types/general";
const IconUtils = findByPropsLazy("getGuildBannerURL");
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
const UserRow = LazyComponent(() => findByCode(".listDiscriminator"));
const cl = classNameFactory("vc-gp-");
export function openGuildProfileModal(guild: Guild) {
openModal(props =>
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<GuildProfileModal guild={guild} />
</ModalRoot>
);
}
const enum Tabs {
ServerInfo,
Friends,
BlockedUsers
}
interface GuildProps {
guild: Guild;
}
interface RelationshipProps extends GuildProps {
setCount(count: number): void;
}
const fetched = {
friends: false,
blocked: false
};
function renderTimestamp(timestamp: number) {
return (
<Timestamp timestamp={moment(timestamp)} />
);
}
function GuildProfileModal({ guild }: GuildProps) {
const [friendCount, setFriendCount] = useState<number>();
const [blockedCount, setBlockedCount] = useState<number>();
useEffect(() => {
fetched.friends = false;
fetched.blocked = false;
}, []);
const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);
const bannerUrl = guild.banner && IconUtils.getGuildBannerURL({
id: guild.id,
banner: guild.banner
}, true).replace(/\?size=\d+$/, "?size=1024");
const iconUrl = guild.icon && IconUtils.getGuildIconURL({
id: guild.id,
icon: guild.icon,
canAnimate: true,
size: 512
});
return (
<div className={cl("root")}>
{bannerUrl && currentTab === Tabs.ServerInfo && (
<img
className={cl("banner")}
src={bannerUrl}
alt=""
onClick={() => openImageModal(bannerUrl)}
/>
)}
<div className={cl("header")}>
{guild.icon
? <img
src={iconUrl}
alt=""
onClick={() => openImageModal(iconUrl)}
/>
: <div aria-hidden className={classes(IconClasses.childWrapper, IconClasses.acronym)}>{guild.acronym}</div>
}
<div className={cl("name-and-description")}>
<Forms.FormTitle tag="h5" className={cl("name")}>{guild.name}</Forms.FormTitle>
{guild.description && <Forms.FormText>{guild.description}</Forms.FormText>}
</div>
</div>
<TabBar
type="top"
look="brand"
className={cl("tab-bar")}
selectedItem={currentTab}
onItemSelect={setCurrentTab}
>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.ServerInfo })}
id={Tabs.ServerInfo}
>
Server Info
</TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.Friends })}
id={Tabs.Friends}
>
Friends{friendCount !== undefined ? ` (${friendCount})` : ""}
</TabBar.Item>
<TabBar.Item
className={cl("tab", { selected: currentTab === Tabs.BlockedUsers })}
id={Tabs.BlockedUsers}
>
Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""}
</TabBar.Item>
</TabBar>
<div className={cl("tab-content")}>
{currentTab === Tabs.ServerInfo && <ServerInfoTab guild={guild} />}
{currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />}
{currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />}
</div>
</div>
);
}
function Owner(guildId: string, owner: User) {
const guildAvatar = GuildMemberStore.getMember(guildId, owner.id)?.avatar;
const ownerAvatarUrl =
guildAvatar
? IconUtils.getGuildMemberAvatarURLSimple({
userId: owner!.id,
avatar: guildAvatar,
guildId,
canAnimate: true
}, true)
: IconUtils.getUserAvatarURL(owner, true);
return (
<div className={cl("owner")}>
<img src={ownerAvatarUrl} alt="" onClick={() => openImageModal(ownerAvatarUrl)} />
{Parser.parse(`<@${owner.id}>`)}
</div>
);
}
function ServerInfoTab({ guild }: GuildProps) {
const [owner] = useAwaiter(() => UserUtils.fetchUser(guild.ownerId), {
deps: [guild.ownerId],
fallbackValue: null
});
const Fields = {
"Server Owner": owner ? Owner(guild.id, owner) : "Loading...",
"Created At": renderTimestamp(SnowflakeUtils.extractTimestamp(guild.id)),
"Joined At": renderTimestamp(guild.joinedAt.getTime()),
"Vanity Link": guild.vanityURLCode ? (<a>{`discord.gg/${guild.vanityURLCode}`}</a>) : "-", // Making the anchor href valid would cause Discord to reload
"Preferred Locale": guild.preferredLocale || "-",
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category
"Roles": Object.keys(guild.roles).length - 1, // - @everyone
};
return (
<div className={cl("info")}>
{Object.entries(Fields).map(([name, node]) =>
<div className={cl("server-info-pair")} key={name}>
<Forms.FormTitle tag="h5">{name}</Forms.FormTitle>
{typeof node === "string" ? <span>{node}</span> : node}
</div>
)}
</div>
);
}
function FriendsTab({ guild, setCount }: RelationshipProps) {
return UserList("friends", guild, RelationshipStore.getFriendIDs(), setCount);
}
function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
const blockedIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isBlocked(id));
return UserList("blocked", guild, blockedIds, setCount);
}
function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setCount: (count: number) => void) {
const missing = [] as string[];
const members = [] as string[];
for (const id of ids) {
if (GuildMemberStore.isMember(guild.id, id))
members.push(id);
else
missing.push(id);
}
// Used for side effects (rerender on member request success)
useStateFromStores(
[GuildMemberStore],
() => GuildMemberStore.getMemberIds(guild.id),
null,
(old, curr) => old.length === curr.length
);
useEffect(() => {
if (!fetched[type] && missing.length) {
fetched[type] = true;
FluxDispatcher.dispatch({
type: "GUILD_MEMBERS_REQUEST",
guildIds: [guild.id],
userIds: missing
});
}
}, []);
useEffect(() => setCount(members.length), [members.length]);
return (
<ScrollerThin fade className={cl("scroller")}>
{members.map(id =>
<UserRow
user={UserStore.getUser(id)}
status={PresenceStore.getStatus(id) || "offline"}
onSelect={() => openUserProfile(id)}
onContextMenu={() => { }}
/>
)}
</ScrollerThin>
);
}

View File

@ -0,0 +1,7 @@
# ServerProfile
Allows you to view info about servers and see friends and blocked users
![image](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580)
![image](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630)
![image](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54)

View File

@ -0,0 +1,40 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
import { Guild } from "discord-types/general";
import { openGuildProfileModal } from "./GuildProfileModal";
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => () => {
const group = findGroupChildrenByChildId("privacy", children);
group?.push(
<Menu.MenuItem
id="vc-server-profile"
label="Server Profile"
action={() => openGuildProfileModal(guild)}
/>
);
};
export default definePlugin({
name: "ServerProfile",
description: "Allows you to view info about a server by right clicking it in the server list",
authors: [Devs.Ven, Devs.Nuckyz],
tags: ["guild", "info"],
start() {
addContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
},
stop() {
removeContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
}
});

View File

@ -0,0 +1,97 @@
.vc-gp-root {
height: 100%;
user-select: text;
}
.vc-gp-banner {
width: 100%;
cursor: pointer;
}
.vc-gp-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5em;
margin: 0.5em;
}
.vc-gp-header img {
width: 48px;
height: 48px;
cursor: pointer;
}
.vc-gp-name-and-description {
display: flex;
flex-direction: column;
gap: 0.2em;
}
.vc-gp-name {
margin: 0;
}
.vc-gp-tab-bar {
border-bottom: 2px solid var(--background-modifier-accent);
margin: 20px 12px 0;
display: flex;
gap: 40px;
align-items: stretch;
flex-direction: row;
}
.vc-gp-tab {
border-bottom: 2px solid transparent;
color: var(--interactive-normal);
cursor: pointer;
height: 39px;
line-height: 14px;
}
.vc-gp-tab-content {
margin: 1em;
}
.vc-gp-tab:where(.vc-gp-selected, :hover, :focus) {
border-bottom-color: var(--interactive-active);
}
.vc-gp-info {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1em;
}
.vc-gp-server-info-pair {
color: var(--text-normal);
}
.vc-gp-server-info-pair [class^="timestamp"] {
margin-left: 0;
}
.vc-gp-owner {
display: flex;
align-items: center;
gap: 0.2em;
}
.vc-gp-owner img {
height: 20px;
border-radius: 50%;
cursor: pointer;
}
.vc-gp-scroller {
width: 100%;
max-height: 500px;
}
.vc-gp-scroller [class^="listRow"] {
margin: 1px 0;
}
.vc-gp-scroller [class^="listRow"]:hover {
background-color: var(--background-modifier-hover);
}

View File

@ -27,13 +27,12 @@ import { copyWithToast } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { findByCode, findByCodeLazy, findByPropsLazy, findStoreLazy } from "@webpack";
import { Text, Tooltip } from "@webpack/common";
import { Text, Tooltip, UserProfileStore } from "@webpack/common";
import { User } from "discord-types/general";
import { VerifiedIcon } from "./VerifiedIcon";
const Section = LazyComponent(() => findByCode("().lastSection"));
const UserProfileStore = findStoreLazy("UserProfileStore");
const ThemeStore = findStoreLazy("ThemeStore");
const platforms: { get(type: string): ConnectionPlatform; } = findByPropsLazy("isSupported", "getByUrl");
const getTheme: (user: User, displayProfile: any) => any = findByCodeLazy(',"--profile-gradient-primary-color"');

View File

@ -0,0 +1,35 @@
/*
* 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";
export default definePlugin({
name: "ShowTimeouts",
description: "Display member timeout icons in chat regardless of permissions.",
authors: [Devs.Dolfies],
patches: [
{
find: "showCommunicationDisabledStyles",
replacement: {
match: /&&\i\.\i\.canManageUser\(\i\.\i\.MODERATE_MEMBERS,\i\.author,\i\)/,
replace: "",
},
},
],
});

View File

@ -213,15 +213,12 @@ function applyRules(content: string): string {
if (content.length === 0)
return content;
// pad so that rules can use " word " to only match whole "word"
content = " " + content + " ";
if (stringRules) {
for (const rule of stringRules) {
if (!rule.find || !rule.replace) continue;
if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue;
content = content.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n"));
content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, "");
}
}

View File

@ -80,6 +80,9 @@ export default definePlugin({
}
],
data,
settingsAboutComponent: () => {
return (
<Link href="https://github.com/AutumnVN/usrbg#how-to-request-your-own-usrbg-banner">CLICK HERE TO GET YOUR OWN BANNER</Link>
@ -116,7 +119,9 @@ export default definePlugin({
enableStyle(style);
const res = await fetch(BASE_URL);
if (res.ok)
if (res.ok) {
data = await res.json();
this.data = data;
}
}
});

View File

@ -16,8 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { addButton, removeButton } from "@api/MessagePopover";
import { definePluginSettings } from "@api/Settings";
import { CodeBlock } from "@components/CodeBlock";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
@ -25,12 +27,12 @@ import { Margins } from "@utils/margins";
import { copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, Parser, Text } from "@webpack/common";
import { Button, ChannelStore, Forms, Menu, Text } from "@webpack/common";
import { Message } from "discord-types/general";
const CopyIcon = () => {
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="22" height="22">
return <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" width="18" height="18">
<path d="M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z" />
<path d="M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z" />
</svg>;
@ -58,19 +60,7 @@ function cleanMessage(msg: Message) {
return clone;
}
function CodeBlock(props: { content: string, lang: string; }) {
return (
// make text selectable
<div style={{ userSelect: "text" }}>
{Parser.defaultRules.codeBlock.react(props, null, {})}
</div>
);
}
function openViewRawModal(msg: Message) {
msg = cleanMessage(msg);
const msgJson = JSON.stringify(msg, null, 4);
function openViewRawModal(json: string, type: string, msgContent?: string) {
const key = openModal(props => (
<ErrorBoundary>
<ModalRoot {...props} size={ModalSize.LARGE}>
@ -80,26 +70,28 @@ function openViewRawModal(msg: Message) {
</ModalHeader>
<ModalContent>
<div style={{ padding: "16px 0" }}>
{!!msg.content && (
{!!msgContent && (
<>
<Forms.FormTitle tag="h5">Content</Forms.FormTitle>
<CodeBlock content={msg.content} lang="" />
<CodeBlock content={msgContent} lang="" />
<Forms.FormDivider className={Margins.bottom20} />
</>
)}
<Forms.FormTitle tag="h5">Message Data</Forms.FormTitle>
<CodeBlock content={msgJson} lang="json" />
<Forms.FormTitle tag="h5">{type} Data</Forms.FormTitle>
<CodeBlock content={json} lang="json" />
</div>
</ModalContent >
<ModalFooter>
<Flex cellSpacing={10}>
<Button onClick={() => copyWithToast(msgJson, "Message data copied to clipboard!")}>
Copy Message JSON
</Button>
<Button onClick={() => copyWithToast(msg.content, "Content copied to clipboard!")}>
Copy Raw Content
<Button onClick={() => copyWithToast(json, `${type} data copied to clipboard!`)}>
Copy {type} JSON
</Button>
{!!msgContent && (
<Button onClick={() => copyWithToast(msgContent, "Content copied to clipboard!")}>
Copy Raw Content
</Button>
)}
</Flex>
</ModalFooter>
</ModalRoot >
@ -107,6 +99,13 @@ function openViewRawModal(msg: Message) {
));
}
function openViewRawModalMessage(msg: Message) {
msg = cleanMessage(msg);
const msgJson = JSON.stringify(msg, null, 4);
return openViewRawModal(msgJson, "Message", msg.content);
}
const settings = definePluginSettings({
clickMethod: {
description: "Change the button to view the raw content/data of any message.",
@ -118,10 +117,34 @@ const settings = definePluginSettings({
}
});
function MakeContextCallback(name: string) {
const callback: NavContextMenuPatchCallback = (children, props) => () => {
if (name === "Guild" && !props.guild) return;
const lastChild = children.at(-1);
if (lastChild?.key === "developer-actions") {
const p = lastChild.props;
if (!Array.isArray(p.children))
p.children = [p.children];
({ children } = p);
}
children.splice(-1, 0,
<Menu.MenuItem
id={`vc-view-${name.toLowerCase()}-raw`}
label="View Raw"
action={() => openViewRawModal(JSON.stringify(props[name.toLowerCase()], null, 4), name)}
icon={CopyIcon}
/>
);
};
return callback;
}
export default definePlugin({
name: "ViewRaw",
description: "Copy and view the raw content/data of any message.",
authors: [Devs.KingFish, Devs.Ven, Devs.rad],
description: "Copy and view the raw content/data of any message, channel or guild",
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],
dependencies: ["MessagePopoverAPI"],
settings,
@ -131,7 +154,7 @@ export default definePlugin({
if (settings.store.clickMethod === "Right") {
copyWithToast(msg.content);
} else {
openViewRawModal(msg);
openViewRawModalMessage(msg);
}
};
@ -143,7 +166,7 @@ export default definePlugin({
} else {
e.preventDefault();
e.stopPropagation();
openViewRawModal(msg);
openViewRawModalMessage(msg);
}
};
@ -160,9 +183,16 @@ export default definePlugin({
onContextMenu: handleContextMenu
};
});
addContextMenuPatch("guild-context", MakeContextCallback("Guild"));
addContextMenuPatch("channel-context", MakeContextCallback("Channel"));
addContextMenuPatch("user-context", MakeContextCallback("User"));
},
stop() {
removeButton("CopyRawMessage");
removeContextMenuPatch("guild-context", MakeContextCallback("Guild"));
removeContextMenuPatch("channel-context", MakeContextCallback("Channel"));
removeContextMenuPatch("user-context", MakeContextCallback("User"));
}
});

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