merge: dev

This commit is contained in:
Lewis Crichton 2023-10-13 16:21:40 +01:00
commit 791eaa06d4
No known key found for this signature in database
54 changed files with 499 additions and 1622 deletions

View file

@ -2,9 +2,29 @@ name: Blank Issue
description: Create a blank issue. ALWAYS FIRST USE OUR SUPPORT CHANNEL! ONLY USE THIS FORM IF YOU ARE A CONTRIBUTOR OR WERE TOLD TO DO SO IN THE SUPPORT CHANNEL. description: Create a blank issue. ALWAYS FIRST USE OUR SUPPORT CHANNEL! ONLY USE THIS FORM IF YOU ARE A CONTRIBUTOR OR WERE TOLD TO DO SO IN THE SUPPORT CHANNEL.
body: body:
- type: markdown
attributes:
value: |
# READ THIS BEFORE OPENING AN ISSUE
This form is ONLY FOR DEVELOPERS. YOUR ISSUE WILL BE CLOSED AND YOU WILL POSSIBLY BE BLOCKED FROM THE REPOSITORY IF YOU IGNORE THIS.
DO NOT USE THIS FORM, unless
- you are a vencord contributor
- you were given explicit permission to use this form by a moderator in our support server
- you are filing a security related report
- type: textarea - type: textarea
id: content id: content
attributes: attributes:
label: Content label: Content
validations: validations:
required: true required: true
- type: checkboxes
id: agreement-check
attributes:
label: Request Agreement
options:
- label: I have read the requirements for opening an issue above
required: true

View file

@ -4,6 +4,18 @@ labels: [bug]
title: "[Bug] <title>" title: "[Bug] <title>"
body: body:
- type: markdown
attributes:
value: |
# READ THIS BEFORE OPENING AN ISSUE
This form is ONLY FOR DEVELOPERS. YOUR ISSUE WILL BE CLOSED AND YOU WILL POSSIBLY BE BLOCKED FROM THE REPOSITORY IF YOU IGNORE THIS.
DO NOT USE THIS FORM, unless
- you are a vencord contributor
- you were given explicit permission to use this form by a moderator in our support server
- you are filing a security related report
- type: input - type: input
id: discord id: discord
attributes: attributes:
@ -64,3 +76,5 @@ body:
options: options:
- label: I am using Discord Stable or tried on Stable and this bug happens there as well - label: I am using Discord Stable or tried on Stable and this bug happens there as well
required: true required: true
- label: I have read the requirements for opening an issue above
required: true

View file

