Compare commits

...

35 Commits

Author SHA1 Message Date
Vendicated
5d7ede34d8 bump to v1.5.7 2023-10-19 01:23:59 +02:00
Vendicated
cd61354998 ContributorModal: Fix vertical overflow on multi word names 2023-10-19 00:22:49 +02:00
AutumnVN
a452945ac8 feat(plugin): NoTypingAnimation (#1680) 2023-10-19 00:14:14 +02:00
Marocco2
b577660800 feat(VcNarrator): add {{NICKNAME}} as placeholder (#1792) 2023-10-19 00:05:47 +02:00
AutumnVN
4f57c7eded betterNotes, userVoiceShow: fix padding issue (#1804) 2023-10-18 23:54:35 +02:00
Ryan Cao
e3e5da10a9 fix: Content Security Policy patching (#1814)
Co-authored-by: Vendicated <vendicated@riseup.net>
2023-10-18 23:44:29 +02:00
Vendicated
998ce72f3b ForceOwnerCrown: Remove log spam 2023-10-14 02:46:57 +02:00
AutumnVN
188d12d1a3 roleColorEverywhere: role group color in thread/forum (#1778) 2023-10-13 04:26:18 +02:00
AutumnVN
a522eab40d feat(plugin): NoMosaic (#1791) 2023-10-13 04:10:36 +02:00
AutumnVN
c2721f158f imageZoom: fix again (#1793)
Co-authored-by: V <vendicated@riseup.net>
2023-10-13 04:07:21 +02:00
Vendicated
61cd7b4d99 arrpc: refactor for use in vesktop 2023-10-13 03:49:58 +02:00
Marocco2
926af0d1cd feat(VcNarrator): add {{DISPLAY_NAME}} as placeholder (#1642)
Co-authored-by: V <vendicated@riseup.net>
2023-10-12 04:05:46 +02:00
sunnie
dcaf4aec97 fix moreUserTags (#1780) 2023-10-12 03:30:32 +02:00
Nuckyz
5a97adb435 Fix broken IgnoreActivities patch 2023-10-10 05:10:46 -03:00
V
925d709335 bump to v1.5.6 2023-10-09 03:57:44 +02:00
V
1a36dbbc9b ci: cleanup publish workflow 2023-10-09 03:56:41 +02:00
V
b59db2f8c2 Drop Firefox extension support
Despite me already fixing all issues, mozilla is still giving me more
trouble. Now they are asking me to provide them with testing credentials
for discord. Not only do i not want to give them my account, it also
isn't even possible because of how discord's login from new location
verification works

i am very tired of having to fight mozilla and their stupid guidelines /
requests. publishing to amo is a nightmare. as such, official support
for the extension is hereby dropped

we cannot even distribute the extension ourselves because extensions
NEED TO BE SIGNED to install them (unless you use firefox nightly).
and guess how you sign? VIA THEIR STUPID STORE

Options for firefox users:
- use the UserScript
- grab extension-firefox.zip from releases and install it on firefox
  nightly
- make your own firefox developer account and manually sign the
  extension-firefox.zip and pray they sign it for you (they wouldn't
  sign my unlisted upload of it)
- use a chromium browser
2023-10-09 03:49:33 +02:00
V
d81302f64c Revert mozilla store compliance changes
This reverts commit 97b6699afefe373d510dda5589a0754a4b380153.

Vencord is dropping support for the firefox extension, so these changes
are now obsolete. revert so users can still install the extension
manually and enjoy the full experience
2023-10-09 03:15:43 +02:00
V
390987e4a9 Remove ReviewDB
abuse / harassment has gotten pretty bad.
i proposed the ability to allow users to delete comments from their own
profile, but there seems to be no interest among the reviewdb owners.

i don't want vencord to harbour harassment, so i'm removing the plugin
for now
2023-10-09 03:09:56 +02:00
V
377cf60055 fix global settings listeners 2023-10-09 03:01:54 +02:00
Nico
34ac718705 fix(forceOwnerCrown): update broken patch (#1777) 2023-10-07 23:20:36 -03:00
Nuckyz
e4659ed7c3 Rewrite IgnoreActivities (#1693) 2023-10-07 23:04:17 -03:00
Vendicated
c33d59b45d serverProfile: fix crash with lurked guilds
Co-authored-by: aamiaa <9750071+aamiaa@users.noreply.github.com>
2023-10-08 03:49:00 +02:00
Vendicated
ac1b67ccbd Experiments(isStaff): Fix search history not showing 2023-10-07 23:00:44 +02:00
Vendicated
c0f2c97458 ReviewDB: proper multi account support 2023-10-06 19:43:24 +02:00
Vendicated
664dd0a992 ReviewDB: allow deleting reviews on own profile 2023-10-06 18:44:22 +02:00
Vendicated
f66e35b658 fix(SendTimestamps): Do not add to ReviewDB input 2023-10-06 18:05:53 +02:00
Vendicated
df214e1e93 chore: remove legacy code 2023-10-06 18:01:19 +02:00
Vendicated
47a39a062e Fix Vesktop SettingsCog context menu 2023-10-06 17:54:46 +02:00
Vendicated
9e63da6d78 bump to v1.5.5 2023-10-06 04:08:49 +02:00
AutumnVN
79295683ee PictureInPicture: pip button hover styles (#1775)
Co-authored-by: V <vendicated@riseup.net>
2023-10-06 04:07:16 +02:00
Vendicated
5eb9dd04df Fix member list decorations api 2023-10-06 04:00:09 +02:00
Vendicated
03b5dc9c27 BetterRoleDot: Fix ci test false positives 2023-10-06 03:17:44 +02:00
wntiv-main
726a1b5d96 Fix command API (#1776)
Co-authored-by: V <vendicated@riseup.net>
2023-10-06 03:16:21 +02:00
AutumnVN
581fe252a4 fix imageZoom (#1772) 2023-10-03 02:39:34 +02:00
50 changed files with 391 additions and 1546 deletions

View File

@ -36,26 +36,10 @@ jobs:
- name: Publish extension
run: |
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later
EXIT_CODE=0
# Chrome
cd dist/chromium-unpacked
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$?
# Firefox
cd ../firefox-unpacked
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
web-ext-submit || EXIT_CODE=$?
exit $EXIT_CODE
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish
env:
# Chrome
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
# Firefox
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}

View File

@ -22,29 +22,7 @@ The cutest Discord client mod
## Installing / Uninstalling
Click the below button to install Vencord to the Discord Desktop app
[![Download and run the Installer](https://img.shields.io/github/v/release/Vencord/Installer?label=Download%20Vencord%20Installer&style=for-the-badge)](https://github.com/Vencord/Installer#vencord-installer)
## Installing on Browser
[![Get it on the Firefox Webstore](https://blog.mozilla.org/addons/files/2015/11/get-the-addon.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) [![Get it on the Chrome Webstore](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/vencord-web/cbghhgpcnddeihccjmnadmkaejncjndb)
Or use the [UserScript](https://raw.githubusercontent.com/Vencord/builds/main/Vencord.user.js) - Please note that the CSS Editor, Themes loaded from remote sources and co. will not work in the UserScript. Use the extension if you need any of those
<details>
<summary>Alternative Downloads</summary>
## Vencord Desktop
> **Warning**
> This is an alternative app. It currently doesn't support keybinds and possibly some more features. If you just want to install to the normal Discord Desktop app, scroll up
As an alternative to the Discord Desktop app, Vencord also has its own standalone Desktop app that is snappier and lighter than Discord's official Desktop app
[![Download Vencord Desktop](https://img.shields.io/github/v/release/Vencord/Desktop?label=Download%20Vencord%20Desktop&style=for-the-badge)](https://github.com/Vencord/Desktop#vencord-desktop)
</details>
Visit https://vencord.dev/download
## Join our Support/Community Server

32
browser/background.js Normal file
View File

@ -0,0 +1,32 @@
/**
* @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

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

View File

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.5.4",
"version": "1.5.7",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -94,7 +94,7 @@
"build": {
"overwriteDest": true
},
"sourceDir": "./dist/extension-v2-unpacked"
"sourceDir": "./dist/firefox-unpacked"
},
"engines": {
"node": ">=18",

View File

@ -145,11 +145,11 @@ async function loadDir(dir, basePath = "") {
/**
* @type {(target: string, files: string[]) => Promise<void>}
*/
async function buildExtension(target, files, noMonaco = false) {
async function buildExtension(target, files) {
const entries = {
"dist/Vencord.js": await readFile("dist/extension.js"),
"dist/Vencord.css": await readFile("dist/extension.css"),
...(noMonaco ? {} : await loadDir("dist/monaco")),
...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}`)]
))),
@ -195,8 +195,11 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
await Promise.all([
appendCssRuntime,
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
buildExtension("firefox-unpacked", ["content.js", "manifestv2.json", "icon.png"], true),
buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
]);
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension.zip");
console.info("Packed Chromium Extension written to dist/extension.zip");
Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
console.info("Packed Firefox Extension written to dist/extension-firefox.zip");

View File

@ -20,7 +20,6 @@ import { Channel, User } from "discord-types/general/index.js";
interface DecoratorProps {
activities: any[];
canUseAvatarDecorations: boolean;
channel: Channel;
/**
* Only for DM members
@ -52,9 +51,9 @@ export function removeDecorator(identifier: string) {
decorators.delete(identifier);
}
export function __addDecoratorsToList(props: DecoratorProps): (JSX.Element | null)[] {
export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] {
const isInGuild = !!(props.guildId);
return [...decorators.values()].map(decoratorObj => {
return Array.from(decorators.values(), decoratorObj => {
const { decorator, onlyIn } = decoratorObj;
// this can most likely be done cleaner
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) {

View File

@ -237,7 +237,8 @@ type ResolvePropDeep<T, P> = P extends "" ? T :
export function addSettingsListener<Path extends keyof Settings>(path: Path, onUpdate: (newValue: Settings[Path], path: Path) => void): void;
export function addSettingsListener<Path extends string>(path: Path, onUpdate: (newValue: Path extends "" ? any : ResolvePropDeep<Settings, Path>, path: Path extends "" ? string : Path) => void): void;
export function addSettingsListener(path: string, onUpdate: (newValue: any, path: string) => void) {
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
if (path)
((onUpdate as SubscriptionCallback)._paths ??= []).push(path);
subscriptions.add(onUpdate);
}

View File

@ -17,6 +17,7 @@
font-size: 20px;
height: 20px;
position: relative;
text-wrap: nowrap;
}
.vc-author-modal-name::before {

View File

@ -18,11 +18,9 @@
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { DeleteIcon } from "@components/Icons";
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";
@ -251,14 +249,12 @@ function ThemesTab() {
>
Load missing Themes
</Button>
{!IsFirefox && (
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
>
Edit QuickCSS
</Button>
)}
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
>
Edit QuickCSS
</Button>
</>
</Card>
@ -320,15 +316,6 @@ 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,7 +21,6 @@ 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";
@ -110,14 +109,12 @@ function VencordSettings() {
Restart Client
</Button>
)}
{!IsFirefox && (
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}>
Open QuickCSS File
</Button>
)}
<Button
onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}>
Open QuickCSS File
</Button>
{!IS_WEB && (
<Button
onClick={() => showItemInFolder(settingsDir)}

View File

@ -62,6 +62,10 @@ if (IS_VESKTOP || !IS_VANILLA) {
} catch { }
const findHeader = (headers: Record<string, string[]>, headerName: Lowercase<string>) => {
return Object.keys(headers).find(h => h.toLowerCase() === headerName);
};
// Remove CSP
type PolicyResult = Record<string, string[]>;
@ -73,6 +77,7 @@ if (IS_VESKTOP || !IS_VANILLA) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
@ -81,31 +86,39 @@ if (IS_VESKTOP || !IS_VANILLA) {
.map(directive => directive.flat().join(" "))
.join("; ");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const patchCsp = (headers: Record<string, string[]>) => {
const header = findHeader(headers, "content-security-policy");
if (header) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] = ["*", "blob:", "data:", "vencord:", "'unsafe-inline'"];
csp[directive] ??= [];
csp[directive].push("*", "blob:", "data:", "vencord:", "'unsafe-inline'");
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
}
};
session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
patchCsp(responseHeaders);
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["text/css"];
if (resourceType === "stylesheet") {
const header = findHeader(responseHeaders, "content-type");
if (header)
responseHeaders[header] = ["text/css"];
}
}
cb({ cancel: false, responseHeaders });
});

View File

@ -26,7 +26,7 @@ export default definePlugin({
patches: [
// obtain BUILT_IN_COMMANDS instance
{
find: '"giphy","tenor"',
find: ',"tenor"',
replacement: [
{
// Matches BUILT_IN_COMMANDS. This is not exported so this is
@ -34,7 +34,7 @@ export default definePlugin({
// patch simpler
// textCommands = builtInCommands.filter(...)
match: /(?<=\w=)(\w)(\.filter\(.{0,30}giphy)/,
match: /(?<=\w=)(\w)(\.filter\(.{0,60}tenor)/,
replace: "Vencord.Api.Commands._init($1)$2",
}
],

View File

@ -22,21 +22,28 @@ import definePlugin from "@utils/types";
export default definePlugin({
name: "MemberListDecoratorsAPI",
description: "API to add decorators to member list (both in servers and DMs)",
authors: [Devs.TheSun],
authors: [Devs.TheSun, Devs.Ven],
patches: [
{
find: "lostPermissionTooltipText,",
replacement: {
match: /Fragment,{children:\[(.{30,80})\]/,
replace: "Fragment,{children:Vencord.Api.MemberListDecorators.__addDecoratorsToList(this.props).concat($1)"
match: /decorators:.{0,100}?children:\[(?<=(\i)\.lostPermissionTooltipText.+?)/,
replace: "$&...Vencord.Api.MemberListDecorators.__getDecorators($1),"
}
},
{
find: "PrivateChannel.renderAvatar",
replacement: {
match: /(subText:(.{1,2})\.renderSubtitle\(\).{1,50}decorators):(.{30,100}:null)/,
replace: "$1:Vencord.Api.MemberListDecorators.__addDecoratorsToList($2.props).concat($3)"
}
replacement: [
// props are shadowed by nested props so we have to do this
{
match: /\i=(\i)\.applicationStream,/,
replace: "$&vencordProps=$1,"
},
{
match: /decorators:(\i\.isSystemDM\(\))\?(.+?):null/,
replace: "decorators:[...(typeof vencordProps=='undefined'?[]:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)), $1?$2:null]"
}
]
}
],
});

View File

@ -39,8 +39,9 @@ export default definePlugin({
addContextMenuPatch("user-settings-cog", children => () => {
const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any;
section?.forEach(c => {
if (c?.props?.id?.startsWith("Vencord")) {
c.props.action = () => SettingsRouter.open(c.props.id);
const id = c?.props?.id;
if (id?.startsWith("Vencord") || id?.startsWith("Vesktop")) {
c.props.action = () => SettingsRouter.open(id);
}
});
});

View File

@ -17,7 +17,7 @@
*/
import { DataStore } from "@api/index";
import { Devs, IsFirefox, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { isPluginDev } from "@utils/misc";
import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
@ -30,7 +30,6 @@ import plugins from "~plugins";
import settings from "./settings";
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
const FIREFOX_DISMISS_KEY = "Vencord-Firefox-Warning-Dismiss";
const AllowedChannelIds = [
SUPPORT_CHANNEL_ID,
@ -116,22 +115,6 @@ ${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

@ -58,6 +58,26 @@ export default definePlugin({
</>
),
async handleEvent(e: MessageEvent<any>) {
const data = JSON.parse(e.data);
const { activity } = data;
const assets = activity?.assets;
if (assets?.large_image) assets.large_image = await lookupAsset(activity.application_id, assets.large_image);
if (assets?.small_image) assets.small_image = await lookupAsset(activity.application_id, assets.small_image);
if (activity) {
const appId = activity.application_id;
apps[appId] ||= await lookupApp(appId);
const app = apps[appId];
activity.name ||= app.name;
}
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...data });
},
async start() {
// ArmCord comes with its own arRPC implementation, so this plugin just confuses users
if ("armcord" in window) return;
@ -65,22 +85,7 @@ export default definePlugin({
if (ws) ws.close();
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
ws.onmessage = async e => { // on message, set status to data
const data = JSON.parse(e.data);
if (data.activity?.assets?.large_image) data.activity.assets.large_image = await lookupAsset(data.activity.application_id, data.activity.assets.large_image);
if (data.activity?.assets?.small_image) data.activity.assets.small_image = await lookupAsset(data.activity.application_id, data.activity.assets.small_image);
if (data.activity) {
const appId = data.activity.application_id;
apps[appId] ||= await lookupApp(appId);
const app = apps[appId];
data.activity.name ||= app.name;
}
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...data });
};
ws.onmessage = this.handleEvent;
const connectionSuccessful = await new Promise(res => setTimeout(() => res(ws.readyState === WebSocket.OPEN), 1000)); // check if open after 1s
if (!connectionSuccessful) {

View File

@ -19,6 +19,9 @@
import { Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
const UserPopoutSectionCssClasses = findByPropsLazy("section", "lastSection");
export default definePlugin({
name: "BetterNotesBox",
@ -34,12 +37,20 @@ export default definePlugin({
match: /hideNote:.+?(?=[,}])/g,
replace: "hideNote:true",
}
}, {
},
{
find: "Messages.NOTE_PLACEHOLDER",
replacement: {
match: /\.NOTE_PLACEHOLDER,/,
replace: "$&spellCheck:!Vencord.Settings.plugins.BetterNotesBox.noSpellCheck,"
}
},
{
find: ".Messages.NOTE}",
replacement: {
match: /(\i)\.hideNote\?null/,
replace: "$1.hideNote?$self.patchPadding($1)"
}
}
],
@ -56,5 +67,12 @@ export default definePlugin({
disabled: () => Settings.plugins.BetterNotesBox.hide,
default: false
}
},
patchPadding(e: any) {
if (!e.lastSection) return;
return (
<div className={UserPopoutSectionCssClasses.lastSection}></div>
);
}
});

View File

@ -48,6 +48,7 @@ export default definePlugin({
{
find: ".ADD_ROLE_A11Y_LABEL",
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
noWarn: true,
replacement: {
match: /"dot"===\i/,
replace: "true"
@ -56,6 +57,7 @@ export default definePlugin({
{
find: ".roleVerifiedIcon",
predicate: () => Settings.plugins.BetterRoleDot.copyRoleColorInProfilePopout && !Settings.plugins.BetterRoleDot.bothStyles,
noWarn: true,
replacement: {
match: /"dot"===\i/,
replace: "true"

View File

@ -33,12 +33,6 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
},
forceStagingBanner: {
description: "Whether to force Staging banner under user area.",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
}
});
@ -83,12 +77,13 @@ export default definePlugin({
}
]
},
// Fix search history being disabled / broken with isStaff
{
find: ".Messages.DEV_NOTICE_STAGING",
predicate: () => settings.store.forceStagingBanner,
find: 'get("disable_new_search")',
predicate: () => settings.store.enableIsStaff,
replacement: {
match: /"staging"===window\.GLOBAL_ENV\.RELEASE_CHANNEL/,
replace: "true"
match: /(?<=showNewSearch"\);return)\s?!/,
replace: "!1&&!"
}
},
{

View File

@ -222,8 +222,7 @@ export default definePlugin({
predicate: () => settings.store.enableStreamQualityBypass,
replacement: [
"canUseHighVideoUploadQuality",
// TODO: Remove the last two when they get removed from stable
"(?:canStreamQuality|canStreamHighQuality|canStreamMidQuality)",
"canStreamQuality",
].map(func => {
return {
match: new RegExp(`${func}:function\\(\\i(?:,\\i)?\\){`, "g"),

View File

@ -19,6 +19,7 @@
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { GuildStore } from "@webpack/common";
import { Channel, User } from "discord-types/general";
export default definePlugin({
name: "ForceOwnerCrown",
@ -27,32 +28,22 @@ export default definePlugin({
patches: [
{
// This is the logic where it decides whether to render the owner crown or not
find: ".renderOwner=",
find: ".MULTIPLE_AVATAR",
replacement: {
match: /isOwner;return null!=(\w+)?&&/g,
replace: "isOwner;if($self.isGuildOwner(this.props)){$1=true;}return null!=$1&&"
match: /(\i)=(\i)\.isOwner,/,
replace: "$1=$self.isGuildOwner($2),"
}
},
],
isGuildOwner(props) {
// Check if channel is a Group DM, if so return false
if (props?.channel?.type === 3) {
return false;
}
],
isGuildOwner(props: { user: User, channel: Channel, guildId?: string; }) {
if (!props?.user?.id) return false;
if (props.channel?.type === 3 /* GROUP_DM */)
return false;
// guild id is in props twice, fallback if the first is undefined
const guildId = props?.guildId ?? props?.channel?.guild_id;
const userId = props?.user?.id;
const guildId = props.guildId ?? props.channel?.guild_id;
const userId = props.user.id;
if (guildId && userId) {
const guild = GuildStore.getGuild(guildId);
if (guild) {
return guild.ownerId === userId;
}
console.error("[ForceOwnerCrown] failed to get guild", { guildId, guild, props });
} else {
console.error("[ForceOwnerCrown] no guildId or userId", { guildId, userId, props });
}
return false;
return GuildStore.getGuild(guildId)?.ownerId === userId;
},
});

View File

@ -1,27 +1,17 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import * as DataStore from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStore";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { findStoreLazy } from "@webpack";
import { Tooltip } from "webpack/common";
const enum ActivitiesTypes {
@ -31,203 +21,154 @@ const enum ActivitiesTypes {
interface IgnoredActivity {
id: string;
name: string;
type: ActivitiesTypes;
}
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon");
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
const RunningGameStore = findStoreLazy("RunningGameStore");
const ShowCurrentGame = getSettingStoreLazy<boolean>("status", "showCurrentGame");
function ToggleIconOff() {
return (
<svg
className={RegisteredGamesClasses.overlayToggleIconOff}
height="24"
width="24"
viewBox="0 2.2 32 26"
aria-hidden={true}
role="img"
>
<g
fill="none"
fillRule="evenodd"
>
<path
className={RegisteredGamesClasses.fill}
fill="currentColor"
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
/>
<rect
className={RegisteredGamesClasses.fill}
x="3"
y="26"
width="26"
height="2"
transform="rotate(-45 2 20)"
/>
</g>
</svg>
);
}
function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) {
return (
<svg
className={RegisteredGamesClasses.overlayToggleIconOn}
height="24"
width="24"
viewBox="0 2.2 32 26"
>
<path
className={forceWhite ? "" : RegisteredGamesClasses.fill}
fill={forceWhite ? "var(--white-500)" : ""}
d="M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z"
/>
</svg>
);
}
function ToggleActivityComponent({ activity, forceWhite, forceLeftMargin }: { activity: IgnoredActivity; forceWhite?: boolean; forceLeftMargin?: boolean; }) {
function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) {
const forceUpdate = useForceUpdater();
return (
<Tooltip text="Toggle activity">
{({ onMouseLeave, onMouseEnter }) => (
<div
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
className={RegisteredGamesClasses.overlayToggleIcon}
role="button"
aria-label="Toggle activity"
tabIndex={0}
style={forceLeftMargin ? { marginLeft: "2px" } : undefined}
<Tooltip text={tooltipText}>
{tooltipProps => (
<button
{...tooltipProps}
onClick={e => handleActivityToggle(e, activity, forceUpdate)}
style={{ all: "unset", cursor: "pointer", display: "flex", justifyContent: "center", alignItems: "center" }}
>
{
ignoredActivitiesCache.has(activity.id)
? <ToggleIconOff />
: <ToggleIconOn forceWhite={forceWhite} />
}
</div>
<svg
width="24"
height="24"
viewBox="0 -960 960 960"
>
<path fill={fill} d={path} />
</svg>
</button>
)}
</Tooltip>
);
}
function ToggleActivityComponentWithBackground({ activity }: { activity: IgnoredActivity; }) {
return (
<div
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`}
style={{ padding: "0px 2px", height: 28 }}
>
<ToggleActivityComponent activity={activity} forceWhite={true} />
</div>
);
const ToggleIconOn = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Disable Activity", "M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z", fill);
const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Enable Activity", "m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z", fill);
function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
if (getIgnoredActivities().some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)");
}
function handleActivityToggle(e: React.MouseEvent<HTMLDivElement, MouseEvent>, activity: IgnoredActivity, forceUpdateComponent: () => void) {
function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity, forceUpdateButton: () => void) {
e.stopPropagation();
if (ignoredActivitiesCache.has(activity.id)) ignoredActivitiesCache.delete(activity.id);
else ignoredActivitiesCache.set(activity.id, activity);
forceUpdateComponent();
saveCacheToDatastore();
const ignoredActivityIndex = getIgnoredActivities().findIndex(act => act.id === activity.id);
if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity);
else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex);
// Trigger activities recalculation
ShowCurrentGame?.updateSetting(old => old);
forceUpdateButton();
}
async function saveCacheToDatastore() {
await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache);
}
const settings = definePluginSettings({}).withPrivateSettings<{
ignoredActivities: IgnoredActivity[];
}>();
let ignoredActivitiesCache = new Map<IgnoredActivity["id"], IgnoredActivity>();
function getIgnoredActivities() {
return settings.store.ignoredActivities ??= [];
}
export default definePlugin({
name: "IgnoreActivities",
authors: [Devs.Nuckyz],
description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.",
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are ignored from the Registered Games and Activities tabs.",
dependencies: ["SettingsStoreAPI"],
settings,
patches: [
{
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
replacement: {
match: /!(\i)(\)return null;var \i=(\i)\.overlay.+?children:)(\[.{0,70}overlayStatusText.+?\])(?=}\)}\(\))/,
replace: (_, platformCheck, restWithoutPlatformCheck, props, children) => "false"
+ `${restWithoutPlatformCheck}`
+ `(${platformCheck}?${children}:[])`
+ `.concat(Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(${props}))`
}
},
{
find: ".overlayBadge",
find: '.displayName="LocalActivityStore"',
replacement: [
{
match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i)\.name.+?null/,
replace: (m, props) => `[${m},$self.renderToggleActivityButton(${props})]`
},
{
match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i\.application)\.name.+?null/,
replace: (m, props) => `${m},$self.renderToggleActivityButton(${props})`
match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/,
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
}
]
},
{
find: '.displayName="LocalActivityStore"',
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
replacement: {
match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/,
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
match: /\(\)\.removeGame.+?null(?<=(\i)\?\i=\i\.\i\.Messages\.SETTINGS_GAMES_NOW_PLAYING_STATE.+?=(\i)\.overlay.+?)/,
replace: (m, nowPlaying, props) => `${m},$self.renderToggleGameActivityButton(${props},${nowPlaying})`
}
},
{
find: ".Messages.EMBEDDED_ACTIVITIES_HAVE_PLAYED_ONE_KNOWN",
replacement: [
{
match: /(?<=\(\)\.activityTitleText.+?children:(\i)\.name.*?}\),)/,
replace: (_, props) => `$self.renderToggleActivityButton(${props}),`
},
{
match: /(?<=\(\)\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),)/,
replace: (_, props) => `$self.renderToggleActivityButton(${props}),`
}
]
}
],
async start() {
const ignoredActivitiesData = await DataStore.get<string[] | Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities") ?? new Map<IgnoredActivity["id"], IgnoredActivity>();
/** Migrate old data */
if (Array.isArray(ignoredActivitiesData)) {
for (const id of ignoredActivitiesData) {
ignoredActivitiesCache.set(id, { id, type: ActivitiesTypes.Game });
}
const oldIgnoredActivitiesData = await DataStore.get<Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities");
await saveCacheToDatastore();
} else ignoredActivitiesCache = ignoredActivitiesData;
if (oldIgnoredActivitiesData != null) {
settings.store.ignoredActivities = Array.from(oldIgnoredActivitiesData.values())
.map(activity => ({ ...activity, name: "Unknown Name" }));
if (ignoredActivitiesCache.size !== 0) {
const gamesSeen: { id?: string; exePath: string; }[] = RunningGameStore.getGamesSeen();
DataStore.del("IgnoreActivities_ignoredActivities");
}
for (const ignoredActivity of ignoredActivitiesCache.values()) {
if (getIgnoredActivities().length !== 0) {
const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[];
for (const [index, ignoredActivity] of getIgnoredActivities().entries()) {
if (ignoredActivity.type !== ActivitiesTypes.Game) continue;
if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
/** Custom added game which no longer exists */
ignoredActivitiesCache.delete(ignoredActivity.id);
getIgnoredActivities().splice(index, 1);
}
}
await saveCacheToDatastore();
}
},
renderToggleGameActivityButton(props: { id?: string; exePath: string; }) {
return (
<ErrorBoundary noop>
<ToggleActivityComponent activity={{ id: props.id ?? props.exePath, type: ActivitiesTypes.Game }} forceLeftMargin={true} />
</ErrorBoundary>
);
},
renderToggleActivityButton(props: { id: string; }) {
return (
<ErrorBoundary noop>
<ToggleActivityComponentWithBackground activity={{ id: props.id, type: ActivitiesTypes.Embedded }} />
</ErrorBoundary>
);
},
isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) {
if (props.type === 0) {
if (props.application_id !== undefined) return !ignoredActivitiesCache.has(props.application_id);
if (props.type === 0 || props.type === 3) {
if (props.application_id != null) return !getIgnoredActivities().some(activity => activity.id === props.application_id);
else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) return !ignoredActivitiesCache.has(exePath);
if (exePath) return !getIgnoredActivities().some(activity => activity.id === exePath);
}
}
return true;
},
renderToggleGameActivityButton(props: { id?: string; name: string, exePath: string; }, nowPlaying: boolean) {
return (
<ErrorBoundary noop>
<div style={{ marginLeft: 12, zIndex: 0 }}>
{ToggleActivityComponent({ id: props.id ?? props.exePath, name: props.name, type: ActivitiesTypes.Game }, nowPlaying)}
</div>
</ErrorBoundary>
);
},
renderToggleActivityButton(props: { id: string; name: string; }) {
return (
<ErrorBoundary noop>
{ToggleActivityComponent({ id: props.id, name: props.name, type: ActivitiesTypes.Embedded })}
</ErrorBoundary>
);
}
});

View File

@ -165,7 +165,7 @@ export default definePlugin({
{
find: '"renderLinkComponent","maxWidth"',
replacement: {
match: /(return\(.{1,100}\(\)\.wrapper.{1,100})(src)/,
match: /(return\(.{1,100}\(\)\.wrapper.{1,200})(src)/,
replace: `$1id: '${ELEMENT_ID}',$2`
}
},
@ -174,8 +174,8 @@ export default definePlugin({
find: "handleImageLoad=",
replacement: [
{
match: /(render=function\(\){.{1,500}limitResponsiveWidth.{1,600})onMouseEnter:/,
replace: "$1...$self.makeProps(this),onMouseEnter:"
match: /showThumbhashPlaceholder:/,
replace: "...$self.makeProps(this),$&"
},
{
@ -189,7 +189,6 @@ export default definePlugin({
}
]
},
{
find: ".carouselModal,",
replacement: {

View File

@ -53,8 +53,6 @@ interface TagSettings {
[k: string]: TagSetting;
}
const CLYDE_ID = "1081004946872352958";
// PermissionStore.computePermissions is not the same function and doesn't work here
const PermissionUtil = findByPropsLazy("computePermissions", "canEveryoneRole") as {
computePermissions({ ...args }): bigint;
@ -215,7 +213,7 @@ export default definePlugin({
},
// add HTML data attributes (for easier theming)
{
match: /children:\[(?=\i,\(0,\i\.jsx\)\("span",{className:\i\(\)\.botText,children:(\i)}\)\])/,
match: /children:\[(?=\i\?null:\i,\i,\(0,\i\.jsx\)\("span",{className:\i\(\)\.botText,children:(\i)}\)\])/,
replace: "'data-tag':$1.toLowerCase(),children:["
}
],
@ -230,10 +228,10 @@ export default definePlugin({
},
// in the member list
{
find: ".renderBot=function(){",
find: ".Messages.GUILD_OWNER,",
replacement: {
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"
match: /(?<type>\i)=\(null==.{0,50}\.BOT,null!=(?<user>\i)&&\i\.bot/,
replace: "$<type> = $self.getTag({user: $<user>, channel: arguments[0].channel, origType: $<user>.bot ? 0 : null, location: 'not-chat' }), typeof $<type> === 'number'"
}
},
// pass channel id down props to be used in profiles
@ -253,7 +251,7 @@ export default definePlugin({
},
// in profiles
{
find: ",botType:",
find: "showStreamerModeTooltip:",
replacement: {
match: /,botType:(\i\((\i)\)),/g,
replace: ",botType:$self.getTag({user:$2,channelId:arguments[0].moreTags_channelId,origType:$1,location:'not-chat'}),"
@ -341,15 +339,17 @@ export default definePlugin({
message, user, channelId, origType, location, channel
}: {
message?: Message,
user: User,
user: User & { isClyde(): boolean; },
channel?: Channel & { isForumPost(): boolean; },
channelId?: string;
origType?: number;
location: "chat" | "not-chat";
}): number | null {
if (!user)
return null;
if (location === "chat" && user.id === "1")
return Tag.Types.OFFICIAL;
if (user.id === CLYDE_ID)
if (user.isClyde())
return Tag.Types.AI;
let type = typeof origType === "number" ? origType : null;

View File

@ -0,0 +1,41 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import style from "./styles.css?managed";
export default definePlugin({
name: "NoMosaic",
authors: [Devs.AutumnVN],
description: "Removes Discord new image mosaic",
tags: ["image", "mosaic", "media"],
patches: [{
find: "Media Mosaic",
replacement: [
{
match: /mediaLayoutType:\i\.\i\.MOSAIC/,
replace: 'mediaLayoutType:"RESPONSIVE"',
},
{
match: /\i===\i\.\i\.MOSAIC/,
replace: "true",
},
{
match: /null!==\(\i=\i\.get\(\i\)\)&&void 0!==\i\?\i:"INVALID"/,
replace: '"INVALID"',
},
],
}],
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
}
});

View File

@ -0,0 +1,3 @@
[class^="nonMediaAttachmentsContainer-"] [class*="messageAttachment-"] {
position: relative;
}

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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "NoTypingAnimation",
authors: [Devs.AutumnVN],
description: "Disables the CPU-intensive typing dots animation",
patches: [{
find: "dotCycle",
replacement: {
match: /document.hasFocus\(\)/,
replace: "false"
}
}]
});

View File

@ -4,6 +4,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
@ -41,6 +43,7 @@ export default definePlugin({
{tooltipProps => (
<div
{...tooltipProps}
className="vc-pip-button"
role="button"
style={{
cursor: "pointer",
@ -71,7 +74,7 @@ export default definePlugin({
>
<svg width="24px" height="24px" viewBox="0 0 24 24">
<path
fill="var(--interactive-normal)"
fill="currentColor"
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>

View File

@ -0,0 +1,8 @@
.vc-pip-button {
color: var(--interactive-normal);
}
.vc-pip-button:hover {
background-color: var(--background-modifier-hover);
color: var(--interactive-hover);
}

View File

@ -68,8 +68,7 @@ export default definePlugin({
find: ".USER_PROFILE_ACTIVITY",
replacement: [
{
/* FIXME: old name is getGlobalName, new name is getName. Remove optional Global once stable has also migrated */
match: /\.get(?:Global)?Name\(\i\);(?<=displayProfile.{0,200})/,
match: /\.getName\(\i\);(?<=displayProfile.{0,200})/,
replace: "$&const [vcPronounce,vcPronounSource]=$self.useProfilePronouns(arguments[0].user.id,true);if(arguments[0].displayProfile&&vcPronounce)arguments[0].displayProfile.pronouns=vcPronounce;"
},
PRONOUN_TOOLTIP_PATCH

View File

@ -1,61 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { DeleteIcon } from "@components/Icons";
import { classes } from "@utils/misc";
import { findByPropsLazy } from "@webpack";
import { Tooltip } from "@webpack/common";
const iconClasses = findByPropsLazy("button", "wrapper", "disabled", "separator");
export function DeleteButton({ onClick }: { onClick(): void; }) {
return (
<Tooltip text="Delete Review">
{props => (
<div
{...props}
className={classes(iconClasses.button, iconClasses.dangerous)}
onClick={onClick}
>
<DeleteIcon width="20" height="20" />
</div>
)}
</Tooltip>
);
}
export function ReportButton({ onClick }: { onClick(): void; }) {
return (
<Tooltip text="Report Review">
{props => (
<div
{...props}
className={iconClasses.button}
onClick={onClick}
>
<svg width="20" height="20" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M20,6.002H14V3.002C14,2.45 13.553,2.002 13,2.002H4C3.447,2.002 3,2.45 3,3.002V22.002H5V14.002H10.586L8.293,16.295C8.007,16.581 7.922,17.011 8.076,17.385C8.23,17.759 8.596,18.002 9,18.002H20C20.553,18.002 21,17.554 21,17.002V7.002C21,6.45 20.553,6.002 20,6.002Z"
/>
</svg>
</div>
)}
</Tooltip>
);
}

View File

@ -1,46 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { MaskedLinkStore, Tooltip } from "@webpack/common";
import { Badge } from "../entities";
import { cl } from "../utils";
export default function ReviewBadge(badge: Badge) {
return (
<Tooltip
text={badge.name}>
{({ onMouseEnter, onMouseLeave }) => (
<img
className={cl("badge")}
width="22px"
height="22px"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
src={badge.icon}
alt={badge.description}
onClick={() =>
MaskedLinkStore.openUntrustedLink({
href: badge.redirectURL,
})
}
/>
)}
</Tooltip>
);
}

View File

@ -1,147 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { openUserProfile } from "@utils/discord";
import { classes } from "@utils/misc";
import { LazyComponent } from "@utils/react";
import { filters, findBulk } from "@webpack";
import { Alerts, moment, Parser, Timestamp, UserStore } from "@webpack/common";
import { Review, ReviewType } from "../entities";
import { deleteReview, reportReview } from "../reviewDbApi";
import { settings } from "../settings";
import { canDeleteReview, cl, showToast } from "../utils";
import { DeleteButton, ReportButton } from "./MessageButton";
import ReviewBadge from "./ReviewBadge";
export default LazyComponent(() => {
// this is terrible, blame ven
const p = filters.byProps;
const [
{ cozyMessage, buttons, message, buttonsInner, groupStart },
{ container, isHeader },
{ avatar, clickable, username, wrapper, cozy },
buttonClasses,
botTag
] = findBulk(
p("cozyMessage"),
p("container", "isHeader"),
p("avatar", "zalgo"),
p("button", "wrapper", "selected"),
p("botTag", "botTagRegular")
);
const dateFormat = new Intl.DateTimeFormat();
return function ReviewComponent({ review, refetch }: { review: Review; refetch(): void; }) {
function openModal() {
openUserProfile(review.sender.discordID);
}
function delReview() {
Alerts.show({
title: "Are you sure?",
body: "Do you really want to delete this review?",
confirmText: "Delete",
cancelText: "Nevermind",
onConfirm: () => {
deleteReview(review.id).then(res => {
if (res.success) {
refetch();
}
showToast(res.message);
});
}
});
}
function reportRev() {
Alerts.show({
title: "Are you sure?",
body: "Do you really you want to report this review?",
confirmText: "Report",
cancelText: "Nevermind",
// confirmColor: "red", this just adds a class name and breaks the submit button guh
onConfirm: () => reportReview(review.id)
});
}
return (
<div className={classes(cozyMessage, wrapper, message, groupStart, cozy, cl("review"))} style={
{
marginLeft: "0px",
paddingLeft: "52px", // wth is this
paddingRight: "16px"
}
}>
<img
className={classes(avatar, clickable)}
onClick={openModal}
src={review.sender.profilePhoto || "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
style={{ left: "0px", zIndex: 0 }}
/>
<div style={{ display: "inline-flex", justifyContent: "center", alignItems: "center" }}>
<span
className={classes(clickable, username)}
style={{ color: "var(--channels-default)", fontSize: "14px" }}
onClick={() => openModal()}
>
{review.sender.username}
</span>
{review.type === ReviewType.System && (
<span
className={classes(botTag.botTagVerified, botTag.botTagRegular, botTag.botTag, botTag.px, botTag.rem)}
style={{ marginLeft: "4px" }}>
<span className={botTag.botText}>
System
</span>
</span>
)}
</div>
{review.sender.badges.map(badge => <ReviewBadge {...badge} />)}
{
!settings.store.hideTimestamps && review.type !== ReviewType.System && (
<Timestamp timestamp={moment(review.timestamp * 1000)} >
{dateFormat.format(review.timestamp * 1000)}
</Timestamp>)
}
<div className={cl("review-comment")}>
{Parser.parseGuildEventDescription(review.comment)}
</div>
{review.id !== 0 && (
<div className={classes(container, isHeader, buttons)} style={{
padding: "0px",
}}>
<div className={classes(buttonClasses.wrapper, buttonsInner)} >
<ReportButton onClick={reportRev} />
{canDeleteReview(review, UserStore.getCurrentUser().id) && (
<DeleteButton onClick={delReview} />
)}
</div>
</div>
)}
</div>
);
};
});

View File

@ -1,104 +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 ErrorBoundary from "@components/ErrorBoundary";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useForceUpdater } from "@utils/react";
import { Paginator, Text, useRef, useState } from "@webpack/common";
import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi";
import { settings } from "../settings";
import { cl } from "../utils";
import ReviewComponent from "./ReviewComponent";
import ReviewsView, { ReviewsInputComponent } from "./ReviewsView";
function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: string; name: string; }) {
const [data, setData] = useState<Response>();
const [signal, refetch] = useForceUpdater(true);
const [page, setPage] = useState(1);
const ref = useRef<HTMLDivElement>(null);
const reviewCount = data?.reviewCount;
const ownReview = data?.reviews.find(r => r.sender.discordID === settings.store.user?.discordID);
return (
<ErrorBoundary>
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-lg/semibold" className={cl("modal-header")}>
{name}'s Reviews
{!!reviewCount && <span> ({reviewCount} Reviews)</span>}
</Text>
<ModalCloseButton onClick={modalProps.onClose} />
</ModalHeader>
<ModalContent scrollerRef={ref}>
<div className={cl("modal-reviews")}>
<ReviewsView
discordId={discordId}
name={name}
page={page}
refetchSignal={signal}
onFetchReviews={setData}
scrollToTop={() => ref.current?.scrollTo({ top: 0, behavior: "smooth" })}
hideOwnReview
/>
</div>
</ModalContent>
<ModalFooter className={cl("modal-footer")}>
<div>
{ownReview && (
<ReviewComponent
refetch={refetch}
review={ownReview}
/>
)}
<ReviewsInputComponent
isAuthor={ownReview != null}
discordId={discordId}
name={name}
refetch={refetch}
/>
{!!reviewCount && (
<Paginator
currentPage={page}
maxVisiblePages={5}
pageSize={REVIEWS_PER_PAGE}
totalCount={reviewCount}
onPageChange={setPage}
/>
)}
</div>
</ModalFooter>
</ModalRoot>
</ErrorBoundary>
);
}
export function openReviewsModal(discordId: string, name: string) {
openModal(props => (
<Modal
modalProps={props}
discordId={discordId}
name={name}
/>
));
}

View File

@ -1,197 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { 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";
import { settings } from "../settings";
import { authorize, cl, showToast } from "../utils";
import ReviewComponent from "./ReviewComponent";
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;
name: string;
}
interface Props extends UserProps {
onFetchReviews(data: Response): void;
refetchSignal?: unknown;
showInput?: boolean;
page?: number;
scrollToTop?(): void;
hideOwnReview?: boolean;
}
export default function ReviewsView({
discordId,
name,
onFetchReviews,
refetchSignal,
scrollToTop,
page = 1,
showInput = false,
hideOwnReview = false,
}: Props) {
const [signal, refetch] = useForceUpdater(true);
const [reviewData] = useAwaiter(() => getReviews(discordId, (page - 1) * REVIEWS_PER_PAGE), {
fallbackValue: null,
deps: [refetchSignal, signal, page],
onSuccess: data => {
if (settings.store.hideBlockedUsers)
data!.reviews = data!.reviews?.filter(r => !RelationshipStore.isBlocked(r.sender.discordID));
scrollToTop?.();
onFetchReviews(data!);
}
});
if (!reviewData) return null;
return (
<>
<ReviewList
refetch={refetch}
reviews={reviewData!.reviews}
hideOwnReview={hideOwnReview}
/>
{showInput && (
<ReviewsInputComponent
name={name}
discordId={discordId}
refetch={refetch}
isAuthor={reviewData!.reviews?.some(r => r.sender.discordID === UserStore.getCurrentUser().id)}
/>
)}
</>
);
}
function ReviewList({ refetch, reviews, hideOwnReview }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; }) {
const myId = UserStore.getCurrentUser().id;
return (
<div className={cl("view")}>
{reviews?.map(review =>
(review.sender.discordID !== myId || !hideOwnReview) &&
<ReviewComponent
key={review.id}
review={review}
refetch={refetch}
/>
)}
{reviews?.length === 0 && (
<Forms.FormText className={cl("placeholder")}>
Looks like nobody reviewed this user yet. You could be the first!
</Forms.FormText>
)}
</div>
);
}
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;
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 (
<>
<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

@ -1,91 +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/>.
*/
export const enum UserType {
Banned = -1,
Normal = 0,
Admin = 1
}
export const enum ReviewType {
User = 0,
Server = 1,
Support = 2,
System = 3
}
export const enum NotificationType {
Info = 0,
Ban = 1,
Unban = 2,
Warning = 3
}
export interface Badge {
name: string;
description: string;
icon: string;
redirectURL: string;
type: number;
}
export interface BanInfo {
id: string;
discordID: string;
reviewID: number;
reviewContent: string;
banEndDate: number;
}
export interface Notification {
id: number;
title: string;
content: string;
type: NotificationType;
}
export interface ReviewDBUser {
ID: number;
discordID: string;
username: string;
profilePhoto: string;
clientMod: string;
warningCount: number;
badges: any[];
banInfo: BanInfo | null;
notification: Notification | null;
lastReviewID: number;
type: UserType;
}
export interface ReviewAuthor {
id: number,
discordID: string,
username: string,
profilePhoto: string,
badges: Badge[];
}
export interface Review {
comment: string,
id: number,
star: number,
sender: ReviewAuthor,
timestamp: number;
type?: ReviewType;
}

View File

@ -1,140 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./style.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import ErrorBoundary from "@components/ErrorBoundary";
import ExpandableHeader from "@components/ExpandableHeader";
import { OpenExternalIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
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 { NotificationType } from "./entities";
import { getCurrentUserInfo, readNotification } from "./reviewDbApi";
import { settings } from "./settings";
import { showToast } from "./utils";
const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => {
children.push(
<Menu.MenuItem
label="View Reviews"
id="vc-rdb-server-reviews"
icon={OpenExternalIcon}
action={() => openReviewsModal(props.guild.id, props.guild.name)}
/>
);
};
export default definePlugin({
name: "ReviewDB",
description: "Review other users (Adds a new settings to profiles)",
authors: [Devs.mantikafasi, Devs.Ven],
settings,
patches: [
{
find: "disableBorderColor:!0",
replacement: {
match: /\(.{0,10}\{user:(.),setNote:.,canDM:.,.+?\}\)/,
replace: "$&,$self.getReviewsComponent($1)"
}
}
],
async start() {
const s = settings.store;
const { token, lastReviewId, notifyReviews } = s;
if (!notifyReviews || !token) return;
setTimeout(async () => {
const user = await getCurrentUserInfo(token);
if (lastReviewId && lastReviewId < user.lastReviewID) {
s.lastReviewId = user.lastReviewID;
if (user.lastReviewID !== 0)
showToast("You have new reviews on your profile!");
}
addContextMenuPatch("guild-header-popout", guildPopoutPatch);
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);
},
stop() {
removeContextMenuPatch("guild-header-popout", guildPopoutPatch);
},
getReviewsComponent: ErrorBoundary.wrap((user: User) => {
const [reviewCount, setReviewCount] = useState<number>();
return (
<ExpandableHeader
headerText="User Reviews"
onMoreClick={() => openReviewsModal(user.id, user.username)}
moreTooltipText={
reviewCount && reviewCount > 50
? `View all ${reviewCount} reviews`
: "Open Review Modal"
}
onDropDownClick={state => settings.store.reviewsDropdownState = !state}
defaultState={settings.store.reviewsDropdownState}
>
<ReviewsView
discordId={user.id}
name={user.username}
onFetchReviews={r => setReviewCount(r.reviewCount)}
showInput
/>
</ExpandableHeader>
);
}, { message: "Failed to render Reviews" })
});

View File

@ -1,151 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Review, ReviewDBUser } from "./entities";
import { settings } from "./settings";
import { authorize, showToast } from "./utils";
const API_URL = "https://manti.vendicated.dev";
export const REVIEWS_PER_PAGE = 50;
export interface Response {
success: boolean,
message: string;
reviews: Review[];
updated: boolean;
hasNextPage: boolean;
reviewCount: number;
}
const WarningFlag = 0b00000010;
export async function getReviews(id: string, offset = 0): Promise<Response> {
let flags = 0;
if (!settings.store.showWarning) flags |= WarningFlag;
const params = new URLSearchParams({
flags: String(flags),
offset: String(offset)
});
const req = await fetch(`${API_URL}/api/reviewdb/users/${id}/reviews?${params}`);
const res = (req.status === 200)
? await req.json() as Response
: {
success: false,
message: "An Error occured while fetching reviews. Please try again later.",
reviews: [],
updated: false,
hasNextPage: false,
reviewCount: 0
};
if (!res.success) {
showToast(res.message);
return {
...res,
reviews: [
{
id: 0,
comment: "An Error occured while fetching reviews. Please try again later.",
star: 0,
timestamp: 0,
sender: {
id: 0,
username: "Error",
profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128",
discordID: "0",
badges: []
}
}
]
};
}
return res;
}
export async function addReview(review: any): Promise<Response | null> {
review.token = settings.store.token;
if (!review.token) {
showToast("Please authorize to add a review.");
authorize();
return null;
}
return fetch(API_URL + `/api/reviewdb/users/${review.userid}/reviews`, {
method: "PUT",
body: JSON.stringify(review),
headers: {
"Content-Type": "application/json",
}
})
.then(r => r.json())
.then(res => {
showToast(res.message);
return res ?? null;
});
}
export function deleteReview(id: number): Promise<Response> {
return fetch(API_URL + `/api/reviewdb/users/${id}/reviews`, {
method: "DELETE",
headers: new Headers({
"Content-Type": "application/json",
Accept: "application/json",
}),
body: JSON.stringify({
token: settings.store.token,
reviewid: id
})
}).then(r => r.json());
}
export async function reportReview(id: number) {
const res = await fetch(API_URL + "/api/reviewdb/reports", {
method: "PUT",
headers: new Headers({
"Content-Type": "application/json",
Accept: "application/json",
}),
body: JSON.stringify({
reviewid: id,
token: settings.store.token
})
}).then(r => r.json()) as Response;
showToast(res.message);
}
export function getCurrentUserInfo(token: string): Promise<ReviewDBUser> {
return fetch(API_URL + "/api/reviewdb/users", {
body: JSON.stringify({ token }),
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

@ -1,87 +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 { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
import { Button } from "@webpack/common";
import { ReviewDBUser } from "./entities";
import { authorize } from "./utils";
export const settings = definePluginSettings({
authorize: {
type: OptionType.COMPONENT,
description: "Authorize with ReviewDB",
component: () => (
<Button onClick={authorize}>
Authorize with ReviewDB
</Button>
)
},
notifyReviews: {
type: OptionType.BOOLEAN,
description: "Notify about new reviews on startup",
default: true,
},
showWarning: {
type: OptionType.BOOLEAN,
description: "Display warning to be respectful at the top of the reviews list",
default: true,
},
hideTimestamps: {
type: OptionType.BOOLEAN,
description: "Hide timestamps on reviews",
default: false,
},
hideBlockedUsers: {
type: OptionType.BOOLEAN,
description: "Hide reviews from blocked users",
default: true,
},
website: {
type: OptionType.COMPONENT,
description: "ReviewDB website",
component: () => (
<Button onClick={() => {
let url = "https://reviewdb.mantikafasi.dev/";
if (settings.store.token)
url += "/api/redirect?token=" + encodeURIComponent(settings.store.token);
VencordNative.native.openExternal(url);
}}>
ReviewDB website
</Button>
)
},
supportServer: {
type: OptionType.COMPONENT,
description: "ReviewDB Support Server",
component: () => (
<Button onClick={() => {
VencordNative.native.openExternal("https://discord.gg/eWPBSbvznt");
}}>
ReviewDB Support Server
</Button>
)
}
}).withPrivateSettings<{
token?: string;
user?: ReviewDBUser;
lastReviewId?: number;
reviewsDropdownState?: boolean;
}>();

View File

@ -1,76 +0,0 @@
[class|="section"]:not([class|="lastSection"]) + .vc-rdb-view {
margin-top: 12px;
}
.vc-rdb-badge {
vertical-align: middle;
margin-left: 4px;
}
.vc-rdb-input {
margin-top: 6px;
margin-bottom: 12px;
resize: none;
overflow: hidden;
background: transparent;
border: 1px solid var(--profile-message-input-border-color);
}
.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 {
margin-bottom: 4px;
font-weight: bold;
font-style: italic;
color: var(--text-muted);
}
.vc-rdb-input * {
font-size: 14px;
}
.vc-rdb-modal-footer {
padding: 0;
}
.vc-rdb-modal-footer .vc-rdb-input {
margin-bottom: 0;
background: var(--input-background);
}
.vc-rdb-modal-footer [class|="pageControlContainer"] {
margin-top: 0;
}
.vc-rdb-modal-header {
flex-grow: 1;
}
.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

@ -1,81 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classNameFactory } from "@api/Styles";
import { Logger } from "@utils/Logger";
import { openModal } from "@utils/modal";
import { findByProps } from "@webpack";
import { React, Toasts } from "@webpack/common";
import { Review, UserType } from "./entities";
import { settings } from "./settings";
export const cl = classNameFactory("vc-rdb-");
export function authorize(callback?: any) {
const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");
openModal((props: any) =>
<OAuth2AuthorizeModal
{...props}
scopes={["identify"]}
responseType="code"
redirectUri="https://manti.vendicated.dev/api/reviewdb/auth"
permissions={0n}
clientId="915703782174752809"
cancelCompletesFlow={false}
callback={async (response: any) => {
try {
const url = new URL(response.location);
url.searchParams.append("clientMod", "vencord");
const res = await fetch(url, {
headers: new Headers({ Accept: "application/json" })
});
const { token, success } = await res.json();
if (success) {
settings.store.token = token;
showToast("Successfully logged in!");
callback?.();
} else if (res.status === 1) {
showToast("An Error occurred while logging in.");
}
} catch (e) {
new Logger("ReviewDB").error("Failed to authorize", e);
}
}}
/>
);
}
export function showToast(text: string) {
Toasts.show({
type: Toasts.Type.MESSAGE,
message: text,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
},
});
}
export function canDeleteReview(review: Review, userId: string) {
return (
review.sender.discordID === userId
|| settings.store.user?.type === UserType.Admin
);
}

View File

@ -44,7 +44,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "RoleColorEverywhere",
authors: [Devs.KingFish, Devs.lewisakura],
authors: [Devs.KingFish, Devs.lewisakura, Devs.AutumnVN],
description: "Adds the top role color anywhere possible",
patches: [
// Chat Mentions
@ -78,6 +78,10 @@ export default definePlugin({
match: /(memo\(\(function\((\i)\).{300,500}CHANNEL_MEMBERS_A11Y_LABEL.{100,200}roleIcon.{5,20}null,).," \u2014 ",.\]/,
replace: "$1$self.roleGroupColor($2)]"
},
{
match: /children:\[.," \u2014 ",.\]/,
replace: "children:[$self.roleGroupColor(arguments[0])]"
},
],
predicate: () => settings.store.memberList,
},
@ -105,7 +109,7 @@ export default definePlugin({
return colorString && parseInt(colorString.slice(1), 16);
},
roleGroupColor({ id, count, title, guildId }: { id: string; count: number; title: string; guildId: string; }) {
roleGroupColor({ id, count, title, guildId, label }: { id: string; count: number; title: string; guildId: string; label: string; }) {
const guild = GuildStore.getGuild(guildId);
const role = guild?.roles[id];
@ -113,7 +117,7 @@ export default definePlugin({
color: role?.colorString,
fontWeight: "unset",
letterSpacing: ".05em"
}}>{title} &mdash; {count}</span>;
}}>{title ?? label} &mdash; {count}</span>;
},
getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) {

View File

@ -124,7 +124,7 @@ export default definePlugin({
find: ".activeCommandOption",
replacement: {
match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
replace: "$&;try{$2||$1.push($self.chatBarIcon())}catch{}",
replace: "$&;try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}",
}
},
],
@ -139,7 +139,9 @@ export default definePlugin({
removePreSendListener(this.listener);
},
chatBarIcon() {
chatBarIcon(chatBoxProps: { type: { analyticsName: string; }; }) {
if (chatBoxProps.type.analyticsName !== "normal") return null;
return (
<Tooltip text="Insert Timestamp">
{({ onMouseEnter, onMouseLeave }) => (

View File

@ -170,7 +170,7 @@ function ServerInfoTab({ guild }: GuildProps) {
const Fields = {
"Server Owner": owner ? Owner(guild.id, owner) : "Loading...",
"Created At": renderTimestamp(SnowflakeUtils.extractTimestamp(guild.id)),
"Joined At": renderTimestamp(guild.joinedAt.getTime()),
"Joined At": guild.joinedAt ? renderTimestamp(guild.joinedAt.getTime()) : "-", // Not available in lurked guild
"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] || "?",

View File

@ -18,7 +18,7 @@ const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild;
group?.push(
<Menu.MenuItem
id="vc-server-profile"
label="Server Profile"
label="Server Info"
action={() => openGuildProfileModal(guild)}
/>
);

View File

@ -1,4 +1,4 @@
.vc-uvs-button > div {
.vc-uvs-button>div {
white-space: normal !important;
}
@ -21,6 +21,7 @@
margin-bottom: 0 !important;
}
.vc-uvs-popout-margin > [class^="section"] {
margin-top: -12px;
.vc-uvs-popout-margin-self>[class^="section"] {
padding-top: 0;
padding-bottom: 12px;
}

View File

@ -20,14 +20,13 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { findStoreLazy } from "@webpack";
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
import { VoiceChannelSection } from "./components/VoiceChannelSection";
const VoiceStateStore = findStoreLazy("VoiceStateStore");
const UserPopoutSectionCssClasses = findByPropsLazy("section", "lastSection");
const settings = definePluginSettings({
showInUserProfileModal: {
@ -88,7 +87,7 @@ export default definePlugin({
patchPopout: ({ user }: UserProps) => {
const isSelfUser = user.id === UserStore.getCurrentUser().id;
return (
<div className={isSelfUser ? `vc-uvs-popout-margin ${UserPopoutSectionCssClasses.lastSection}` : ""}>
<div className={isSelfUser ? "vc-uvs-popout-margin-self" : ""}>
<VoiceChannelField user={user} />
</div>
);

View File

@ -24,7 +24,7 @@ import { Margins } from "@utils/margins";
import { wordsToTitle } from "@utils/text";
import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Forms, SelectedChannelStore, useMemo, UserStore } from "@webpack/common";
import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common";
interface VoiceState {
userId: string;
@ -70,10 +70,12 @@ function clean(str: string) {
.trim();
}
function formatText(str: string, user: string, channel: string) {
function formatText(str: string, user: string, channel: string, displayName: string, nickname: string) {
return str
.replaceAll("{{USER}}", clean(user) || (user ? "Someone" : ""))
.replaceAll("{{CHANNEL}}", clean(channel) || "channel");
.replaceAll("{{CHANNEL}}", clean(channel) || "channel")
.replaceAll("{{DISPLAY_NAME}}", clean(displayName) || (displayName ? "Someone" : ""))
.replaceAll("{{NICKNAME}}", clean(nickname) || (nickname ? "Someone" : ""));
}
/*
@ -143,8 +145,10 @@ function updateStatuses(type: string, { deaf, mute, selfDeaf, selfMute, userId,
function playSample(tempSettings: any, type: string) {
const settings = Object.assign({}, Settings.plugins.VcNarrator, tempSettings);
const currentUser = UserStore.getCurrentUser();
const myGuildId = SelectedGuildStore.getGuildId();
speak(formatText(settings[type + "Message"], UserStore.getCurrentUser().username, "general"), settings);
speak(formatText(settings[type + "Message"], currentUser.username, "general", (currentUser as any).globalName ?? currentUser.username, GuildMemberStore.getNick(myGuildId, currentUser.id) ?? currentUser.username), settings);
}
export default definePlugin({
@ -154,6 +158,7 @@ export default definePlugin({
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
const myGuildId = SelectedGuildStore.getGuildId();
const myChanId = SelectedChannelStore.getVoiceChannelId();
const myId = UserStore.getCurrentUser().id;
@ -172,9 +177,11 @@ export default definePlugin({
const template = Settings.plugins.VcNarrator[type + "Message"];
const user = isMe && !Settings.plugins.VcNarrator.sayOwnName ? "" : UserStore.getUser(userId).username;
const displayName = user && ((UserStore.getUser(userId) as any).globalName ?? user);
const nickname = user && (GuildMemberStore.getNick(myGuildId, userId) ?? user);
const channel = ChannelStore.getChannel(id).name;
speak(formatText(template, user, channel));
speak(formatText(template, user, channel, displayName, nickname));
// updateStatuses(type, state, isMe);
}
@ -186,7 +193,7 @@ export default definePlugin({
if (!s) return;
const event = s.mute || s.selfMute ? "unmute" : "mute";
speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name));
speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name, "", ""));
},
AUDIO_TOGGLE_SELF_DEAF() {
@ -195,7 +202,7 @@ export default definePlugin({
if (!s) return;
const event = s.deaf || s.selfDeaf ? "undeafen" : "deafen";
speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name));
speak(formatText(Settings.plugins.VcNarrator[event + "Message"], "", ChannelStore.getChannel(chanId).name, "", ""));
}
},
@ -312,8 +319,8 @@ export default definePlugin({
You can customise the spoken messages below. You can disable specific messages by setting them to nothing
</Forms.FormText>
<Forms.FormText>
The special placeholders <code>{"{{USER}}"}</code> and <code>{"{{CHANNEL}}"}</code>{" "}
will be replaced with the user's name (nothing if it's yourself) and the channel's name respectively
The special placeholders <code>{"{{USER}}"}</code>, <code>{"{{DISPLAY_NAME}}"}</code>, <code>{"{{NICKNAME}}"}</code> and <code>{"{{CHANNEL}}"}</code>{" "}
will be replaced with the user's name (nothing if it's yourself), the user's display name, the user's nickname on current server and the channel's name respectively
</Forms.FormText>
{hasEnglishVoices && (
<>

View File

@ -389,5 +389,3 @@ export const DevsById = /* #__PURE__*/ (() =>
.map(([_, v]) => [v.id, v] as const)
))
)() as Record<string, Dev>;
export const IsFirefox = IS_EXTENSION && navigator.userAgent.toLowerCase().includes("firefox");