@ -36,26 +36,10 @@ jobs:
- name: Publish extension - name: Publish extension
run: | 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 cd dist/chromium-unpacked
pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$? pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish
# 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
env: env:
# Chrome
EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }} EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }} CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }} CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }} 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 ## Installing / Uninstalling
Click the below button to install Vencord to the Discord Desktop app Visit https://vencord.dev/download
[![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>
## Join our Support/Community Server ## 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": { "browser_specific_settings": {
"gecko": { "gecko": {

View file

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

View file

@ -145,11 +145,11 @@ async function loadDir(dir, basePath = "") {
/** /**
* @type {(target: string, files: string[]) => Promise<void>} * @type {(target: string, files: string[]) => Promise<void>}
*/ */
async function buildExtension(target, files, noMonaco = false) { async function buildExtension(target, files) {
const entries = { const entries = {
"dist/Vencord.js": await readFile("dist/extension.js"), "dist/Vencord.js": await readFile("dist/extension.js"),
"dist/Vencord.css": await readFile("dist/extension.css"), "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 => ...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}`)] [`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([ await Promise.all([
appendCssRuntime, appendCssRuntime,
buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]), 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"); Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
console.info("Packed Chromium Extension written to dist/extension.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 { interface DecoratorProps {
activities: any[]; activities: any[];
canUseAvatarDecorations: boolean;
channel: Channel; channel: Channel;
/** /**
* Only for DM members * Only for DM members
@ -52,9 +51,9 @@ export function removeDecorator(identifier: string) {
decorators.delete(identifier); decorators.delete(identifier);
} }
export function __addDecoratorsToList(props: DecoratorProps): (JSX.Element | null)[] { export function __getDecorators(props: DecoratorProps): (JSX.Element | null)[] {
const isInGuild = !!(props.guildId); const isInGuild = !!(props.guildId);
return [...decorators.values()].map(decoratorObj => { return Array.from(decorators.values(), decoratorObj => {
const { decorator, onlyIn } = decoratorObj; const { decorator, onlyIn } = decoratorObj;
// this can most likely be done cleaner // this can most likely be done cleaner
if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) { if (!onlyIn || (onlyIn === "guilds" && isInGuild) || (onlyIn === "dms" && !isInGuild)) {

View file

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

View file

@ -20,13 +20,11 @@ import "./themesStyles.css";
import { Settings, useSettings } from "@api/Settings"; import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { CogWheel, DeleteIcon } from "@components/Icons"; import { CogWheel, DeleteIcon } from "@components/Icons";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { AddonCard } from "@components/VencordSettings/AddonCard"; import { AddonCard } from "@components/VencordSettings/AddonCard";
import { SettingsTab, wrapTab } from "@components/VencordSettings/shared"; import { SettingsTab, wrapTab } from "@components/VencordSettings/shared";
import { IsFirefox } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
import { openModal } from "@utils/modal"; import { openModal } from "@utils/modal";
@ -353,14 +351,12 @@ function ThemesTab() {
> >
Load missing Themes Load missing Themes
</Button> </Button>
{!IsFirefox && (
<Button <Button
onClick={() => VencordNative.quickCss.openEditor()} onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
> >
Edit QuickCSS Edit QuickCSS
</Button> </Button>
)}
</> </>
</Card> </Card>
@ -435,15 +431,6 @@ function ThemesTab() {
return ( return (
<SettingsTab title="Themes"> <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 <TabBar
type="top" type="top"
look="brand" look="brand"

View file

@ -21,7 +21,6 @@ import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton"; import DonateButton from "@components/DonateButton";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { IsFirefox } from "@utils/constants";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
import { identity } from "@utils/misc"; import { identity } from "@utils/misc";
import { relaunch, showItemInFolder } from "@utils/native"; import { relaunch, showItemInFolder } from "@utils/native";
@ -110,14 +109,12 @@ function VencordSettings() {
Restart Client Restart Client
</Button> </Button>
)} )}
{!IsFirefox && (
<Button <Button
onClick={() => VencordNative.quickCss.openEditor()} onClick={() => VencordNative.quickCss.openEditor()}
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}> disabled={settingsDir === "Loading..."}>
Open QuickCSS File Open QuickCSS File
</Button> </Button>
)}
{!IS_WEB && ( {!IS_WEB && (
<Button <Button
onClick={() => showItemInFolder(settingsDir)} onClick={() => showItemInFolder(settingsDir)}

View file

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

View file

@ -22,21 +22,28 @@ import definePlugin from "@utils/types";
export default definePlugin({ export default definePlugin({
name: "MemberListDecoratorsAPI", name: "MemberListDecoratorsAPI",
description: "API to add decorators to member list (both in servers and DMs)", description: "API to add decorators to member list (both in servers and DMs)",
authors: [Devs.TheSun], authors: [Devs.TheSun, Devs.Ven],
patches: [ patches: [
{ {
find: "lostPermissionTooltipText,", find: "lostPermissionTooltipText,",
replacement: { replacement: {
match: /Fragment,{children:\[(.{30,80})\]/, match: /decorators:.{0,100}?children:\[(?<=(\i)\.lostPermissionTooltipText.+?)/,
replace: "Fragment,{children:Vencord.Api.MemberListDecorators.__addDecoratorsToList(this.props).concat($1)" replace: "$&...Vencord.Api.MemberListDecorators.__getDecorators($1),"
} }
}, },
{ {
find: "PrivateChannel.renderAvatar", find: "PrivateChannel.renderAvatar",
replacement: { replacement: [
match: /(subText:(.{1,2})\.renderSubtitle\(\).{1,50}decorators):(.{30,100}:null)/, // props are shadowed by nested props so we have to do this
replace: "$1:Vencord.Api.MemberListDecorators.__addDecoratorsToList($2.props).concat($3)" {
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 => () => { addContextMenuPatch("user-settings-cog", children => () => {
const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any; const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any;
section?.forEach(c => { section?.forEach(c => {
if (c?.props?.id?.startsWith("Vencord")) { const id = c?.props?.id;
c.props.action = () => SettingsRouter.open(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 { 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 { isPluginDev } from "@utils/misc";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
@ -30,7 +30,6 @@ import plugins from "~plugins";
import settings from "./settings"; import settings from "./settings";
const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss"; const REMEMBER_DISMISS_KEY = "Vencord-SupportHelper-Dismiss";
const FIREFOX_DISMISS_KEY = "Vencord-Firefox-Warning-Dismiss";
const AllowedChannelIds = [ const AllowedChannelIds = [
SUPPORT_CHANNEL_ID, SUPPORT_CHANNEL_ID,
@ -116,22 +115,6 @@ ${makeCodeblock(enabledPlugins.join(", ") + "\n\n" + enabledApiPlugins.join(", "
onConfirm: rememberDismiss 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() { async start() {
// ArmCord comes with its own arRPC implementation, so this plugin just confuses users // ArmCord comes with its own arRPC implementation, so this plugin just confuses users
if ("armcord" in window) return; if ("armcord" in window) return;
@ -65,22 +85,7 @@ export default definePlugin({
if (ws) ws.close(); if (ws) ws.close();
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
ws.onmessage = async e => { // on message, set status to data ws.onmessage = this.handleEvent;
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 });
};
const connectionSuccessful = await new Promise(res => setTimeout(() => res(ws.readyState === WebSocket.OPEN), 1000)); // check if open after 1s const connectionSuccessful = await new Promise(res => setTimeout(() => res(ws.readyState === WebSocket.OPEN), 1000)); // check if open after 1s
if (!connectionSuccessful) { if (!connectionSuccessful) {

View file

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

View file

@ -33,12 +33,6 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: false, default: false,
restartNeeded: true 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", find: 'get("disable_new_search")',
predicate: () => settings.store.forceStagingBanner, predicate: () => settings.store.enableIsStaff,
replacement: { replacement: {
match: /"staging"===window\.GLOBAL_ENV\.RELEASE_CHANNEL/, match: /(?<=showNewSearch"\);return)\s?!/,
replace: "true" replace: "!1&&!"
} }
}, },
{ {

View file

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

View file

@ -27,12 +27,12 @@ export default definePlugin({
patches: [ patches: [
{ {
// This is the logic where it decides whether to render the owner crown or not // This is the logic where it decides whether to render the owner crown or not
find: ".renderOwner=", find: ".MULTIPLE_AVATAR",
replacement: { replacement: {
match: /isOwner;return null!=(\w+)?&&/g, match: /(\i)=(\i)\.isOwner,/,
replace: "isOwner;if($self.isGuildOwner(this.props)){$1=true;}return null!=$1&&" replace: "$1=$self.isGuildOwner($2),"
}
} }
},
], ],
isGuildOwner(props) { isGuildOwner(props) {
// Check if channel is a Group DM, if so return false // Check if channel is a Group DM, if so return false

View file

@ -1,27 +1,17 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2022 Vendicated and contributors * Copyright (c) 2023 Vendicated and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStore";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react"; import { useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack"; import { findStoreLazy } from "@webpack";
import { Tooltip } from "webpack/common"; import { Tooltip } from "webpack/common";
const enum ActivitiesTypes { const enum ActivitiesTypes {
@ -31,203 +21,154 @@ const enum ActivitiesTypes {
interface IgnoredActivity { interface IgnoredActivity {
id: string; id: string;
name: string;
type: ActivitiesTypes; type: ActivitiesTypes;
} }
const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn");
const TryItOutClasses = findByPropsLazy("tryItOutBadge", "tryItOutBadgeIcon");
const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight");
const RunningGameStore = findStoreLazy("RunningGameStore"); const RunningGameStore = findStoreLazy("RunningGameStore");
const ShowCurrentGame = getSettingStoreLazy<boolean>("status", "showCurrentGame");
function ToggleIconOff() { function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) {
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; }) {
const forceUpdate = useForceUpdater(); const forceUpdate = useForceUpdater();
return ( return (
<Tooltip text="Toggle activity"> <Tooltip text={tooltipText}>
{({ onMouseLeave, onMouseEnter }) => ( {tooltipProps => (
<div <button
onMouseLeave={onMouseLeave} {...tooltipProps}
onMouseEnter={onMouseEnter}
className={RegisteredGamesClasses.overlayToggleIcon}
role="button"
aria-label="Toggle activity"
tabIndex={0}
style={forceLeftMargin ? { marginLeft: "2px" } : undefined}
onClick={e => handleActivityToggle(e, activity, forceUpdate)} onClick={e => handleActivityToggle(e, activity, forceUpdate)}
style={{ all: "unset", cursor: "pointer", display: "flex", justifyContent: "center", alignItems: "center" }}
> >
{ <svg
ignoredActivitiesCache.has(activity.id) width="24"
? <ToggleIconOff /> height="24"
: <ToggleIconOn forceWhite={forceWhite} /> viewBox="0 -960 960 960"
} >
</div> <path fill={fill} d={path} />
</svg>
</button>
)} )}
</Tooltip> </Tooltip>
); );
} }
function ToggleActivityComponentWithBackground({ activity }: { activity: IgnoredActivity; }) { 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);
return ( 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);
<div
className={`${TryItOutClasses.tryItOutBadge} ${BaseShapeRoundClasses.baseShapeRound}`} function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) {
style={{ padding: "0px 2px", height: 28 }} if (getIgnoredActivities().some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)");
> return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)");
<ToggleActivityComponent activity={activity} forceWhite={true} />
</div>
);
} }
function handleActivityToggle(e: React.MouseEvent<HTMLDivElement, MouseEvent>, activity: IgnoredActivity, forceUpdateComponent: () => void) { function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>, activity: IgnoredActivity, forceUpdateButton: () => void) {
e.stopPropagation(); e.stopPropagation();
if (ignoredActivitiesCache.has(activity.id)) ignoredActivitiesCache.delete(activity.id);
else ignoredActivitiesCache.set(activity.id, activity); const ignoredActivityIndex = getIgnoredActivities().findIndex(act => act.id === activity.id);
forceUpdateComponent(); if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity);
saveCacheToDatastore(); else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex);
// Trigger activities recalculation
ShowCurrentGame?.updateSetting(old => old);
forceUpdateButton();
} }
async function saveCacheToDatastore() { const settings = definePluginSettings({}).withPrivateSettings<{
await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache); ignoredActivities: IgnoredActivity[];
} }>();
let ignoredActivitiesCache = new Map<IgnoredActivity["id"], IgnoredActivity>(); function getIgnoredActivities() {
return settings.store.ignoredActivities ??= [];
}
export default definePlugin({ export default definePlugin({
name: "IgnoreActivities", name: "IgnoreActivities",
authors: [Devs.Nuckyz], 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: [ patches: [
{ {
find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY", find: '.displayName="LocalActivityStore"',
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",
replacement: [ replacement: [
{ {
match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i)\.name.+?null/, match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/,
replace: (m, props) => `[${m},$self.renderToggleActivityButton(${props})]` replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);`
},
{
match: /(?<=\(\)\.badgeContainer,children:).{0,50}?name:(\i\.application)\.name.+?null/,
replace: (m, props) => `${m},$self.renderToggleActivityButton(${props})`
} }
] ]
}, },
{ {
find: '.displayName="LocalActivityStore"', find: ".Messages.SETTINGS_GAMES_TOGGLE_OVERLAY",
replacement: { replacement: {
match: /LISTENING.+?\)\);(?<=(\i)\.push.+?)/, match: /\(\)\.removeGame.+?null(?<=(\i)\?\i=\i\.\i\.Messages\.SETTINGS_GAMES_NOW_PLAYING_STATE.+?=(\i)\.overlay.+?)/,
replace: (m, activities) => `${m}${activities}=${activities}.filter($self.isActivityNotIgnored);` 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() { async start() {
const ignoredActivitiesData = await DataStore.get<string[] | Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities") ?? new Map<IgnoredActivity["id"], IgnoredActivity>(); const oldIgnoredActivitiesData = await DataStore.get<Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities");
/** Migrate old data */
if (Array.isArray(ignoredActivitiesData)) { if (oldIgnoredActivitiesData != null) {
for (const id of ignoredActivitiesData) { settings.store.ignoredActivities = Array.from(oldIgnoredActivitiesData.values())
ignoredActivitiesCache.set(id, { id, type: ActivitiesTypes.Game }); .map(activity => ({ ...activity, name: "Unknown Name" }));
DataStore.del("IgnoreActivities_ignoredActivities");
} }
await saveCacheToDatastore(); if (getIgnoredActivities().length !== 0) {
} else ignoredActivitiesCache = ignoredActivitiesData; const gamesSeen = RunningGameStore.getGamesSeen() as { id?: string; exePath: string; }[];
if (ignoredActivitiesCache.size !== 0) { for (const [index, ignoredActivity] of getIgnoredActivities().entries()) {
const gamesSeen: { id?: string; exePath: string; }[] = RunningGameStore.getGamesSeen();
for (const ignoredActivity of ignoredActivitiesCache.values()) {
if (ignoredActivity.type !== ActivitiesTypes.Game) continue; if (ignoredActivity.type !== ActivitiesTypes.Game) continue;
if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) { if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) {
/** Custom added game which no longer exists */ getIgnoredActivities().splice(index, 1);
ignoredActivitiesCache.delete(ignoredActivity.id);
} }
} }
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; }) { isActivityNotIgnored(props: { type: number; application_id?: string; name?: string; }) {
if (props.type === 0) { if (props.type === 0 || props.type === 3) {
if (props.application_id !== undefined) return !ignoredActivitiesCache.has(props.application_id); if (props.application_id != null) return !getIgnoredActivities().some(activity => activity.id === props.application_id);
else { else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; 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; 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"', find: '"renderLinkComponent","maxWidth"',
replacement: { replacement: {
match: /(return\(.{1,100}\(\)\.wrapper.{1,100})(src)/, match: /(return\(.{1,100}\(\)\.wrapper.{1,200})(src)/,
replace: `$1id: '${ELEMENT_ID}',$2` replace: `$1id: '${ELEMENT_ID}',$2`
} }
}, },
@ -174,8 +174,8 @@ export default definePlugin({
find: "handleImageLoad=", find: "handleImageLoad=",
replacement: [ replacement: [
{ {
match: /(render=function\(\){.{1,500}limitResponsiveWidth.{1,600})onMouseEnter:/, match: /showThumbhashPlaceholder:/,
replace: "$1...$self.makeProps(this),onMouseEnter:" replace: "...$self.makeProps(this),$&"
}, },
{ {
@ -189,7 +189,6 @@ export default definePlugin({
} }
] ]
}, },
{ {
find: ".carouselModal,", find: ".carouselModal,",
replacement: { replacement: {

View file

@ -53,8 +53,6 @@ interface TagSettings {
[k: string]: TagSetting; [k: string]: TagSetting;
} }
const CLYDE_ID = "1081004946872352958";
// PermissionStore.computePermissions is not the same function and doesn't work here // PermissionStore.computePermissions is not the same function and doesn't work here
const PermissionUtil = findByPropsLazy("computePermissions", "canEveryoneRole") as { const PermissionUtil = findByPropsLazy("computePermissions", "canEveryoneRole") as {
computePermissions({ ...args }): bigint; computePermissions({ ...args }): bigint;
@ -215,7 +213,7 @@ export default definePlugin({
}, },
// add HTML data attributes (for easier theming) // 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:[" replace: "'data-tag':$1.toLowerCase(),children:["
} }
], ],
@ -230,10 +228,10 @@ export default definePlugin({
}, },
// in the member list // in the member list
{ {
find: ".renderBot=function(){", find: ".Messages.GUILD_OWNER,",
replacement: { replacement: {
match: /\.BOT;return null!=(\i)&&.{0,10}\?(.{0,50})\.botTag,type:\i/, match: /(?<type>\i)=\(null==.{0,50}\.BOT,null!=(?<user>\i)&&\i\.bot/,
replace: ".BOT;var type=$self.getTag({...this.props,origType:$1.bot?0:null,location:'not-chat'});return type!==null?$2.botTag,type" 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 // pass channel id down props to be used in profiles
@ -253,7 +251,7 @@ export default definePlugin({
}, },
// in profiles // in profiles
{ {
find: ",botType:", find: "showStreamerModeTooltip:",
replacement: { replacement: {
match: /,botType:(\i\((\i)\)),/g, match: /,botType:(\i\((\i)\)),/g,
replace: ",botType:$self.getTag({user:$2,channelId:arguments[0].moreTags_channelId,origType:$1,location:'not-chat'})," 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, user, channelId, origType, location, channel
}: { }: {
message?: Message, message?: Message,
user: User, user: User & { isClyde(): boolean; },
channel?: Channel & { isForumPost(): boolean; }, channel?: Channel & { isForumPost(): boolean; },
channelId?: string; channelId?: string;
origType?: number; origType?: number;
location: "chat" | "not-chat"; location: "chat" | "not-chat";
}): number | null { }): number | null {
if (!user)
return null;
if (location === "chat" && user.id === "1") if (location === "chat" && user.id === "1")
return Tag.Types.OFFICIAL; return Tag.Types.OFFICIAL;
if (user.id === CLYDE_ID) if (user.isClyde())
return Tag.Types.AI; return Tag.Types.AI;
let type = typeof origType === "number" ? origType : null; 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,7 @@
# OnePingPerDM
If unread messages are sent by a user in DMs multiple times, you'll only receive one audio ping. Read the messages to reset the limit
## Purpose
- Prevents ping audio spam in DMs
- Be able to distinguish more than one ping as multiple users
- Be less annoyed while gaming

View file

@ -0,0 +1,39 @@
/*
* 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";
import { ChannelStore, ReadStateStore } from "@webpack/common";
import { Message } from "discord-types/general";
const enum ChannelType {
DM = 1,
GROUP_DM = 3
}
export default definePlugin({
name: "OnePingPerDM",
description: "If unread messages are sent by a user in DMs multiple times, you'll only receive one audio ping. Read the messages to reset the limit",
authors: [Devs.ProffDea],
patches: [{
find: ".getDesktopType()===",
replacement: [{
match: /if\((\i\.\i\.getDesktopType\(\)===\i\.\i\.NEVER)\){/,
replace: "if($1){if(!$self.isPrivateChannelRead(arguments[0]?.message))return;"
},
{
match: /sound:(\i\?\i:void 0,volume:\i,onClick:)/,
replace: "sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1"
}]
}],
isPrivateChannelRead(message: Message) {
const channelType = ChannelStore.getChannel(message.channel_id)?.type;
if (channelType !== ChannelType.DM && channelType !== ChannelType.GROUP_DM) {
return false;
}
return ReadStateStore.getOldestUnreadMessageId(message.channel_id) === message.id;
},
});

View file

@ -19,10 +19,7 @@
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack"; import { FluxDispatcher } from "@webpack/common";
import { GenericStore } from "@webpack/common";
const PoggerModeSettingsStore: GenericStore = findStoreLazy("PoggermodeSettingsStore");
const enum Intensity { const enum Intensity {
Normal, Normal,
@ -61,9 +58,12 @@ export default definePlugin({
}); });
function setPoggerState(state: boolean) { function setPoggerState(state: boolean) {
Object.assign(PoggerModeSettingsStore.__getLocalVars().state, { FluxDispatcher.dispatch({
type: "POGGERMODE_SETTINGS_UPDATE",
settings: {
enabled: state, enabled: state,
settingsVisible: state settingsVisible: state
}
}); });
} }
@ -101,5 +101,8 @@ function setSettings(intensity: Intensity) {
} }
} }
Object.assign(PoggerModeSettingsStore.__getLocalVars().state, state); FluxDispatcher.dispatch({
type: "POGGERMODE_SETTINGS_UPDATE",
settings: state
});
} }

View file

@ -0,0 +1,9 @@
# PermissionFreeWill
Removes the client-side restrictions that prevent editing channel permissions, such as permission lockouts ("Pretty sure
you don't want to do this") and onboarding requirements ("Making this change will make your server incompatible [...]")
## Warning
This plugin will let you create permissions in servers that **WILL** lock you out of channels until an administrator
can resolve it for you. Please be careful with the overwrites you are making and check carefully.

View file

@ -0,0 +1,56 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
lockout: {
type: OptionType.BOOLEAN,
default: true,
description: 'Bypass the permission lockout prevention ("Pretty sure you don\'t want to do this")',
restartNeeded: true
},
onboarding: {
type: OptionType.BOOLEAN,
default: true,
description: 'Bypass the onboarding requirements ("Making this change will make your server incompatible [...]")',
restartNeeded: true
}
});
export default definePlugin({
name: "PermissionFreeWill",
description: "Disables the client-side restrictions for channel permission management.",
authors: [Devs.lewisakura],
patches: [
// Permission lockout, just set the check to true
{
find: "Messages.SELF_DENY_PERMISSION_BODY",
replacement: [
{
match: /case"DENY":.{0,50}if\((?=\i\.\i\.can)/,
replace: "$&true||"
}
],
predicate: () => settings.store.lockout
},
// Onboarding, same thing but we need to prevent the check
{
find: "Messages.ONBOARDING_CHANNEL_THRESHOLD_WARNING",
replacement: [
{
match: /case 1:if\((?=!\i\.sent.{20,30}Messages\.CANNOT_CHANGE_CHANNEL_PERMS)/,
replace: "$&false&&"
}
],
predicate: () => settings.store.onboarding
}
],
settings
});

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import "./styles.css";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
@ -41,6 +43,7 @@ export default definePlugin({
{tooltipProps => ( {tooltipProps => (
<div <div
{...tooltipProps} {...tooltipProps}
className="vc-pip-button"
role="button" role="button"
style={{ style={{
cursor: "pointer", cursor: "pointer",
@ -71,7 +74,7 @@ export default definePlugin({
> >
<svg width="24px" height="24px" viewBox="0 0 24 24"> <svg width="24px" height="24px" viewBox="0 0 24 24">
<path <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" 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> </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", find: ".USER_PROFILE_ACTIVITY",
replacement: [ replacement: [
{ {
/* FIXME: old name is getGlobalName, new name is getName. Remove optional Global once stable has also migrated */ match: /\.getName\(\i\);(?<=displayProfile.{0,200})/,
match: /\.get(?:Global)?Name\(\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;" replace: "$&const [vcPronounce,vcPronounSource]=$self.useProfilePronouns(arguments[0].user.id,true);if(arguments[0].displayProfile&&vcPronounce)arguments[0].displayProfile.pronouns=vcPronounce;"
}, },
PRONOUN_TOOLTIP_PATCH 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")
);
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" }}
/>
<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({ export default definePlugin({
name: "RoleColorEverywhere", name: "RoleColorEverywhere",
authors: [Devs.KingFish, Devs.lewisakura], authors: [Devs.KingFish, Devs.lewisakura, Devs.AutumnVN],
description: "Adds the top role color anywhere possible", description: "Adds the top role color anywhere possible",
patches: [ patches: [
// Chat Mentions // 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 ",.\]/, match: /(memo\(\(function\((\i)\).{300,500}CHANNEL_MEMBERS_A11Y_LABEL.{100,200}roleIcon.{5,20}null,).," \u2014 ",.\]/,
replace: "$1$self.roleGroupColor($2)]" replace: "$1$self.roleGroupColor($2)]"
}, },
{
match: /children:\[.," \u2014 ",.\]/,
replace: "children:[$self.roleGroupColor(arguments[0])]"
},
], ],
predicate: () => settings.store.memberList, predicate: () => settings.store.memberList,
}, },
@ -105,7 +109,7 @@ export default definePlugin({
return colorString && parseInt(colorString.slice(1), 16); 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 guild = GuildStore.getGuild(guildId);
const role = guild?.roles[id]; const role = guild?.roles[id];
@ -113,7 +117,7 @@ export default definePlugin({
color: role?.colorString, color: role?.colorString,
fontWeight: "unset", fontWeight: "unset",
letterSpacing: ".05em" letterSpacing: ".05em"
}}>{title} &mdash; {count}</span>; }}>{title ?? label} &mdash; {count}</span>;
}, },
getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) { getVoiceProps({ user: { id: userId }, guildId }: { user: { id: string; }; guildId: string; }) {

View file

@ -124,7 +124,7 @@ export default definePlugin({
find: ".activeCommandOption", find: ".activeCommandOption",
replacement: { replacement: {
match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, 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); removePreSendListener(this.listener);
}, },
chatBarIcon() { chatBarIcon(chatBoxProps: { type: { analyticsName: string; }; }) {
if (chatBoxProps.type.analyticsName !== "normal") return null;
return ( return (
<Tooltip text="Insert Timestamp"> <Tooltip text="Insert Timestamp">
{({ onMouseEnter, onMouseLeave }) => ( {({ onMouseEnter, onMouseLeave }) => (

View file

@ -170,7 +170,7 @@ function ServerInfoTab({ guild }: GuildProps) {
const Fields = { const Fields = {
"Server Owner": owner ? Owner(guild.id, owner) : "Loading...", "Server Owner": owner ? Owner(guild.id, owner) : "Loading...",
"Created At": renderTimestamp(SnowflakeUtils.extractTimestamp(guild.id)), "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 "Vanity Link": guild.vanityURLCode ? (<a>{`discord.gg/${guild.vanityURLCode}`}</a>) : "-", // Making the anchor href valid would cause Discord to reload
"Preferred Locale": guild.preferredLocale || "-", "Preferred Locale": guild.preferredLocale || "-",
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?", "Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",

View file

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

View file

@ -70,10 +70,11 @@ function clean(str: string) {
.trim(); .trim();
} }
function formatText(str: string, user: string, channel: string) { function formatText(str: string, user: string, channel: string, displayName: string) {
return str return str
.replaceAll("{{USER}}", clean(user) || (user ? "Someone" : "")) .replaceAll("{{USER}}", clean(user) || (user ? "Someone" : ""))
.replaceAll("{{CHANNEL}}", clean(channel) || "channel"); .replaceAll("{{CHANNEL}}", clean(channel) || "channel")
.replaceAll("{{DISPLAY_NAME}}", clean(displayName) || (displayName ? "Someone" : ""));
} }
/* /*
@ -143,8 +144,9 @@ function updateStatuses(type: string, { deaf, mute, selfDeaf, selfMute, userId,
function playSample(tempSettings: any, type: string) { function playSample(tempSettings: any, type: string) {
const settings = Object.assign({}, Settings.plugins.VcNarrator, tempSettings); const settings = Object.assign({}, Settings.plugins.VcNarrator, tempSettings);
const currentUser = UserStore.getCurrentUser();
speak(formatText(settings[type + "Message"], UserStore.getCurrentUser().username, "general"), settings); speak(formatText(settings[type + "Message"], currentUser.username, "general", (currentUser as any).globalName ?? currentUser.username), settings);
} }
export default definePlugin({ export default definePlugin({
@ -172,9 +174,10 @@ export default definePlugin({
const template = Settings.plugins.VcNarrator[type + "Message"]; const template = Settings.plugins.VcNarrator[type + "Message"];
const user = isMe && !Settings.plugins.VcNarrator.sayOwnName ? "" : UserStore.getUser(userId).username; const user = isMe && !Settings.plugins.VcNarrator.sayOwnName ? "" : UserStore.getUser(userId).username;
const displayName = user && ((UserStore.getUser(userId) as any).globalName ?? user);
const channel = ChannelStore.getChannel(id).name; const channel = ChannelStore.getChannel(id).name;
speak(formatText(template, user, channel)); speak(formatText(template, user, channel, displayName));
// updateStatuses(type, state, isMe); // updateStatuses(type, state, isMe);
} }
@ -186,7 +189,7 @@ export default definePlugin({
if (!s) return; if (!s) return;
const event = s.mute || s.selfMute ? "unmute" : "mute"; 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() { AUDIO_TOGGLE_SELF_DEAF() {
@ -195,7 +198,7 @@ export default definePlugin({
if (!s) return; if (!s) return;
const event = s.deaf || s.selfDeaf ? "undeafen" : "deafen"; 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 +315,8 @@ export default definePlugin({
You can customise the spoken messages below. You can disable specific messages by setting them to nothing You can customise the spoken messages below. You can disable specific messages by setting them to nothing
</Forms.FormText> </Forms.FormText>
<Forms.FormText> <Forms.FormText>
The special placeholders <code>{"{{USER}}"}</code> and <code>{"{{CHANNEL}}"}</code>{" "} The special placeholders <code>{"{{USER}}"}</code>, <code>{"{{DISPLAY_NAME}}"}</code> and <code>{"{{CHANNEL}}"}</code>{" "}
will be replaced with the user's name (nothing if it's yourself) and the channel's name respectively will be replaced with the user's name (nothing if it's yourself), the user's display name and the channel's name respectively
</Forms.FormText> </Forms.FormText>
{hasEnglishVoices && ( {hasEnglishVoices && (
<> <>

View file

@ -0,0 +1,5 @@
# WhoReacted
Next to each reaction, display each user's avatar. Each avatar can be clicked and will open the profile.
![](https://github.com/Vendicated/Vencord/assets/57493648/97fec9e8-396f-4f5e-916e-1ec21445113d)

View file

@ -24,14 +24,14 @@ import { LazyComponent, useForceUpdater } from "@utils/react";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack"; import { findByCode, findByPropsLazy } from "@webpack";
import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common"; import { ChannelStore, FluxDispatcher, React, RestAPI, Tooltip } from "@webpack/common";
import { ReactionEmoji, User } from "discord-types/general"; import { CustomEmoji } from "@webpack/types";
import { Message, ReactionEmoji, User } from "discord-types/general";
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers")); const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
const ReactionStore = findByPropsLazy("getReactions");
const queue = new Queue(); const queue = new Queue();
let reactions: Record<string, ReactionCacheEntry>;
function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) { function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
const key = emoji.name + (emoji.id ? `:${emoji.id}` : ""); const key = emoji.name + (emoji.id ? `:${emoji.id}` : "");
@ -57,11 +57,9 @@ function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) { function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) {
const key = `${msg.id}:${e.name}:${e.id ?? ""}:${type}`; const key = `${msg.id}:${e.name}:${e.id ?? ""}:${type}`;
const cache = ReactionStore.__getLocalVars().reactions[key] ??= { fetched: false, users: {} }; const cache = reactions[key] ??= { fetched: false, users: {} };
if (!cache.fetched) { if (!cache.fetched) {
queue.unshift(() => queue.unshift(() => fetchReactions(msg, e, type));
fetchReactions(msg, e, type)
);
cache.fetched = true; cache.fetched = true;
} }
@ -92,7 +90,7 @@ function handleClickAvatar(event: React.MouseEvent<HTMLElement, MouseEvent>) {
export default definePlugin({ export default definePlugin({
name: "WhoReacted", name: "WhoReacted",
description: "Renders the Avatars of reactors", description: "Renders the avatars of users who reacted to a message",
authors: [Devs.Ven, Devs.KannaDev], authors: [Devs.Ven, Devs.KannaDev],
patches: [{ patches: [{
@ -101,6 +99,12 @@ export default definePlugin({
match: /(?<=(\i)=(\i)\.hideCount,)(.+?reactionCount.+?\}\))/, match: /(?<=(\i)=(\i)\.hideCount,)(.+?reactionCount.+?\}\))/,
replace: (_, hideCount, props, rest) => `whoReactedProps=${props},${rest},${hideCount}?null:$self.renderUsers(whoReactedProps)` replace: (_, hideCount, props, rest) => `whoReactedProps=${props},${rest},${hideCount}?null:$self.renderUsers(whoReactedProps)`
} }
}, {
find: '.displayName="MessageReactionsStore";',
replacement: {
match: /(?<=CONNECTION_OPEN:function\(\){)(\i)={}/,
replace: "$&;$self.reactions=$1"
}
}], }],
renderUsers(props: RootObject) { renderUsers(props: RootObject) {
@ -150,106 +154,25 @@ export default definePlugin({
</div> </div>
</div> </div>
); );
},
set reactions(value: any) {
reactions = value;
} }
}); });
interface ReactionCacheEntry {
export interface GuildMemberAvatar { } fetched: boolean;
users: Record<string, User>;
export interface Author {
id: string;
username: string;
discriminator: string;
avatar: string;
avatarDecoration?: any;
email: string;
verified: boolean;
bot: boolean;
system: boolean;
mfaEnabled: boolean;
mobile: boolean;
desktop: boolean;
premiumType: number;
flags: number;
publicFlags: number;
purchasedFlags: number;
premiumUsageFlags: number;
phone: string;
nsfwAllowed: boolean;
guildMemberAvatars: GuildMemberAvatar;
} }
export interface Emoji { interface RootObject {
id: string;
name: string;
}
export interface Reaction {
emoji: Emoji;
count: number;
burst_user_ids: any[];
burst_count: number;
burst_colors: any[];
burst_me: boolean;
me: boolean;
}
export interface Message {
id: string;
type: number;
channel_id: string;
author: Author;
content: string;
deleted: boolean;
editHistory: any[];
attachments: any[];
embeds: any[];
mentions: any[];
mentionRoles: any[];
mentionChannels: any[];
mentioned: boolean;
pinned: boolean;
mentionEveryone: boolean;
tts: boolean;
codedLinks: any[];
giftCodes: any[];
timestamp: string;
editedTimestamp?: any;
state: string;
nonce?: any;
blocked: boolean;
call?: any;
bot: boolean;
webhookId?: any;
reactions: Reaction[];
applicationId?: any;
application?: any;
activity?: any;
messageReference?: any;
flags: number;
isSearchHit: boolean;
stickers: any[];
stickerItems: any[];
components: any[];
loggingName?: any;
interaction?: any;
interactionData?: any;
interactionError?: any;
}
export interface Emoji {
id: string;
name: string;
animated: boolean;
}
export interface RootObject {
message: Message; message: Message;
readOnly: boolean; readOnly: boolean;
isLurking: boolean; isLurking: boolean;
isPendingMember: boolean; isPendingMember: boolean;
useChatFontScaling: boolean; useChatFontScaling: boolean;
emoji: Emoji; emoji: CustomEmoji;
count: number; count: number;
burst_user_ids: any[]; burst_user_ids: any[];
burst_count: number; burst_count: number;

View file

@ -374,7 +374,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({
archeruwu: { archeruwu: {
name: "archer_uwu", name: "archer_uwu",
id: 160068695383736320n id: 160068695383736320n
} },
ProffDea: {
name: "ProffDea",
id: 609329952180928513n
},
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly // iife so #__PURE__ works correctly
@ -385,5 +389,3 @@ export const DevsById = /* #__PURE__*/ (() =>
.map(([_, v]) => [v.id, v] as const) .map(([_, v]) => [v.id, v] as const)
)) ))
)() as Record<string, Dev>; )() as Record<string, Dev>;
export const IsFirefox = IS_EXTENSION && navigator.userAgent.toLowerCase().includes("firefox");

View file

@ -20,5 +20,5 @@ import { findByPropsLazy } from "@webpack";
import * as t from "./types/classes"; import * as t from "./types/classes";
export const ModalImageClasses: t.ImageModalClasses = findByPropsLazy("image", "modal"); export const ModalImageClasses: t.ImageModalClasses = findByPropsLazy("image", "modal", "responsiveWidthMobile");
export const ButtonWrapperClasses: t.ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent"); export const ButtonWrapperClasses: t.ButtonWrapperClasses = findByPropsLazy("buttonWrapper", "buttonContent");