Merge branch 'Vendicated:main' into main

This commit is contained in:
Manti 2023-04-22 22:03:30 +03:00 committed by GitHub
commit 8065b19cd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 2673 additions and 641 deletions

View file

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

View file

@ -35,15 +35,15 @@ jobs:
- name: Publish extension - name: Publish extension
run: | run: |
cd dist/extension-unpacked
# Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later # 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 EXIT_CODE=0
# Chrome # Chrome
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 || EXIT_CODE=$?
# Firefox # Firefox
cd ../firefox-unpacked
npm i -g web-ext@7.4.0 web-ext-submit@7.4.0 npm i -g web-ext@7.4.0 web-ext-submit@7.4.0
web-ext-submit || EXIT_CODE=$? web-ext-submit || EXIT_CODE=$?
@ -58,4 +58,3 @@ jobs:
# Firefox # Firefox
WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }} WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }}
WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }} WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }}

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

@ -21,7 +21,8 @@
{ {
"run_at": "document_start", "run_at": "document_start",
"matches": ["*://*.discord.com/*"], "matches": ["*://*.discord.com/*"],
"js": ["content.js"] "js": ["content.js"],
"all_frames": true
} }
], ],

41
browser/manifestv2.json Normal file
View file

@ -0,0 +1,41 @@
{
"manifest_version": 2,
"minimum_chrome_version": "91",
"name": "Vencord Web",
"description": "The cutest Discord mod now in your browser",
"author": "Vendicated",
"homepage_url": "https://github.com/Vendicated/Vencord",
"icons": {
"128": "icon.png"
},
"permissions": [
"webRequest",
"webRequestBlocking",
"*://*.discord.com/*",
"https://raw.githubusercontent.com/*"
],
"content_scripts": [
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["content.js"],
"all_frames": true
}
],
"background": {
"scripts": ["background.js"]
},
"web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"],
"browser_specific_settings": {
"gecko": {
"id": "vencord-firefox@vendicated.dev",
"strict_min_version": "91.0"
}
}
}

View file

@ -15,7 +15,7 @@
] ]
}, },
"condition": { "condition": {
"resourceTypes": ["main_frame"] "resourceTypes": ["main_frame", "sub_frame"]
} }
}, },
{ {

View file

@ -1,7 +1,7 @@
{ {
"name": "vencord", "name": "vencord",
"private": "true", "private": "true",
"version": "1.1.6", "version": "1.1.9",
"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": {

View file

@ -142,6 +142,7 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
await Promise.all([ await Promise.all([
appendCssRuntime, appendCssRuntime,
buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true), buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true),
buildPluginZip("extension-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false), buildPluginZip("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false),
buildPluginZip("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"], false),
]); ]);

View file

@ -186,7 +186,16 @@ page.on("console", async e => {
} else if (isDebug) { } else if (isDebug) {
console.error(e.text()); console.error(e.text());
} else if (level === "error") { } else if (level === "error") {
const text = e.text(); const text = await Promise.all(
e.args().map(async a => {
try {
return await maybeGetError(a) || await a.jsonValue();
} catch (e) {
return a.toString();
}
})
).then(a => a.join(" "));
if (!text.startsWith("Failed to load resource: the server responded with a status of")) { if (!text.startsWith("Failed to load resource: the server responded with a status of")) {
console.error("Got unexpected error", text); console.error("Got unexpected error", text);
report.otherErrors.push(text); report.otherErrors.push(text);
@ -258,7 +267,7 @@ function runTime(token: string) {
if (!isWasm) if (!isWasm)
await wreq.e(id as any); await wreq.e(id as any);
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 500));
} }
console.error("[PUP_DEBUG]", "Finished loading chunks!"); console.error("[PUP_DEBUG]", "Finished loading chunks!");

View file

@ -27,12 +27,11 @@ export { PlainSettings, Settings };
import "./utils/quickCss"; import "./utils/quickCss";
import "./webpack/patchWebpack"; import "./webpack/patchWebpack";
import { relaunch } from "@utils/native";
import { showNotification } from "./api/Notifications"; import { showNotification } from "./api/Notifications";
import { PlainSettings, Settings } from "./api/settings"; import { PlainSettings, Settings } from "./api/settings";
import { patches, PMLogger, startAllPlugins } from "./plugins"; import { patches, PMLogger, startAllPlugins } from "./plugins";
import { localStorage } from "./utils/localStorage"; import { localStorage } from "./utils/localStorage";
import { relaunch } from "./utils/native";
import { getCloudSettings, putCloudSettings } from "./utils/settingsSync"; import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater"; import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack"; import { onceReady } from "./webpack";
@ -57,7 +56,7 @@ async function syncSettings() {
title: "Cloud Settings", title: "Cloud Settings",
body: "Your settings have been updated! Click here to restart to fully apply changes!", body: "Your settings have been updated! Click here to restart to fully apply changes!",
color: "var(--green-360)", color: "var(--green-360)",
onClick: () => window.DiscordNative.app.relaunch() onClick: relaunch
}); });
} }
} }

View file

@ -29,11 +29,12 @@ export enum BadgePosition {
export interface ProfileBadge { export interface ProfileBadge {
/** The tooltip to show on hover. Required for image badges */ /** The tooltip to show on hover. Required for image badges */
tooltip?: string; description?: string;
/** Custom component for the badge (tooltip not included) */ /** Custom component for the badge (tooltip not included) */
component?: ComponentType<ProfileBadge & BadgeUserArgs>; component?: ComponentType<ProfileBadge & BadgeUserArgs>;
/** The custom image to use */ /** The custom image to use */
image?: string; image?: string;
link?: string;
/** Action to perform when you click the badge */ /** Action to perform when you click the badge */
onClick?(): void; onClick?(): void;
/** Should the user display this badge? */ /** Should the user display this badge? */
@ -69,17 +70,19 @@ export function removeBadge(badge: ProfileBadge) {
* Inject badges into the profile badges array. * Inject badges into the profile badges array.
* You probably don't need to use this. * You probably don't need to use this.
*/ */
export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) { export function _getBadges(args: BadgeUserArgs) {
const badges = [] as ProfileBadge[];
for (const badge of Badges) { for (const badge of Badges) {
if (!badge.shouldShow || badge.shouldShow(args)) { if (!badge.shouldShow || badge.shouldShow(args)) {
badge.position === BadgePosition.START badge.position === BadgePosition.START
? badgeArray.unshift({ ...badge, ...args }) ? badges.unshift({ ...badge, ...args })
: badgeArray.push({ ...badge, ...args }); : badges.push({ ...badge, ...args });
} }
} }
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id); const donorBadge = (Plugins.BadgeAPI as any).getDonorBadge(args.user.id);
if (donorBadge) badges.unshift(donorBadge);
return badgeArray; return badges;
} }
export interface BadgeUserArgs { export interface BadgeUserArgs {

View file

@ -111,6 +111,7 @@ function registerSubCommands(cmd: Command, plugin: string) {
...o, ...o,
type: ApplicationCommandType.CHAT_INPUT, type: ApplicationCommandType.CHAT_INPUT,
name: `${cmd.name} ${o.name}`, name: `${cmd.name} ${o.name}`,
id: `${o.name}-${cmd.id}`,
displayName: `${cmd.name} ${o.name}`, displayName: `${cmd.name} ${o.name}`,
subCommandPath: [{ subCommandPath: [{
name: o.name, name: o.name,

View file

@ -19,17 +19,20 @@
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
type ContextMenuPatchCallbackReturn = (() => void) | void;
/** /**
* @param children The rendered context menu elements * @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
*/ */
export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, ...args: Array<any>) => void; export type NavContextMenuPatchCallback = (children: Array<React.ReactElement>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
/** /**
* @param The navId of the context menu being patched * @param navId The navId of the context menu being patched
* @param children The rendered context menu elements * @param children The rendered context menu elements
* @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example
* @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates)
*/ */
export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, ...args: Array<any>) => void; export type GlobalContextMenuPatchCallback = (navId: string, children: Array<React.ReactElement>, ...args: Array<any>) => ContextMenuPatchCallbackReturn;
const ContextMenuLogger = new Logger("ContextMenu"); const ContextMenuLogger = new Logger("ContextMenu");
@ -78,6 +81,7 @@ export function removeContextMenuPatch<T extends string | Array<string>>(navId:
/** /**
* Remove a global context menu patch * Remove a global context menu patch
* @param patch The patch to be removed
* @returns Wheter the patch was sucessfully removed * @returns Wheter the patch was sucessfully removed
*/ */
export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean { export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean {
@ -87,12 +91,13 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba
/** /**
* A helper function for finding the children array of a group nested inside a context menu based on the id of one of its childs * A helper function for finding the children array of a group nested inside a context menu based on the id of one of its childs
* @param id The id of the child * @param id The id of the child
* @param children The context menu children
*/ */
export function findGroupChildrenByChildId(id: string, children: Array<React.ReactElement>, itemsArray?: Array<React.ReactElement>): Array<React.ReactElement> | null { export function findGroupChildrenByChildId(id: string, children: Array<React.ReactElement>, _itemsArray?: Array<React.ReactElement>): Array<React.ReactElement> | null {
for (const child of children) { for (const child of children) {
if (child == null) continue; if (child == null) continue;
if (child.props?.id === id) return itemsArray ?? null; if (child.props?.id === id) return _itemsArray ?? null;
let nextChildren = child.props?.children; let nextChildren = child.props?.children;
if (nextChildren) { if (nextChildren) {
@ -118,6 +123,8 @@ interface ContextMenuProps {
onClose: (callback: (...args: Array<any>) => any) => void; onClose: (callback: (...args: Array<any>) => any) => void;
} }
const patchedMenus = new WeakSet();
export function _patchContextMenu(props: ContextMenuProps) { export function _patchContextMenu(props: ContextMenuProps) {
props.contextMenuApiArguments ??= []; props.contextMenuApiArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId); const contextMenuPatches = navPatches.get(props.navId);
@ -127,7 +134,8 @@ export function _patchContextMenu(props: ContextMenuProps) {
if (contextMenuPatches) { if (contextMenuPatches) {
for (const patch of contextMenuPatches) { for (const patch of contextMenuPatches) {
try { try {
patch(props.children, ...props.contextMenuApiArguments); const callback = patch(props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
} catch (err) { } catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
} }
@ -136,9 +144,12 @@ export function _patchContextMenu(props: ContextMenuProps) {
for (const patch of globalPatches) { for (const patch of globalPatches) {
try { try {
patch(props.navId, props.children, ...props.contextMenuApiArguments); const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments);
if (!patchedMenus.has(props)) callback?.();
} catch (err) { } catch (err) {
ContextMenuLogger.error("Global patch errored,", err); ContextMenuLogger.error("Global patch errored,", err);
} }
} }
patchedMenus.add(props);
} }

View file

@ -77,6 +77,8 @@ function _showNotification(notification: NotificationData, id: number) {
} }
function shouldBeNative() { function shouldBeNative() {
if (typeof Notification === "undefined") return false;
const { useNative } = Settings.notifications; const { useNative } = Settings.notifications;
if (useNative === "always") return true; if (useNative === "always") return true;
if (useNative === "not-focused") return !document.hasFocus(); if (useNative === "not-focused") return !document.hasFocus();

View file

@ -91,7 +91,7 @@ export default ErrorBoundary.wrap(function () {
return ( return (
<> <>
<Card className="vc-settings-card vc-text-selectable"> <Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle> <Forms.FormTitle tag="h5">Paste links to .theme.css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText> <Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText> <Forms.FormText>Make sure to use the raw links or github.io links!</Forms.FormText>
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} /> <Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
@ -103,7 +103,7 @@ export default ErrorBoundary.wrap(function () {
<Link href="https://github.com/search?q=discord+theme">GitHub</Link> <Link href="https://github.com/search?q=discord+theme">GitHub</Link>
</div> </div>
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText> <Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button</Forms.FormText> <Forms.FormText>In the GitHub repository of your theme, find X.theme.css, click on it, then click the "Raw" button</Forms.FormText>
<Forms.FormText> <Forms.FormText>
If the theme has configuration that requires you to edit the file: If the theme has configuration that requires you to edit the file:
<ul> <ul>

View file

@ -18,7 +18,7 @@
import { openNotificationLogModal } from "@api/Notifications/notificationLog"; import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/settings"; 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 ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
@ -43,7 +43,6 @@ function VencordSettings() {
fallbackValue: "Loading..." fallbackValue: "Loading..."
}); });
const settings = useSettings(); const settings = useSettings();
const notifSettings = settings.notifications;
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []); const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
@ -158,8 +157,16 @@ function VencordSettings() {
</Forms.FormSection> </Forms.FormSection>
{typeof Notification !== "undefined" && <NotificationSection settings={settings.notifications} />}
</React.Fragment>
);
}
function NotificationSection({ settings }: { settings: typeof Settings["notifications"]; }) {
return (
<>
<Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle> <Forms.FormTitle tag="h5">Notification Style</Forms.FormTitle>
{notifSettings.useNative !== "never" && Notification.permission === "denied" && ( {settings.useNative !== "never" && Notification?.permission === "denied" && (
<ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}> <ErrorCard style={{ padding: "1em" }} className={Margins.bottom8}>
<Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle> <Forms.FormTitle tag="h5">Desktop Notification Permission denied</Forms.FormTitle>
<Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText> <Forms.FormText>You have denied Notification Permissions. Thus, Desktop notifications will not work!</Forms.FormText>
@ -178,35 +185,35 @@ function VencordSettings() {
{ label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true }, { label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true },
{ label: "Always use Desktop notifications", value: "always" }, { label: "Always use Desktop notifications", value: "always" },
{ label: "Always use Vencord notifications", value: "never" }, { label: "Always use Vencord notifications", value: "never" },
] satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record<string, any>>} ] satisfies Array<{ value: typeof settings["useNative"]; } & Record<string, any>>}
closeOnSelect={true} closeOnSelect={true}
select={v => notifSettings.useNative = v} select={v => settings.useNative = v}
isSelected={v => v === notifSettings.useNative} isSelected={v => v === settings.useNative}
serialize={identity} serialize={identity}
/> />
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle> <Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Position</Forms.FormTitle>
<Select <Select
isDisabled={notifSettings.useNative === "always"} isDisabled={settings.useNative === "always"}
placeholder="Notification Position" placeholder="Notification Position"
options={[ options={[
{ label: "Bottom Right", value: "bottom-right", default: true }, { label: "Bottom Right", value: "bottom-right", default: true },
{ label: "Top Right", value: "top-right" }, { label: "Top Right", value: "top-right" },
] satisfies Array<{ value: typeof settings["notifications"]["position"]; } & Record<string, any>>} ] satisfies Array<{ value: typeof settings["position"]; } & Record<string, any>>}
select={v => notifSettings.position = v} select={v => settings.position = v}
isSelected={v => v === notifSettings.position} isSelected={v => v === settings.position}
serialize={identity} serialize={identity}
/> />
<Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle> <Forms.FormTitle tag="h5" className={Margins.top16 + " " + Margins.bottom8}>Notification Timeout</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText> <Forms.FormText className={Margins.bottom16}>Set to 0s to never automatically time out</Forms.FormText>
<Slider <Slider
disabled={notifSettings.useNative === "always"} disabled={settings.useNative === "always"}
markers={[0, 1000, 2500, 5000, 10_000, 20_000]} markers={[0, 1000, 2500, 5000, 10_000, 20_000]}
minValue={0} minValue={0}
maxValue={20_000} maxValue={20_000}
initialValue={notifSettings.timeout} initialValue={settings.timeout}
onValueChange={v => notifSettings.timeout = v} onValueChange={v => settings.timeout = v}
onValueRender={v => (v / 1000).toFixed(2) + "s"} onValueRender={v => (v / 1000).toFixed(2) + "s"}
onMarkerRender={v => (v / 1000) + "s"} onMarkerRender={v => (v / 1000) + "s"}
stickToMarkers={false} stickToMarkers={false}
@ -222,23 +229,22 @@ function VencordSettings() {
minValue={0} minValue={0}
maxValue={200} maxValue={200}
stickToMarkers={true} stickToMarkers={true}
initialValue={notifSettings.logLimit} initialValue={settings.logLimit}
onValueChange={v => notifSettings.logLimit = v} onValueChange={v => settings.logLimit = v}
onValueRender={v => v === 200 ? "∞" : v} onValueRender={v => v === 200 ? "∞" : v}
onMarkerRender={v => v === 200 ? "∞" : v} onMarkerRender={v => v === 200 ? "∞" : v}
/> />
<Button <Button
onClick={openNotificationLogModal} onClick={openNotificationLogModal}
disabled={notifSettings.logLimit === 0} disabled={settings.logLimit === 0}
> >
Open Notification Log Open Notification Log
</Button> </Button>
</React.Fragment> </>
); );
} }
interface DonateCardProps { interface DonateCardProps {
image: string; image: string;
} }

View file

@ -21,6 +21,7 @@ import "./settingsStyles.css";
import { classNameFactory } from "@api/Styles"; import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { handleComponentFailed } from "@components/handleComponentFailed"; import { handleComponentFailed } from "@components/handleComponentFailed";
import { isMobile } from "@utils/misc";
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common"; import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
import BackupRestoreTab from "./BackupRestoreTab"; import BackupRestoreTab from "./BackupRestoreTab";
@ -55,7 +56,10 @@ if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater /
function Settings(props: SettingsProps) { function Settings(props: SettingsProps) {
const { tab = "VencordSettings" } = props; const { tab = "VencordSettings" } = props;
const CurrentTab = SettingsTabs[tab]?.component; const CurrentTab = SettingsTabs[tab]?.component ?? null;
if (isMobile) {
return CurrentTab && <CurrentTab />;
}
return <Forms.FormSection> return <Forms.FormSection>
<Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">Vencord Settings</Text> <Text variant="heading-lg/semibold" style={{ color: "var(--header-primary)" }} tag="h2">Vencord Settings</Text>

View file

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>QuickCss Editor</title> <title>Vencord QuickCSS Editor</title>
<link rel="stylesheet" data-name="vs/editor/editor.main" <link rel="stylesheet" data-name="vs/editor/editor.main"
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css"> href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css">
<style> <style>

View file

@ -93,7 +93,7 @@ export function initIpc(mainWindow: BrowserWindow) {
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
const win = new BrowserWindow({ const win = new BrowserWindow({
title: "QuickCss Editor", title: "Vencord QuickCSS Editor",
autoHideMenuBar: true, autoHideMenuBar: true,
darkTheme: true, darkTheme: true,
webPreferences: { webPreferences: {

View file

@ -1,6 +1,6 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors * Copyright (c) 2023 Vendicated and contributors
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -20,16 +20,18 @@ import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
export default definePlugin({ export default definePlugin({
name: "MuteNewGuild", name: "AlwaysAnimate",
description: "Mutes newly joined guilds", description: "Animates anything that can be animated, besides status emojis.",
authors: [Devs.Glitch], authors: [Devs.FieryFlames],
patches: [ patches: [
{ {
find: ",acceptInvite:function", find: ".canAnimate",
all: true,
replacement: { replacement: {
match: /(\w=null!==[^;]+)/, match: /\.canAnimate\b/g,
replace: "$1;Vencord.Webpack.findByProps('updateGuildNotificationSettings').updateGuildNotificationSettings($1,{'muted':true,'suppress_everyone':true,'suppress_roles':true})" replace: ".canAnimate || true"
} }
} }
], ]
}); });

View file

@ -35,7 +35,7 @@ const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/attachments/10336802034336
const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString()); const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString());
const ContributorBadge: ProfileBadge = { const ContributorBadge: ProfileBadge = {
tooltip: "Vencord Contributor", description: "Vencord Contributor",
image: CONTRIBUTOR_BADGE, image: CONTRIBUTOR_BADGE,
position: BadgePosition.START, position: BadgePosition.START,
props: { props: {
@ -45,10 +45,10 @@ const ContributorBadge: ProfileBadge = {
} }
}, },
shouldShow: ({ user }) => contributorIds.includes(user.id), shouldShow: ({ user }) => contributorIds.includes(user.id),
onClick: () => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord") link: "https://github.com/Vendicated/Vencord"
}; };
const DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "tooltip">>; const DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "description">>;
export default definePlugin({ export default definePlugin({
name: "BadgeAPI", name: "BadgeAPI",
@ -56,26 +56,27 @@ export default definePlugin({
authors: [Devs.Megu, Devs.Ven, Devs.TheSun], authors: [Devs.Megu, Devs.Ven, Devs.TheSun],
required: true, required: true,
patches: [ patches: [
/* Patch the badges array */
{
find: "Messages.PROFILE_USER_BADGES,",
replacement: {
match: /&&((\i)\.push\({tooltip:\i\.\i\.Messages\.PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP\.format.+?;)(?:return\s\i;?})/,
replace: (_, m, badgeArray) => `&&${m} return Vencord.Api.Badges.inject(${badgeArray}, arguments[0]);}`,
}
},
/* Patch the badge list component on user profiles */ /* Patch the badge list component on user profiles */
{ {
find: "Messages.PROFILE_USER_BADGES,role:", find: "Messages.PROFILE_USER_BADGES,role:",
replacement: [ replacement: [
{ {
match: /src:(\i)\[(\i)\.key\],/g, match: /null==\i\?void 0:(\i)\.getBadges\(\)/,
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} /> replace: (_, badgesMod) => `Vencord.Api.Badges._getBadges(arguments[0]).concat(${badgesMod}?.getBadges()??[])`,
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,` },
{
// alt: "", aria-hidden: false, src: originalSrc
match: /alt:" ","aria-hidden":!0,src:(?=(\i)\.src)/g,
// ...badge.props, ..., src: badge.image ?? ...
replace: "...$1.props,$& $1.image??"
}, },
{ {
match: /children:function(?<=(\i)\.(?:tooltip|description),spacing:\d.+?)/g, match: /children:function(?<=(\i)\.(?:tooltip|description),spacing:\d.+?)/g,
replace: "children:$1.component ? () => $self.renderBadgeComponent($1) : function" replace: "children:$1.component ? () => $self.renderBadgeComponent($1) : function"
},
{
match: /onClick:function(?=.{0,200}href:(\i)\.link)/,
replace: "onClick:$1.onClick??function"
} }
] ]
} }
@ -95,15 +96,15 @@ export default definePlugin({
return; return;
} }
for (const line of lines) { for (const line of lines) {
const [id, tooltip, image] = line.split(","); const [id, description, image] = line.split(",");
DonorBadges[id] = { image, tooltip }; DonorBadges[id] = { image, description };
} }
}, },
addDonorBadge(badges: ProfileBadge[], userId: string) { getDonorBadge(userId: string) {
const badge = DonorBadges[userId]; const badge = DonorBadges[userId];
if (badge) { if (badge) {
badges.unshift({ return {
...badge, ...badge,
position: BadgePosition.START, position: BadgePosition.START,
props: { props: {
@ -167,7 +168,7 @@ export default definePlugin({
</ErrorBoundary> </ErrorBoundary>
)); ));
}, },
}); };
} }
} }
}); });

View file

@ -23,6 +23,8 @@ export default definePlugin({
name: "ContextMenuAPI", name: "ContextMenuAPI",
description: "API for adding/removing items to/from context menus.", description: "API for adding/removing items to/from context menus.",
authors: [Devs.Nuckyz, Devs.Ven], authors: [Devs.Nuckyz, Devs.Ven],
required: true,
patches: [ patches: [
{ {
find: "♫ (つ。◕‿‿◕。)つ ♪", find: "♫ (つ。◕‿‿◕。)つ ♪",

View file

@ -18,10 +18,12 @@
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { relaunch } from "@utils/native"; import { relaunch } from "@utils/native";
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { extract, filters, findAll, search } from "@webpack"; import { extract, filters, findAll, search } from "@webpack";
import { React } from "@webpack/common"; import { React, ReactDOM } from "@webpack/common";
import type { ComponentType } from "react";
const WEB_ONLY = (f: string) => () => { const WEB_ONLY = (f: string) => () => {
throw new Error(`'${f}' is Discord Desktop only.`); throw new Error(`'${f}' is Discord Desktop only.`);
@ -59,6 +61,7 @@ export default definePlugin({
}; };
} }
let fakeRenderWin: WeakRef<Window> | undefined;
return { return {
wp: Vencord.Webpack, wp: Vencord.Webpack,
wpc: Webpack.wreq.c, wpc: Webpack.wreq.c,
@ -79,7 +82,18 @@ export default definePlugin({
Settings: Vencord.Settings, Settings: Vencord.Settings,
Api: Vencord.Api, Api: Vencord.Api,
reload: () => location.reload(), reload: () => location.reload(),
restart: IS_WEB ? WEB_ONLY("restart") : relaunch restart: IS_WEB ? WEB_ONLY("restart") : relaunch,
canonicalizeMatch,
canonicalizeReplace,
canonicalizeReplacement,
fakeRender: (component: ComponentType, props: any) => {
const prevWin = fakeRenderWin?.deref();
const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!;
fakeRenderWin = new WeakRef(win);
win.focus();
ReactDOM.render(React.createElement(component, props), win.document.body);
}
}; };
}, },

View file

@ -102,7 +102,8 @@ function initWs(isManual = false) {
(settings.store.notifyOnAutoConnect || isManual) && showNotification({ (settings.store.notifyOnAutoConnect || isManual) && showNotification({
title: "Dev Companion Connected", title: "Dev Companion Connected",
body: "Connected to WebSocket" body: "Connected to WebSocket",
noPersist: true
}); });
}); });
@ -237,10 +238,8 @@ function initWs(isManual = false) {
}); });
} }
const contextMenuPatch: NavContextMenuPatchCallback = kids => { const contextMenuPatch: NavContextMenuPatchCallback = children => () => {
if (kids.some(k => k?.props?.id === NAV_ID)) return; children.unshift(
kids.unshift(
<Menu.MenuItem <Menu.MenuItem
id={NAV_ID} id={NAV_ID}
label="Reconnect Dev Companion" label="Reconnect Dev Companion"
@ -256,7 +255,6 @@ export default definePlugin({
name: "DevCompanion", name: "DevCompanion",
description: "Dev Companion Plugin", description: "Dev Companion Plugin",
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["ContextMenuAPI"],
settings, settings,
start() { start() {

View file

@ -210,7 +210,7 @@ function isGifUrl(url: string) {
return new URL(url).pathname.endsWith(".gif"); return new URL(url).pathname.endsWith(".gif");
} }
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {}; const { favoriteableId, itemHref, itemSrc, favoriteableType } = props ?? {};
if (!favoriteableId || favoriteableType !== "emoji") return; if (!favoriteableId || favoriteableType !== "emoji") return;
@ -220,17 +220,15 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =
const name = match[1] ?? "FakeNitroEmoji"; const name = match[1] ?? "FakeNitroEmoji";
const group = findGroupChildrenByChildId("copy-link", children); const group = findGroupChildrenByChildId("copy-link", children);
if (group && !group.some(child => child?.props?.id === "emote-cloner")) if (group) group.push(buildMenuItem(favoriteableId, name, isGifUrl(itemHref ?? itemSrc)));
group.push(buildMenuItem(favoriteableId, name, isGifUrl(itemHref ?? itemSrc)));
}; };
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => { const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => {
const { id, name, type } = props?.target?.dataset ?? {}; const { id, name, type } = props?.target?.dataset ?? {};
if (!id || !name || type !== "emoji") return; if (!id || !name || type !== "emoji") return;
const firstChild = props.target.firstChild as HTMLImageElement; const firstChild = props.target.firstChild as HTMLImageElement;
if (!children.some(c => c?.props?.id === "emote-cloner"))
children.push(buildMenuItem(id, name, firstChild && isGifUrl(firstChild.src))); children.push(buildMenuItem(id, name, firstChild && isGifUrl(firstChild.src)));
}; };
@ -238,7 +236,6 @@ export default definePlugin({
name: "EmoteCloner", name: "EmoteCloner",
description: "Adds a Clone context menu item to emotes to clone them your own server", description: "Adds a Clone context menu item to emotes to clone them your own server",
authors: [Devs.Ven, Devs.Nuckyz], authors: [Devs.Ven, Devs.Nuckyz],
dependencies: ["ContextMenuAPI"],
start() { start() {
addContextMenuPatch("message", messageContextMenuPatch); addContextMenuPatch("message", messageContextMenuPatch);

View file

@ -26,6 +26,7 @@ import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack"; import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
import { ChannelStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common"; import { ChannelStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common";
import type { Message } from "discord-types/general"; import type { Message } from "discord-types/general";
import type { ReactNode } from "react";
const DRAFT_TYPE = 0; const DRAFT_TYPE = 0;
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR"); const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
@ -135,6 +136,11 @@ const settings = definePluginSettings({
default: true, default: true,
restartNeeded: true restartNeeded: true
}, },
transformCompoundSentence: {
description: "Whether to transform fake stickers and emojis in compound sentences (sentences with more content than just the fake emoji or sticker link)",
type: OptionType.BOOLEAN,
default: false
},
enableStreamQualityBypass: { enableStreamQualityBypass: {
description: "Allow streaming in nitro quality", description: "Allow streaming in nitro quality",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
@ -289,8 +295,8 @@ export default definePlugin({
replace: (m, renderableSticker) => `${m}renderableSticker:${renderableSticker},` replace: (m, renderableSticker) => `${m}renderableSticker:${renderableSticker},`
}, },
{ {
match: /emojiSection.{0,50}description:\i(?<=(\i)\.sticker,.+?)(?=,)/, match: /(emojiSection.{0,50}description:)(\i)(?<=(\i)\.sticker,.+?)(?=,)/,
replace: (m, props) => `${m}+(${props}.renderableSticker?.fake?" This is a Fake Nitro sticker. Only you can see it rendered like a real one, for non Vencord users it will show as a link.":"")` replace: (_, rest, reactNode, props) => `${rest}$self.addFakeNotice("STICKER",${reactNode},!!${props}.renderableSticker?.fake)`
} }
] ]
}, },
@ -299,50 +305,11 @@ export default definePlugin({
predicate: () => settings.store.transformEmojis, predicate: () => settings.store.transformEmojis,
replacement: { replacement: {
match: /((\i)=\i\.node,\i=\i\.emojiSourceDiscoverableGuild)(.+?return )(.{0,450}Messages\.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION.+?}\))/, match: /((\i)=\i\.node,\i=\i\.emojiSourceDiscoverableGuild)(.+?return )(.{0,450}Messages\.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION.+?}\))/,
replace: (_, rest1, node, rest2, messages) => `${rest1},fakeNitroNode=${node}${rest2}(${messages})+(fakeNitroNode.fake?" This is a Fake Nitro emoji. Only you can see it rendered like a real one, for non Vencord users it will show as a link.":"")` replace: (_, rest1, node, rest2, reactNode) => `${rest1},fakeNitroNode=${node}${rest2}$self.addFakeNotice("EMOJI",${reactNode},fakeNitroNode.fake)`
} }
} }
], ],
options: {
enableEmojiBypass: {
description: "Allow sending fake emojis",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true,
},
emojiSize: {
description: "Size of the emojis when sending",
type: OptionType.SLIDER,
default: 48,
markers: [32, 48, 64, 128, 160, 256, 512],
},
transformEmojis: {
description: "Whether to transform fake emojis into real ones",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true,
},
enableStickerBypass: {
description: "Allow sending fake stickers",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true,
},
stickerSize: {
description: "Size of the stickers when sending",
type: OptionType.SLIDER,
default: 160,
markers: [32, 64, 128, 160, 256, 512],
},
enableStreamQualityBypass: {
description: "Allow streaming in nitro quality",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true,
}
},
get guildId() { get guildId() {
return getCurrentGuild()?.id; return getCurrentGuild()?.id;
}, },
@ -418,7 +385,7 @@ export default definePlugin({
}, },
patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) { patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {
if (content.length > 1) return content; if (content.length > 1 && !settings.store.transformCompoundSentence) return content;
const newContent: Array<any> = []; const newContent: Array<any> = [];
@ -441,7 +408,7 @@ export default definePlugin({
const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji"; const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji";
newContent.push(Parser.defaultRules.customEmoji.react({ newContent.push(Parser.defaultRules.customEmoji.react({
jumboable: !inline, jumboable: !inline && content.length === 1,
animated: fakeNitroMatch[2] === "gif", animated: fakeNitroMatch[2] === "gif",
emojiId: fakeNitroMatch[1], emojiId: fakeNitroMatch[1],
name: emojiName, name: emojiName,
@ -465,8 +432,8 @@ export default definePlugin({
newContent.push(element); newContent.push(element);
} }
const firstTextElementIdx = newContent.findIndex(element => typeof element === "string"); const firstContent = newContent[0];
if (firstTextElementIdx !== -1) newContent[firstTextElementIdx] = newContent[firstTextElementIdx].trimStart(); if (typeof firstContent === "string") newContent[0] = firstContent.trimStart();
return newContent; return newContent;
}, },
@ -475,7 +442,8 @@ export default definePlugin({
const itemsToMaybePush: Array<string> = []; const itemsToMaybePush: Array<string> = [];
const contentItems = message.content.split(/\s/); const contentItems = message.content.split(/\s/);
if (contentItems.length === 1) itemsToMaybePush.push(contentItems[0]); if (contentItems.length === 1 && !settings.store.transformCompoundSentence) itemsToMaybePush.push(contentItems[0]);
else itemsToMaybePush.push(...contentItems);
itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url)); itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url));
@ -516,7 +484,7 @@ export default definePlugin({
}, },
shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) { shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) {
if (message.content.split(/\s/).length > 1) return false; if (message.content.split(/\s/).length > 1 && !settings.store.transformCompoundSentence) return false;
switch (embed.type) { switch (embed.type) {
case "image": { case "image": {
@ -559,6 +527,25 @@ export default definePlugin({
return link.target && fakeNitroEmojiRegex.test(link.target); return link.target && fakeNitroEmojiRegex.test(link.target);
}, },
addFakeNotice(type: "STICKER" | "EMOJI", node: Array<ReactNode>, fake: boolean) {
if (!fake) return node;
node = Array.isArray(node) ? node : [node];
switch (type) {
case "STICKER": {
node.push(" This is a FakeNitro sticker and renders like a real sticker only for you. Appears as a link to non-plugin users.");
return node;
}
case "EMOJI": {
node.push(" This is a FakeNitro emoji and renders like a real emoji only for you. Appears as a link to non-plugin users.");
return node;
}
}
},
hasPermissionToUseExternalEmojis(channelId: string) { hasPermissionToUseExternalEmojis(channelId: string) {
const channel = ChannelStore.getChannel(channelId); const channel = ChannelStore.getChannel(channelId);

View file

@ -83,7 +83,7 @@ const settings = definePluginSettings({
export default definePlugin({ export default definePlugin({
name: "FakeProfileThemes", name: "FakeProfileThemes",
description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding.", description: "Allows profile theming by hiding the colors in your bio thanks to invisible 3y3 encoding",
authors: [Devs.Alyxia, Devs.Remty], authors: [Devs.Alyxia, Devs.Remty],
patches: [ patches: [
{ {
@ -112,7 +112,7 @@ export default definePlugin({
<li> click the "Copy 3y3" button</li> <li> click the "Copy 3y3" button</li>
<li> paste the invisible text anywhere in your bio</li> <li> paste the invisible text anywhere in your bio</li>
</ul><br /> </ul><br />
<b>Please note:</b> if you are using a theme which hides nitro upsells, you should disable it temporarily to set colors. <b>Please note:</b> if you are using a theme which hides nitro ads, you should disable it temporarily to set colors.
</Forms.FormText> </Forms.FormText>
</Forms.FormSection>), </Forms.FormSection>),
settings, settings,

View file

@ -47,12 +47,16 @@ export default definePlugin({
body: { body: {
modified_contacts: { modified_contacts: {
[random]: [1, "", ""] [random]: [1, "", ""]
} },
phone_contact_methods_count: 1
} }
}).then(res => }).then(res =>
FriendInvites.createFriendInvite({ FriendInvites.createFriendInvite({
code: res.body.invite_suggestions[0][3], code: res.body.invite_suggestions[0][3],
recipient_phone_number_or_email: random recipient_phone_number_or_email: random,
contact_visibility: 1,
filter_visibilities: [],
filtered_invite_suggestions_index: 1
}) })
); );

View file

@ -48,7 +48,7 @@ function GameActivityToggleButton() {
return ( return (
<Button <Button
tooltipText="Toggle Game Activity" tooltipText={showCurrentGame ? "Disable Game Activity" : "Enable Game Activity"}
icon={makeIcon(showCurrentGame)} icon={makeIcon(showCurrentGame)}
role="switch" role="switch"
aria-checked={!showCurrentGame} aria-checked={!showCurrentGame}

View file

@ -17,13 +17,13 @@
*/ */
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { insertTextIntoChatInputBox } from "@utils/discord";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { filters, findLazy, mapMangledModuleLazy } from "@webpack"; import { filters, mapMangledModuleLazy } from "@webpack";
const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', { const ExpressionPickerState = mapMangledModuleLazy('name:"expression-picker-last-active-view"', {
close: filters.byCode("activeView:null", "setState") close: filters.byCode("activeView:null", "setState")
}); });
const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
export default definePlugin({ export default definePlugin({
name: "GifPaste", name: "GifPaste",
@ -40,7 +40,7 @@ export default definePlugin({
handleSelect(gif?: { url: string; }) { handleSelect(gif?: { url: string; }) {
if (gif) { if (gif) {
ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: gif.url + " " }); insertTextIntoChatInputBox(gif.url + " ");
ExpressionPickerState.close(); ExpressionPickerState.close();
} }
} }

View file

@ -88,7 +88,7 @@ function ToggleIconOn({ forceWhite }: { forceWhite?: boolean; }) {
); );
} }
function ToggleActivityComponent({ activity, forceWhite }: { activity: IgnoredActivity; forceWhite?: boolean; }) { function ToggleActivityComponent({ activity, forceWhite, forceLeftMargin }: { activity: IgnoredActivity; forceWhite?: boolean; forceLeftMargin?: boolean; }) {
const forceUpdate = useForceUpdater(); const forceUpdate = useForceUpdater();
return ( return (
@ -101,6 +101,7 @@ function ToggleActivityComponent({ activity, forceWhite }: { activity: IgnoredAc
role="button" role="button"
aria-label="Toggle activity" aria-label="Toggle activity"
tabIndex={0} tabIndex={0}
style={forceLeftMargin ? { marginLeft: "2px" } : undefined}
onClick={e => handleActivityToggle(e, activity, forceUpdate)} onClick={e => handleActivityToggle(e, activity, forceUpdate)}
> >
{ {
@ -200,7 +201,7 @@ export default definePlugin({
renderToggleGameActivityButton(props: { id?: string; exePath: string; }) { renderToggleGameActivityButton(props: { id?: string; exePath: string; }) {
return ( return (
<ErrorBoundary noop> <ErrorBoundary noop>
<ToggleActivityComponent activity={{ id: props.id ?? props.exePath, type: ActivitiesTypes.Game }} /> <ToggleActivityComponent activity={{ id: props.id ?? props.exePath, type: ActivitiesTypes.Game }} forceLeftMargin={true} />
</ErrorBoundary> </ErrorBoundary>
); );
}, },

View file

@ -36,7 +36,6 @@ export interface MagnifierProps {
export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSize, zoom: initalZoom }) => { export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSize, zoom: initalZoom }) => {
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 }); const [lensPosition, setLensPosition] = useState<Vec2>({ x: 0, y: 0 });
const [imagePosition, setImagePosition] = useState<Vec2>({ x: 0, y: 0 }); const [imagePosition, setImagePosition] = useState<Vec2>({ x: 0, y: 0 });
const [opacity, setOpacity] = useState(0); const [opacity, setOpacity] = useState(0);
@ -157,7 +156,7 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
return ( return (
<div <div
className="lens" className="vc-imgzoom-lens"
style={{ style={{
opacity, opacity,
width: size.current + "px", width: size.current + "px",
@ -190,7 +189,8 @@ export const Magnifier: React.FC<MagnifierProps> = ({ instance, size: initialSiz
}} }}
width={`${box.width * zoom.current}px`} width={`${box.width * zoom.current}px`}
height={`${box.height * zoom.current}px`} height={`${box.height * zoom.current}px`}
src={instance.props.src} alt="" src={instance.props.src}
alt=""
/> />
)} )}
</div> </div>

View file

@ -16,4 +16,4 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export const ELEMENT_ID = "magnify-modal"; export const ELEMENT_ID = "vc-imgzoom-magnify-modal";

View file

@ -16,10 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import "./styles.css";
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { definePluginSettings } from "@api/settings"; import { definePluginSettings } from "@api/settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { makeRange } from "@components/PluginSettings/components"; import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { debounce } from "@utils/debounce"; import { debounce } from "@utils/debounce";
@ -29,6 +28,7 @@ import type { Root } from "react-dom/client";
import { Magnifier, MagnifierProps } from "./components/Magnifier"; import { Magnifier, MagnifierProps } from "./components/Magnifier";
import { ELEMENT_ID } from "./constants"; import { ELEMENT_ID } from "./constants";
import styles from "./styles.css?managed";
export const settings = definePluginSettings({ export const settings = definePluginSettings({
saveZoomValues: { saveZoomValues: {
@ -75,8 +75,7 @@ export const settings = definePluginSettings({
}); });
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, _) => { const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => {
if (!children.some(child => child?.props?.id === "image-zoom")) {
children.push( children.push(
<Menu.MenuGroup id="image-zoom"> <Menu.MenuGroup id="image-zoom">
{/* thanks SpotifyControls */} {/* thanks SpotifyControls */}
@ -125,7 +124,6 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = (children, _) => {
/> />
</Menu.MenuGroup> </Menu.MenuGroup>
); );
}
}; };
export default definePlugin({ export default definePlugin({
@ -219,6 +217,7 @@ export default definePlugin({
}, },
start() { start() {
enableStyle(styles);
addContextMenuPatch("image-context", imageContextMenuPatch); addContextMenuPatch("image-context", imageContextMenuPatch);
this.element = document.createElement("div"); this.element = document.createElement("div");
this.element.classList.add("MagnifierContainer"); this.element.classList.add("MagnifierContainer");
@ -226,6 +225,7 @@ export default definePlugin({
}, },
stop() { stop() {
disableStyle(styles);
// so componenetWillUnMount gets called if Magnifier component is still alive // so componenetWillUnMount gets called if Magnifier component is still alive
this.root && this.root.unmount(); this.root && this.root.unmount();
this.element?.remove(); this.element?.remove();

View file

@ -1,4 +1,4 @@
.lens { .vc-imgzoom-lens {
position: absolute; position: absolute;
inset: 0; inset: 0;
z-index: 9999; z-index: 9999;
@ -11,21 +11,21 @@
pointer-events: none; pointer-events: none;
} }
.zoom img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* make the carousel take up less space so we can click the backdrop and exit out of it */ /* make the carousel take up less space so we can click the backdrop and exit out of it */
[class^="focusLock"] > [class^="carouselModal"] { [class|="carouselModal"] {
height: fit-content; height: fit-content;
box-shadow: none; box-shadow: none;
} }
[class^="focusLock"] > [class^="carouselModal"] > div { [class*="modalCarouselWrapper"] {
height: fit-content; height: fit-content;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
} }
[class|="wrapper"]:has(> #vc-imgzoom-magnify-modal) {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

View file

@ -20,6 +20,8 @@ import { registerCommand, unregisterCommand } from "@api/Commands";
import { Settings } from "@api/settings"; import { Settings } from "@api/settings";
import Logger from "@utils/Logger"; import Logger from "@utils/Logger";
import { Patch, Plugin } from "@utils/types"; import { Patch, Plugin } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types";
import Plugins from "~plugins"; import Plugins from "~plugins";
@ -111,56 +113,64 @@ export function startDependenciesRecursive(p: Plugin) {
} }
export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) { export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) {
const { name, commands, flux } = p;
if (p.start) { if (p.start) {
logger.info("Starting plugin", p.name); logger.info("Starting plugin", name);
if (p.started) { if (p.started) {
logger.warn(`${p.name} already started`); logger.warn(`${name} already started`);
return false; return false;
} }
try { try {
p.start(); p.start();
p.started = true; p.started = true;
} catch (e) { } catch (e) {
logger.error(`Failed to start ${p.name}\n`, e); logger.error(`Failed to start ${name}\n`, e);
return false; return false;
} }
} }
if (p.commands?.length) { if (commands?.length) {
logger.info("Registering commands of plugin", p.name); logger.info("Registering commands of plugin", name);
for (const cmd of p.commands) { for (const cmd of commands) {
try { try {
registerCommand(cmd, p.name); registerCommand(cmd, name);
} catch (e) { } catch (e) {
logger.error(`Failed to register command ${cmd.name}\n`, e); logger.error(`Failed to register command ${cmd.name}\n`, e);
return false; return false;
} }
} }
}
if (flux) {
for (const event in flux) {
FluxDispatcher.subscribe(event as FluxEvents, flux[event]);
}
} }
return true; return true;
}, p => `startPlugin ${p.name}`); }, p => `startPlugin ${p.name}`);
export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) { export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plugin) {
const { name, commands, flux } = p;
if (p.stop) { if (p.stop) {
logger.info("Stopping plugin", p.name); logger.info("Stopping plugin", name);
if (!p.started) { if (!p.started) {
logger.warn(`${p.name} already stopped`); logger.warn(`${name} already stopped`);
return false; return false;
} }
try { try {
p.stop(); p.stop();
p.started = false; p.started = false;
} catch (e) { } catch (e) {
logger.error(`Failed to stop ${p.name}\n`, e); logger.error(`Failed to stop ${name}\n`, e);
return false; return false;
} }
} }
if (p.commands?.length) { if (commands?.length) {
logger.info("Unregistering commands of plugin", p.name); logger.info("Unregistering commands of plugin", name);
for (const cmd of p.commands) { for (const cmd of commands) {
try { try {
unregisterCommand(cmd.name); unregisterCommand(cmd.name);
} catch (e) { } catch (e) {
@ -170,5 +180,11 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
} }
} }
if (flux) {
for (const event in flux) {
FluxDispatcher.unsubscribe(event as FluxEvents, flux[event]);
}
}
return true; return true;
}, p => `stopPlugin ${p.name}`); }, p => `stopPlugin ${p.name}`);

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { insertTextIntoChatInputBox } from "@utils/discord";
import { import {
ModalContent, ModalContent,
ModalFooter, ModalFooter,
@ -24,13 +25,10 @@ import {
ModalRoot, ModalRoot,
openModal, openModal,
} from "@utils/modal"; } from "@utils/modal";
import { findLazy } from "@webpack";
import { Button, Forms, React, Switch, TextInput } from "@webpack/common"; import { Button, Forms, React, Switch, TextInput } from "@webpack/common";
import { encrypt } from "../index"; import { encrypt } from "../index";
const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
function EncModal(props: ModalProps) { function EncModal(props: ModalProps) {
const [secret, setSecret] = React.useState(""); const [secret, setSecret] = React.useState("");
const [cover, setCover] = React.useState(""); const [cover, setCover] = React.useState("");
@ -87,9 +85,7 @@ function EncModal(props: ModalProps) {
const toSend = noCover ? encrypted.replaceAll("d", "") : encrypted; const toSend = noCover ? encrypted.replaceAll("d", "") : encrypted;
if (!toSend) return; if (!toSend) return;
ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { insertTextIntoChatInputBox(toSend);
rawText: `${toSend}`
});
props.onClose(); props.onClose();
}} }}

View file

@ -85,7 +85,7 @@ function ChatBarIcon() {
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
innerClassName={ButtonWrapperClasses.button} innerClassName={ButtonWrapperClasses.button}
onClick={() => buildEncModal()} onClick={() => buildEncModal()}
style={{ marginRight: "2px" }} style={{ padding: "0 4px" }}
> >
<div className={ButtonWrapperClasses.buttonWrapper}> <div className={ButtonWrapperClasses.buttonWrapper}>
<svg <svg

View file

@ -19,7 +19,7 @@
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { ChannelStore, FluxDispatcher, NavigationRouter, SelectedChannelStore, SelectedGuildStore } from "@webpack/common"; import { ChannelStore, NavigationRouter, SelectedChannelStore, SelectedGuildStore } from "@webpack/common";
export interface LogoutEvent { export interface LogoutEvent {
type: "LOGOUT"; type: "LOGOUT";
@ -37,63 +37,54 @@ interface PreviousChannel {
channelId: string | null; channelId: string | null;
} }
let isSwitchingAccount = false;
let previousCache: PreviousChannel | undefined;
function attemptToNavigateToChannel(guildId: string | null, channelId: string) {
if (!ChannelStore.hasChannel(channelId)) return;
NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${channelId}`);
}
export default definePlugin({ export default definePlugin({
name: "KeepCurrentChannel", name: "KeepCurrentChannel",
description: "Attempt to navigate to the channel you were in before switching accounts or loading Discord.", description: "Attempt to navigate to the channel you were in before switching accounts or loading Discord.",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz],
isSwitchingAccount: false, flux: {
previousCache: {} as PreviousChannel, LOGOUT(e: LogoutEvent) {
({ isSwitchingAccount } = e);
attemptToNavigateToChannel(guildId: string | null, channelId: string) {
if (!ChannelStore.hasChannel(channelId)) return;
NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${channelId}`);
}, },
onLogout(e: LogoutEvent) { CONNECTION_OPEN() {
this.isSwitchingAccount = e.isSwitchingAccount; if (!isSwitchingAccount) return;
isSwitchingAccount = false;
if (previousCache?.channelId)
attemptToNavigateToChannel(previousCache.guildId, previousCache.channelId);
}, },
onConnectionOpen() { async CHANNEL_SELECT({ guildId, channelId }: ChannelSelectEvent) {
if (!this.isSwitchingAccount) return; if (isSwitchingAccount) return;
this.isSwitchingAccount = false;
if (this.previousCache.channelId) this.attemptToNavigateToChannel(this.previousCache.guildId, this.previousCache.channelId); previousCache = {
},
async onChannelSelect({ guildId, channelId }: ChannelSelectEvent) {
if (this.isSwitchingAccount) return;
this.previousCache = {
guildId, guildId,
channelId channelId
}; };
await DataStore.set("KeepCurrentChannel_previousData", this.previousCache); await DataStore.set("KeepCurrentChannel_previousData", previousCache);
}
}, },
async start() { async start() {
const previousData = await DataStore.get<PreviousChannel>("KeepCurrentChannel_previousData"); previousCache = await DataStore.get<PreviousChannel>("KeepCurrentChannel_previousData");
if (previousData) { if (!previousCache) {
this.previousCache = previousData; previousCache = {
if (this.previousCache.channelId) this.attemptToNavigateToChannel(this.previousCache.guildId, this.previousCache.channelId);
} else {
this.previousCache = {
guildId: SelectedGuildStore.getGuildId(), guildId: SelectedGuildStore.getGuildId(),
channelId: SelectedChannelStore.getChannelId() ?? null channelId: SelectedChannelStore.getChannelId() ?? null
}; };
await DataStore.set("KeepCurrentChannel_previousData", this.previousCache);
await DataStore.set("KeepCurrentChannel_previousData", previousCache);
} else if (previousCache.channelId) {
attemptToNavigateToChannel(previousCache.guildId, previousCache.channelId);
} }
FluxDispatcher.subscribe("LOGOUT", this.onLogout.bind(this));
FluxDispatcher.subscribe("CONNECTION_OPEN", this.onConnectionOpen.bind(this));
FluxDispatcher.subscribe("CHANNEL_SELECT", this.onChannelSelect.bind(this));
},
stop() {
FluxDispatcher.unsubscribe("LOGOUT", this.onLogout);
FluxDispatcher.unsubscribe("CONNECTION_OPEN", this.onConnectionOpen);
FluxDispatcher.unsubscribe("CHANNEL_SELECT", this.onChannelSelect);
} }
}); });

View file

@ -22,11 +22,14 @@ import { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord"; import { getCurrentChannel } from "@utils/discord";
import { useForceUpdater } from "@utils/misc"; import { useForceUpdater } from "@utils/misc";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { FluxDispatcher, Tooltip } from "@webpack/common"; import { findStoreLazy } from "@webpack";
import { Tooltip } from "@webpack/common";
const counts = {} as Record<string, [number, number]>; const counts = {} as Record<string, [number, number]>;
let forceUpdate: () => void; let forceUpdate: () => void;
const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore");
function MemberCount() { function MemberCount() {
const guildId = getCurrentChannel().guild_id; const guildId = getCurrentChannel().guild_id;
const c = counts[guildId]; const c = counts[guildId];
@ -37,7 +40,8 @@ function MemberCount() {
let total = c[0].toLocaleString(); let total = c[0].toLocaleString();
if (total === "0" && c[1] > 0) { if (total === "0" && c[1] > 0) {
total = "Loading..."; const approx = GuildMemberCountStore.getMemberCount(guildId);
total = approx ? approx.toLocaleString() : "Loading...";
} }
const online = c[1].toLocaleString(); const online = c[1].toLocaleString();
@ -103,7 +107,8 @@ export default definePlugin({
} }
}], }],
onGuildMemberListUpdate({ guildId, groups, memberCount, id }) { flux: {
GUILD_MEMBER_LIST_UPDATE({ guildId, groups, memberCount, id }) {
// eeeeeh - sometimes it has really wrong counts??? like 10 times less than actual // eeeeeh - sometimes it has really wrong counts??? like 10 times less than actual
// but if we only listen to everyone updates, sometimes we never get the count? // but if we only listen to everyone updates, sometimes we never get the count?
// this seems to work but isn't optional // this seems to work but isn't optional
@ -116,14 +121,7 @@ export default definePlugin({
} }
counts[guildId] = [memberCount, count]; counts[guildId] = [memberCount, count];
forceUpdate?.(); forceUpdate?.();
}, }
start() {
FluxDispatcher.subscribe("GUILD_MEMBER_LIST_UPDATE", this.onGuildMemberListUpdate);
},
stop() {
FluxDispatcher.unsubscribe("GUILD_MEMBER_LIST_UPDATE", this.onGuildMemberListUpdate);
}, },
render: () => ( render: () => (

View file

@ -1,3 +1,3 @@
.messagelogger-deleted { .messagelogger-deleted {
background-color: rgba(240 71 71 / 15%); background-color: rgba(240 71 71 / 15%) !important;
} }

View file

@ -1,8 +1,8 @@
.messagelogger-deleted div { .messagelogger-deleted :is(div, h1, h2, h3, p) {
color: #f04747; color: #f04747 !important;
} }
.messagelogger-deleted a { .messagelogger-deleted a {
color: #be3535; color: #be3535 !important;
text-decoration: underline; text-decoration: underline;
} }

View file

@ -43,21 +43,21 @@ function addDeleteStyle() {
} }
} }
const MENU_ITEM_ID = "message-logger-remove-history"; const REMOVE_HISTORY_ID = "ml-remove-history";
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => { const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style";
const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) => () => {
const { message } = props; const { message } = props;
const { deleted, editHistory, id, channel_id } = message; const { deleted, editHistory, id, channel_id } = message;
if (!deleted && !editHistory?.length) return; if (!deleted && !editHistory?.length) return;
if (children.some(c => c?.props?.id === MENU_ITEM_ID)) return;
children.push(( children.push((
<Menu.MenuItem <Menu.MenuItem
id={MENU_ITEM_ID} id={REMOVE_HISTORY_ID}
key={MENU_ITEM_ID} key={REMOVE_HISTORY_ID}
label="Remove Message History" label="Remove Message History"
action={() => { action={() => {
if (message.deleted) { if (deleted) {
FluxDispatcher.dispatch({ FluxDispatcher.dispatch({
type: "MESSAGE_DELETE", type: "MESSAGE_DELETE",
channelId: channel_id, channelId: channel_id,
@ -70,13 +70,26 @@ const patchMessageContextMenu: NavContextMenuPatchCallback = (children, props) =
}} }}
/> />
)); ));
if (!deleted) return;
const domElement = document.getElementById(`chat-messages-${channel_id}-${id}`);
if (!domElement) return;
children.push((
<Menu.MenuItem
id={TOGGLE_DELETE_STYLE_ID}
key={TOGGLE_DELETE_STYLE_ID}
label="Toggle Deleted Highlight"
action={() => domElement.classList.toggle("messagelogger-deleted")}
/>
));
}; };
export default definePlugin({ export default definePlugin({
name: "MessageLogger", name: "MessageLogger",
description: "Temporarily logs deleted and edited messages.", description: "Temporarily logs deleted and edited messages.",
authors: [Devs.rushii, Devs.Ven], authors: [Devs.rushii, Devs.Ven],
dependencies: ["ContextMenuAPI"],
start() { start() {
addDeleteStyle(); addDeleteStyle();

View file

@ -2,15 +2,17 @@
display: none; display: none;
} }
.messagelogger-deleted-attachment, .messagelogger-deleted :is(video, .emoji, [data-type="sticker"]),
.messagelogger-deleted .messagelogger-deleted-attachment,
.messagelogger-deleted div iframe { .messagelogger-deleted div iframe {
filter: grayscale(1); filter: grayscale(1) !important;
transition: 150ms filter ease-in-out; transition: 150ms filter ease-in-out;
} }
.messagelogger-deleted-attachment:hover, .messagelogger-deleted:hover :is(video, .emoji, [data-type="sticker"]),
.messagelogger-deleted div iframe:hover { .messagelogger-deleted .messagelogger-deleted-attachment:hover,
filter: grayscale(0); .messagelogger-deleted iframe:hover {
filter: grayscale(0) !important;
} }
.theme-dark .messagelogger-edited { .theme-dark .messagelogger-edited {

View file

@ -234,12 +234,15 @@ export default definePlugin({
}); });
break; // end 'preview' break; // end 'preview'
} }
}
return sendBotMessage(ctx.channel.id, { default: {
sendBotMessage(ctx.channel.id, {
author, author,
content: "Invalid sub-command" content: "Invalid sub-command"
}); });
break;
}
}
} }
} }
] ]

View file

@ -118,7 +118,7 @@ const settings = definePluginSettings({
export default definePlugin({ export default definePlugin({
name: "MoreUserTags", name: "MoreUserTags",
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)", description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
authors: [Devs.Cyn, Devs.TheSun], authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev],
settings, settings,
patches: [ patches: [
// add tags to the tag list // add tags to the tag list
@ -140,6 +140,11 @@ export default definePlugin({
{ {
match: /(\i)=(\i)===\i\.ORIGINAL_POSTER/, match: /(\i)=(\i)===\i\.ORIGINAL_POSTER/,
replace: "$1=$self.isOPTag($2)" replace: "$1=$self.isOPTag($2)"
},
// add HTML data attributes (for easier theming)
{
match: /children:\[(?=\i,\(0,\i\.jsx\)\("span",{className:\i\(\)\.botText,children:(\i)}\)\])/,
replace: "'data-tag':$1.toLowerCase(),children:["
} }
], ],
}, },

View file

@ -21,7 +21,7 @@ import { makeRange } from "@components/PluginSettings/components/SettingSliderCo
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { sleep } from "@utils/misc"; import { sleep } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher, SelectedChannelStore, UserStore } from "@webpack/common"; import { SelectedChannelStore, UserStore } from "@webpack/common";
import { Message, ReactionEmoji } from "discord-types/general"; import { Message, ReactionEmoji } from "discord-types/general";
interface IMessageCreate { interface IMessageCreate {
@ -80,14 +80,15 @@ export default definePlugin({
description: "🗿🗿🗿🗿🗿🗿🗿🗿", description: "🗿🗿🗿🗿🗿🗿🗿🗿",
settings, settings,
async onMessage(e: IMessageCreate) { flux: {
if (e.optimistic || e.type !== "MESSAGE_CREATE") return; async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) {
if (e.message.state === "SENDING") return; if (optimistic || type !== "MESSAGE_CREATE") return;
if (settings.store.ignoreBots && e.message.author?.bot) return; if (message.state === "SENDING") return;
if (!e.message.content) return; if (settings.store.ignoreBots && message.author?.bot) return;
if (e.channelId !== SelectedChannelStore.getChannelId()) return; if (!message.content) return;
if (channelId !== SelectedChannelStore.getChannelId()) return;
const moyaiCount = getMoyaiCount(e.message.content); const moyaiCount = getMoyaiCount(message.content);
for (let i = 0; i < moyaiCount; i++) { for (let i = 0; i < moyaiCount; i++) {
boom(); boom();
@ -95,35 +96,24 @@ export default definePlugin({
} }
}, },
onReaction(e: IReactionAdd) { MESSAGE_REACTION_ADD({ optimistic, type, channelId, userId, emoji }: IReactionAdd) {
if (e.optimistic || e.type !== "MESSAGE_REACTION_ADD") return; if (optimistic || type !== "MESSAGE_REACTION_ADD") return;
if (settings.store.ignoreBots && UserStore.getUser(e.userId)?.bot) return; if (settings.store.ignoreBots && UserStore.getUser(userId)?.bot) return;
if (e.channelId !== SelectedChannelStore.getChannelId()) return; if (channelId !== SelectedChannelStore.getChannelId()) return;
const name = e.emoji.name.toLowerCase(); const name = emoji.name.toLowerCase();
if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return; if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return;
boom(); boom();
}, },
onVoiceChannelEffect(e: IVoiceChannelEffectSendEvent) { VOICE_CHANNEL_EFFECT_SEND({ emoji }: IVoiceChannelEffectSendEvent) {
if (!e.emoji?.name) return; if (!emoji?.name) return;
const name = e.emoji.name.toLowerCase(); const name = emoji.name.toLowerCase();
if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return; if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return;
boom(); boom();
}, }
start() {
FluxDispatcher.subscribe("MESSAGE_CREATE", this.onMessage);
FluxDispatcher.subscribe("MESSAGE_REACTION_ADD", this.onReaction);
FluxDispatcher.subscribe("VOICE_CHANNEL_EFFECT_SEND", this.onVoiceChannelEffect);
},
stop() {
FluxDispatcher.unsubscribe("MESSAGE_CREATE", this.onMessage);
FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD", this.onReaction);
FluxDispatcher.unsubscribe("VOICE_CHANNEL_EFFECT_SEND", this.onVoiceChannelEffect);
} }
}); });

View file

@ -0,0 +1,80 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import { ModalContent, ModalFooter, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { findByProps, findStoreLazy } from "@webpack";
import { Button, Text } from "@webpack/common";
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
function NoDMNotificationsModal({ modalProps }: { modalProps: ModalProps; }) {
return (
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<ModalContent>
<div style={{ display: "flex", flexDirection: "column", justifyContent: "center", "alignItems": "center", textAlign: "center", height: "100%", padding: "8px 0", gap: "16px" }}>
<Text variant="text-lg/semibold">You seem to have been affected by a bug that caused DM notifications to be muted and break if you used the MuteNewGuild plugin.</Text>
<Text variant="text-lg/semibold">If you haven't received any notifications for private messages, this is why. This issue is now fixed, so they should work again. Please verify, and in case they are still broken, ask for help in the Vencord support channel!</Text>
<Text variant="text-lg/semibold">We're very sorry for any inconvenience caused by this issue :(</Text>
</div>
</ModalContent>
<ModalFooter>
<div style={{ display: "flex", justifyContent: "center", width: "100%" }}>
<Button
onClick={modalProps.onClose}
size={Button.Sizes.MEDIUM}
color={Button.Colors.BRAND}
>
Understood!
</Button>
</div>
</ModalFooter>
</ModalRoot>
);
}
export default definePlugin({
name: "MuteNewGuild",
description: "Mutes newly joined guilds",
authors: [Devs.Glitch, Devs.Nuckyz],
patches: [
{
find: ",acceptInvite:function",
replacement: {
match: /INVITE_ACCEPT_SUCCESS.+?;(\i)=null.+?;/,
replace: (m, guildId) => `${m}$self.handleMute(${guildId});`
}
}
],
handleMute(guildId: string | null) {
if (guildId === "@me" || guildId === "null" || guildId == null) return;
findByProps("updateGuildNotificationSettings").updateGuildNotificationSettings(guildId, { muted: true, suppress_everyone: true, suppress_roles: true });
},
start() {
const [isMuted, isEveryoneSupressed, isRolesSupressed] = [UserGuildSettingsStore.isMuted(null), UserGuildSettingsStore.isSuppressEveryoneEnabled(null), UserGuildSettingsStore.isSuppressRolesEnabled(null)];
if (isMuted || isEveryoneSupressed || isRolesSupressed) {
findByProps("updateGuildNotificationSettings").updateGuildNotificationSettings(null, { muted: false, suppress_everyone: false, suppress_roles: false });
openModal(modalProps => <NoDMNotificationsModal modalProps={modalProps} />);
}
}
});

View file

@ -83,7 +83,7 @@ async function resolveImage(options: Argument[], ctx: CommandContext, noServerPf
export default definePlugin({ export default definePlugin({
name: "petpet", name: "petpet",
description: "headpet a cutie", description: "Adds a /petpet slash command to create headpet gifs from any image",
authors: [Devs.Ven], authors: [Devs.Ven],
dependencies: ["CommandsAPI"], dependencies: ["CommandsAPI"],
commands: [ commands: [

View file

@ -0,0 +1,75 @@
/*
* 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 { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { Menu } from "@webpack/common";
import { isPinned, movePin, PinOrder, settings, snapshotArray, togglePin } from "./settings";
function PinMenuItem(channelId: string) {
const pinned = isPinned(channelId);
const canMove = pinned && settings.store.pinOrder === PinOrder.Custom;
return (
<>
<Menu.MenuItem
id="pin-dm"
label={pinned ? "Unpin DM" : "Pin DM"}
action={() => togglePin(channelId)}
/>
{canMove && snapshotArray[0] !== channelId && (
<Menu.MenuItem
id="move-pin-up"
label="Move Pin Up"
action={() => movePin(channelId, -1)}
/>
)}
{canMove && snapshotArray[snapshotArray.length - 1] !== channelId && (
<Menu.MenuItem
id="move-pin-down"
label="Move Pin Down"
action={() => movePin(channelId, +1)}
/>
)}
</>
);
}
const GroupDMContext: NavContextMenuPatchCallback = (children, props) => () => {
const container = findGroupChildrenByChildId("leave-channel", children);
if (container)
container.unshift(PinMenuItem(props.channel.id));
};
const UserContext: NavContextMenuPatchCallback = (children, props) => () => {
const container = findGroupChildrenByChildId("close-dm", children);
if (container) {
const idx = container.findIndex(c => c?.props?.id === "close-dm");
container.splice(idx, 0, PinMenuItem(props.channel.id));
}
};
export function addContextMenus() {
addContextMenuPatch("gdm-context", GroupDMContext);
addContextMenuPatch("user-context", UserContext);
}
export function removeContextMenus() {
removeContextMenuPatch("gdm-context", GroupDMContext);
removeContextMenuPatch("user-context", UserContext);
}

View file

@ -0,0 +1,127 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Channel } from "discord-types/general";
import { addContextMenus, removeContextMenus } from "./contextMenus";
import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings";
export default definePlugin({
name: "PinDMs",
description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
authors: [Devs.Ven, Devs.Strencher],
settings,
start: addContextMenus,
stop: removeContextMenus,
usePinCount(channelIds: string[]) {
const pinnedDms = usePinnedDms();
// See comment on 2nd patch for reasoning
return channelIds.length ? [pinnedDms.size] : [];
},
getChannel(channels: Record<string, Channel>, idx: number) {
return channels[getPinAt(idx)];
},
isPinned,
getSnapshot: sortedSnapshot,
getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
if (!isPinned(channelId))
return (
(rowHeight + padding) * 2 // header
+ rowHeight * snapshotArray.length // pins
+ originalOffset // original pin offset minus pins
);
return rowHeight * (snapshotArray.indexOf(channelId) + preRenderedChildren) + padding;
},
patches: [
// Patch DM list
{
find: ".privateChannelsHeaderContainer,",
replacement: [
{
// filter Discord's privateChannelIds list to remove pins, and pass
// pinCount as prop. This needs to be here so that the entire DM list receives
// updates on pin/unpin
match: /privateChannelIds:(\i),/,
replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c)),pinCount:$self.usePinCount($1),"
},
{
// sections is an array of numbers, where each element is a section and
// the number is the amount of rows. Add our pinCount in second place
// - Section 1: buttons for pages like Friends & Library
// - Section 2: our pinned dms
// - Section 3: the normal dm list
match: /(?<=renderRow:(\i)\.renderRow,)sections:\[\i,/,
// For some reason, adding our sections when no private channels are ready yet
// makes DMs infinitely load. Thus usePinCount returns either a single element
// array with the count, or an empty array. Due to spreading, only in the former
// case will an element be added to the outer array
// Thanks for the fix, Strencher!
replace: "$&...$1.props.pinCount,"
},
{
// Patch renderSection (renders the header) to set the text to "Pinned DMs" instead of "Direct Messages"
// lookbehind is used to lookup parameter name. We could use arguments[0], but
// if children ever is wrapped in an iife, it will break
match: /children:(\i\.\i\.Messages.DIRECT_MESSAGES)(?<=renderSection=function\((\i)\).+?)/,
replace: "children:$2.section===1?'Pinned DMs':$1"
},
{
// Patch channel lookup inside renderDM
// channel=channels[channelIds[row]];
match: /(?<=preRenderedChildren,(\i)=)((\i)\[\i\[\i\]\]);/,
// section 1 is us, manually get our own channel
// section === 1 ? getChannel(channels, row) : channels[channelIds[row]];
replace: "arguments[0]===1?$self.getChannel($3,arguments[1]):$2;"
},
{
// Fix getRowHeight's check for whether this is the DMs section
// section === DMS
match: /===\i.DMS&&0/,
// section -1 === DMS
replace: "-1$&"
},
{
// Override scrollToChannel to properly account for pinned channels
match: /(?<=else\{\i\+=)(\i)\*\(.+?(?=;)/,
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
}
]
},
// Fix Alt Up/Down navigation
{
find: '"mod+alt+right"',
replacement: {
// channelIds = __OVERLAY__ ? stuff : toArray(getStaticPaths()).concat(toArray(channelIds))
match: /(?<=(\i)=__OVERLAY__\?\i:.{0,10})\.concat\((.{0,10})\)/,
// ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
replace: ".concat($self.getSnapshot()).concat($2.filter(c=>!$self.isPinned(c)))"
}
}
]
});

View file

@ -0,0 +1,94 @@
/*
* 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, Settings, useSettings } from "@api/settings";
import { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
export const enum PinOrder {
LastMessage,
Custom
}
export const settings = definePluginSettings({
pinOrder: {
type: OptionType.SELECT,
description: "Which order should pinned DMs be displayed in?",
options: [
{ label: "Most recent message", value: PinOrder.LastMessage, default: true },
{ label: "Custom (right click channels to reorder)", value: PinOrder.Custom }
]
}
});
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore");
export let snapshotArray: string[];
let snapshot: Set<string> | undefined;
const getArray = () => (Settings.plugins.PinDMs.pinnedDMs || void 0)?.split(",") as string[] | undefined;
const save = (pins: string[]) => {
snapshot = void 0;
Settings.plugins.PinDMs.pinnedDMs = pins.join(",");
};
const takeSnapshot = () => {
snapshotArray = getArray() ?? [];
return snapshot = new Set<string>(snapshotArray);
};
const requireSnapshot = () => snapshot ?? takeSnapshot();
export function usePinnedDms() {
useSettings(["plugins.PinDMs.pinnedDMs"]);
return requireSnapshot();
}
export function isPinned(id: string) {
return requireSnapshot().has(id);
}
export function togglePin(id: string) {
const snapshot = requireSnapshot();
if (!snapshot.delete(id)) {
snapshot.add(id);
}
save([...snapshot]);
}
export function sortedSnapshot() {
requireSnapshot();
if (settings.store.pinOrder === PinOrder.LastMessage)
return PrivateChannelSortStore.getPrivateChannelIds().filter(isPinned);
return snapshotArray;
}
export function getPinAt(idx: number) {
return sortedSnapshot()[idx];
}
export function movePin(id: string, direction: -1 | 1) {
const pins = getArray()!;
const a = pins.indexOf(id);
const b = a + direction;
[pins[a], pins[b]] = [pins[b], pins[a]];
save(pins);
}

View file

@ -35,29 +35,34 @@ const bulkFetch = debounce(async () => {
const pronouns = await bulkFetchPronouns(ids); const pronouns = await bulkFetchPronouns(ids);
for (const id of ids) { for (const id of ids) {
// Call all callbacks for the id // Call all callbacks for the id
requestQueue[id].forEach(c => c(pronouns[id])); requestQueue[id]?.forEach(c => c(pronouns[id]));
delete requestQueue[id]; delete requestQueue[id];
} }
}); });
export function awaitAndFormatPronouns(id: string): string | null { export function awaitAndFormatPronouns(id: string): string | null {
const [result, , isPending] = useAwaiter(() => fetchPronouns(id), { const [result, , isPending] = useAwaiter(() => fetchPronouns(id), {
fallbackValue: null, fallbackValue: getCachedPronouns(id),
onError: e => console.error("Fetching pronouns failed: ", e) onError: e => console.error("Fetching pronouns failed: ", e)
}); });
// If the promise completed, the result was not "unspecified", and there is a mapping for the code, then return the mappings // If the result is present and not "unspecified", and there is a mapping for the code, then return the mappings
if (!isPending && result && result !== "unspecified" && PronounMapping[result]) if (result && result !== "unspecified" && PronounMapping[result])
return formatPronouns(result); return formatPronouns(result);
return null; return null;
} }
// Gets the cached pronouns, if you're too impatient for a promise!
export function getCachedPronouns(id: string): PronounCode | null {
return cache[id] ?? null;
}
// Fetches the pronouns for one id, returning a promise that resolves if it was cached, or once the request is completed // Fetches the pronouns for one id, returning a promise that resolves if it was cached, or once the request is completed
export function fetchPronouns(id: string): Promise<PronounCode> { export function fetchPronouns(id: string): Promise<PronounCode> {
return new Promise(res => { return new Promise(res => {
// If cached, return the cached pronouns // If cached, return the cached pronouns
if (id in cache) res(cache[id]); if (id in cache) res(getCachedPronouns(id)!);
// If there is already a request added, then just add this callback to it // If there is already a request added, then just add this callback to it
else if (id in requestQueue) requestQueue[id].push(res); else if (id in requestQueue) requestQueue[id].push(res);
// If not already added, then add it and call the debounced function to make sure the request gets executed // If not already added, then add it and call the debounced function to make sure the request gets executed

View file

@ -18,12 +18,10 @@
import { addButton, removeButton } from "@api/MessagePopover"; import { addButton, removeButton } from "@api/MessagePopover";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { insertTextIntoChatInputBox } from "@utils/discord";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { findLazy } from "@webpack";
import { ChannelStore } from "@webpack/common"; import { ChannelStore } from "@webpack/common";
const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
export default definePlugin({ export default definePlugin({
name: "QuickMention", name: "QuickMention",
authors: [Devs.kemo], authors: [Devs.kemo],
@ -37,7 +35,7 @@ export default definePlugin({
icon: this.Icon, icon: this.Icon,
message: msg, message: msg,
channel: ChannelStore.getChannel(msg.channel_id), channel: ChannelStore.getChannel(msg.channel_id),
onClick: () => ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: `<@${msg.author.id}> ` }) onClick: () => insertTextIntoChatInputBox(`<@${msg.author.id}> `)
}; };
}); });
}, },

View file

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

View file

@ -18,12 +18,10 @@
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { forEachEvent } from "./events"; import { onChannelDelete, onGuildDelete, onRelationshipRemove, removeFriend, removeGroup, removeGuild } from "./functions";
import { removeFriend, removeGroup, removeGuild } from "./functions";
import settings from "./settings"; import settings from "./settings";
import { syncAndRunChecks } from "./utils"; import { syncAndRunChecks, syncFriends, syncGroups, syncGuilds } from "./utils";
export default definePlugin({ export default definePlugin({
name: "RelationshipNotifier", name: "RelationshipNotifier",
@ -55,15 +53,24 @@ export default definePlugin({
} }
], ],
flux: {
GUILD_CREATE: syncGuilds,
GUILD_DELETE: onGuildDelete,
CHANNEL_CREATE: syncGroups,
CHANNEL_DELETE: onChannelDelete,
RELATIONSHIP_ADD: syncFriends,
RELATIONSHIP_UPDATE: syncFriends,
RELATIONSHIP_REMOVE(e) {
onRelationshipRemove(e);
syncFriends();
},
CONNECTION_OPEN: syncAndRunChecks
},
async start() { async start() {
setTimeout(() => { setTimeout(() => {
syncAndRunChecks(); syncAndRunChecks();
}, 5000); }, 5000);
forEachEvent((ev, cb) => FluxDispatcher.subscribe(ev, cb));
},
stop() {
forEachEvent((ev, cb) => FluxDispatcher.unsubscribe(ev, cb));
}, },
removeFriend, removeFriend,

View file

@ -18,7 +18,7 @@
import { DataStore, Notices } from "@api/index"; import { DataStore, Notices } from "@api/index";
import { showNotification } from "@api/Notifications"; import { showNotification } from "@api/Notifications";
import { ChannelStore, GuildStore, RelationshipStore, UserUtils } from "@webpack/common"; import { ChannelStore, GuildStore, RelationshipStore, UserStore, UserUtils } from "@webpack/common";
import settings from "./settings"; import settings from "./settings";
import { ChannelType, RelationshipType, SimpleGroupChannel, SimpleGuild } from "./types"; import { ChannelType, RelationshipType, SimpleGroupChannel, SimpleGuild } from "./types";
@ -30,11 +30,20 @@ const friends = {
requests: [] as string[] requests: [] as string[]
}; };
const guildsKey = () => `relationship-notifier-guilds-${UserStore.getCurrentUser().id}`;
const groupsKey = () => `relationship-notifier-groups-${UserStore.getCurrentUser().id}`;
const friendsKey = () => `relationship-notifier-friends-${UserStore.getCurrentUser().id}`;
async function runMigrations() {
DataStore.delMany(["relationship-notifier-guilds", "relationship-notifier-groups", "relationship-notifier-friends"]);
}
export async function syncAndRunChecks() { export async function syncAndRunChecks() {
await runMigrations();
const [oldGuilds, oldGroups, oldFriends] = await DataStore.getMany([ const [oldGuilds, oldGroups, oldFriends] = await DataStore.getMany([
"relationship-notifier-guilds", guildsKey(),
"relationship-notifier-groups", groupsKey(),
"relationship-notifier-friends" friendsKey()
]) as [Map<string, SimpleGuild> | undefined, Map<string, SimpleGroupChannel> | undefined, Record<"friends" | "requests", string[]> | undefined]; ]) as [Map<string, SimpleGuild> | undefined, Map<string, SimpleGroupChannel> | undefined, Record<"friends" | "requests", string[]> | undefined];
await Promise.all([syncGuilds(), syncGroups(), syncFriends()]); await Promise.all([syncGuilds(), syncGroups(), syncFriends()]);
@ -104,7 +113,7 @@ export async function syncGuilds() {
iconURL: icon && `https://cdn.discordapp.com/icons/${id}/${icon}.png` iconURL: icon && `https://cdn.discordapp.com/icons/${id}/${icon}.png`
}); });
} }
await DataStore.set("relationship-notifier-guilds", guilds); await DataStore.set(guildsKey(), guilds);
} }
export function getGroup(id: string) { export function getGroup(id: string) {
@ -126,7 +135,7 @@ export async function syncGroups() {
}); });
} }
await DataStore.set("relationship-notifier-groups", groups); await DataStore.set(groupsKey(), groups);
} }
export async function syncFriends() { export async function syncFriends() {
@ -145,5 +154,5 @@ export async function syncFriends() {
} }
} }
await DataStore.set("relationship-notifier-friends", friends); await DataStore.set(friendsKey(), friends);
} }

View file

@ -34,7 +34,7 @@ function search(src: string, engine: string) {
open(engine + encodeURIComponent(src), "_blank"); open(engine + encodeURIComponent(src), "_blank");
} }
const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => { const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
if (!props) return; if (!props) return;
const { reverseImageSearchType, itemHref, itemSrc } = props; const { reverseImageSearchType, itemHref, itemSrc } = props;
@ -43,7 +43,7 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) =>
const src = itemHref ?? itemSrc; const src = itemHref ?? itemSrc;
const group = findGroupChildrenByChildId("copy-link", children); const group = findGroupChildrenByChildId("copy-link", children);
if (group && !group.some(child => child?.props?.id === "search-image")) { if (group) {
group.push(( group.push((
<Menu.MenuItem <Menu.MenuItem
label="Search Image" label="Search Image"
@ -76,7 +76,6 @@ export default definePlugin({
name: "ReverseImageSearch", name: "ReverseImageSearch",
description: "Adds ImageSearch to image context menus", description: "Adds ImageSearch to image context menus",
authors: [Devs.Ven, Devs.Nuckyz], authors: [Devs.Ven, Devs.Nuckyz],
dependencies: ["ContextMenuAPI"],
patches: [ patches: [
{ {
find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL", find: ".Messages.MESSAGE_ACTIONS_MENU_LABEL",

View file

@ -0,0 +1,115 @@
/*
* 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 { Settings } from "@api/settings";
import { Review } from "../entities/Review";
import { authorize, showToast } from "./Utils";
const API_URL = "https://manti.vendicated.dev";
const getToken = () => Settings.plugins.ReviewDB.token;
interface Response {
success: boolean,
message: string;
reviews: Review[];
updated: boolean;
}
export async function getReviews(id: string): Promise<Review[]> {
const req = await fetch(API_URL + `/api/reviewdb/users/${id}/reviews`);
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 };
if (!res.success) {
showToast(res.message);
return [
{
id: 0,
comment: "An Error occured while fetching reviews. Please try again later.",
star: 0,
sender: {
id: 0,
username: "Error",
profilePhoto: "https://cdn.discordapp.com/attachments/1045394533384462377/1084900598035513447/646808599204593683.png?size=128",
discordID: "0",
badges: []
}
}
];
}
return res.reviews;
}
export async function addReview(review: any): Promise<Response | null> {
review.token = getToken();
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: getToken(),
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: getToken()
})
}).then(r => r.json()) as Response;
showToast(await res.message);
}
export function getLastReviewID(id: string): Promise<number> {
return fetch(API_URL + "/getLastReviewID?discordid=" + id)
.then(r => r.text())
.then(Number);
}

View file

@ -0,0 +1,95 @@
/*
* 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 { Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import Logger from "@utils/Logger";
import { openModal } from "@utils/modal";
import { findByProps } from "@webpack";
import { FluxDispatcher, React, SelectedChannelStore, Toasts, UserUtils } from "@webpack/common";
import { Review } from "../entities/Review";
export async function openUserProfileModal(userId: string) {
await UserUtils.fetchUser(userId);
await FluxDispatcher.dispatch({
type: "USER_PROFILE_MODAL_OPEN",
userId,
channelId: SelectedChannelStore.getChannelId(),
analyticsLocation: "Explosive Hotel"
});
}
export function authorize(callback?: any) {
const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");
openModal((props: any) =>
<OAuth2AuthorizeModal
{...props}
scopes={["identify"]}
responseType="code"
redirectUri="https://manti.vendicated.dev/URauth"
permissions={0n}
clientId="915703782174752809"
cancelCompletesFlow={false}
callback={async (u: string) => {
try {
const url = new URL(u);
url.searchParams.append("returnType", "json");
url.searchParams.append("clientMod", "vencord");
const res = await fetch(url, {
headers: new Headers({ Accept: "application/json" })
});
const { token, status } = await res.json();
if (status === 0) {
Settings.plugins.ReviewDB.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 authorise", e);
}
}}
/>
);
}
export function showToast(text: string) {
Toasts.show({
type: Toasts.Type.MESSAGE,
message: text,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
},
});
}
export const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
export function canDeleteReview(review: Review, userId: string) {
if (review.sender.discordID === userId) return true;
const myId = BigInt(userId);
return myId === Devs.mantikafasi.id ||
myId === Devs.Ven.id ||
myId === Devs.rushii.id;
}

View file

@ -0,0 +1,43 @@
/*
* 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 { classes, LazyComponent } from "@utils/misc";
import { findByProps } from "@webpack";
export default LazyComponent(() => {
const { button, dangerous } = findByProps("button", "wrapper", "disabled","separator");
return function MessageButton(props) {
return props.type === "delete"
? (
<div className={classes(button, dangerous)} aria-label="Delete Review" onClick={props.callback}>
<svg aria-hidden="false" width="16" height="16" viewBox="0 0 20 20">
<path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
<path fill="currentColor" d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"></path>
</svg>
</div>
)
: (
<div className={button} aria-label="Report Review" onClick={() => props.callback()}>
<svg aria-hidden="false" width="16" height="16" viewBox="0 0 20 20">
<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"></path>
</svg>
</div>
);
};
});

View file

@ -0,0 +1,45 @@
/*
* 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/Badge";
export default function ReviewBadge(badge: Badge) {
return (
<Tooltip
text={badge.name}>
{({ onMouseEnter, onMouseLeave }) => (
<img
width="24px"
height="24px"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
src={badge.icon}
alt={badge.description}
style={{ verticalAlign: "middle", marginLeft: "4px" }}
onClick={() =>
MaskedLinkStore.openUntrustedLink({
href: badge.redirectURL,
})
}
/>
)}
</Tooltip>
);
}

View file

@ -0,0 +1,125 @@
/*
* 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 { classes, LazyComponent } from "@utils/misc";
import { filters, findBulk } from "@webpack";
import { Alerts, UserStore } from "@webpack/common";
import { Review } from "../entities/Review";
import { deleteReview, reportReview } from "../Utils/ReviewDBAPI";
import { canDeleteReview, openUserProfileModal, showToast } from "../Utils/Utils";
import MessageButton from "./MessageButton";
import ReviewBadge from "./ReviewBadge";
export default LazyComponent(() => {
// this is terrible, blame mantika
const p = filters.byProps;
const [
{ cozyMessage, buttons, message, groupStart },
{ container, isHeader },
{ avatar, clickable, username, messageContent, wrapper, cozy },
{ contents },
buttonClasses,
{ defaultColor }
] = findBulk(
p("cozyMessage"),
p("container", "isHeader"),
p("avatar", "zalgo"),
p("contents"),
p("button", "wrapper", "selected"),
p("defaultColor")
);
return function ReviewComponent({ review, refetch }: { review: Review; refetch(): void; }) {
function openModal() {
openUserProfileModal(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, "user-review")} style={
{
marginLeft: "0px",
paddingLeft: "52px",
paddingRight: "16px"
}
}>
<div className={contents} style={{ paddingLeft: "0px" }}>
<img
className={classes(avatar, clickable)}
onClick={openModal}
src={review.sender.profilePhoto || "/assets/1f0bfc0865d324c2587920a7d80c609b.png?size=128"}
style={{ left: "0px" }}
/>
<span
className={classes(clickable, username)}
style={{ color: "var(--channels-default)", fontSize: "14px" }}
onClick={() => openModal()}
>
{review.sender.username}
</span>
{review.sender.badges.map(badge => <ReviewBadge {...badge} />)}
<p
className={classes(messageContent, defaultColor)}
style={{ fontSize: 15, marginTop: 4 }}
>
{review.comment}
</p>
<div className={classes(container, isHeader, buttons)} style={{
padding: "0px",
}}>
<div className={buttonClasses.wrapper} >
<MessageButton type="report" callback={reportRev} />
{canDeleteReview(review, UserStore.getCurrentUser().id) && (
<MessageButton type="delete" callback={delReview} />
)}
</div>
</div>
</div>
</div>
);
};
});

View file

@ -0,0 +1,97 @@
/*
* 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 { classes, useAwaiter } from "@utils/misc";
import { findLazy } from "@webpack";
import { Forms, React, Text, UserStore } from "@webpack/common";
import type { KeyboardEvent } from "react";
import { addReview, getReviews } from "../Utils/ReviewDBAPI";
import { showToast } from "../Utils/Utils";
import ReviewComponent from "./ReviewComponent";
const Classes = findLazy(m => typeof m.textarea === "string");
export default function ReviewsView({ userId }: { userId: string; }) {
const [refetchCount, setRefetchCount] = React.useState(0);
const [reviews, _, isLoading] = useAwaiter(() => getReviews(userId), {
fallbackValue: [],
deps: [refetchCount],
});
const username = UserStore.getUser(userId)?.username ?? "";
const dirtyRefetch = () => setRefetchCount(refetchCount + 1);
if (isLoading) return null;
function onKeyPress({ key, target }: KeyboardEvent<HTMLTextAreaElement>) {
if (key === "Enter") {
addReview({
userid: userId,
comment: (target as HTMLInputElement).value,
star: -1
}).then(res => {
if (res?.success) {
(target as HTMLInputElement).value = ""; // clear the input
dirtyRefetch();
} else if (res?.message) {
showToast(res.message);
}
});
}
}
return (
<div className="vc-reviewdb-view">
<Text
tag="h2"
variant="eyebrow"
style={{
marginBottom: "12px",
color: "var(--header-primary)"
}}
>
User Reviews
</Text>
{reviews?.map(review =>
<ReviewComponent
key={review.id}
review={review}
refetch={dirtyRefetch}
/>
)}
{reviews?.length === 0 && (
<Forms.FormText style={{ padding: "12px", paddingTop: "0px", paddingLeft: "4px", fontWeight: "bold", fontStyle: "italic" }}>
Looks like nobody reviewed this user yet. You could be the first!
</Forms.FormText>
)}
<textarea
className={classes(Classes.textarea.replace("textarea", ""), "enter-comment")}
// this produces something like '-_59yqs ...' but since no class exists with that name its fine
placeholder={reviews?.some(r => r.sender.discordID === UserStore.getCurrentUser().id) ? `Update review for @${username}` : `Review @${username}`}
onKeyDown={onKeyPress}
style={{
marginTop: "6px",
resize: "none",
marginBottom: "12px",
overflow: "hidden",
}}
/>
</div>
);
}

View file

@ -0,0 +1,26 @@
/*
* 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/>.
*/
export interface Badge {
name: string;
description: string;
icon: string;
redirectURL : string;
type: number;
}

View file

@ -0,0 +1,34 @@
/*
* 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 { Badge } from "./Badge";
export interface Sender {
id : number,
discordID: string,
username: string,
profilePhoto: string,
badges: Badge[]
}
export interface Review {
comment: string,
id: number,
star: number,
sender: Sender,
}

View file

@ -0,0 +1,82 @@
/*
* 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 { Settings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Button, UserStore } from "@webpack/common";
import { User } from "discord-types/general";
import ReviewsView from "./components/ReviewsView";
import { getLastReviewID } from "./Utils/ReviewDBAPI";
import { authorize, showToast } from "./Utils/Utils";
export default definePlugin({
name: "ReviewDB",
description: "Review other users (Adds a new settings to profiles)",
authors: [Devs.mantikafasi, Devs.Ven],
patches: [
{
find: "disableBorderColor:!0",
replacement: {
match: /\(.{0,10}\{user:(.),setNote:.,canDM:.,.+?\}\)/,
replace: "$&,$self.getReviewsComponent($1)"
}
}
],
options: {
authorize: {
type: OptionType.COMPONENT,
description: "Authorise with ReviewDB",
component: () => (
<Button onClick={authorize}>
Authorise with ReviewDB
</Button>
)
},
notifyReviews: {
type: OptionType.BOOLEAN,
description: "Notify about new reviews on startup",
default: true,
}
},
async start() {
const settings = Settings.plugins.ReviewDB;
if (!settings.lastReviewId || !settings.notifyReviews) return;
setTimeout(async () => {
const id = await getLastReviewID(UserStore.getCurrentUser().id);
if (settings.lastReviewId < id) {
showToast("You have new reviews on your profile!");
settings.lastReviewId = id;
}
}, 4000);
},
getReviewsComponent: (user: User) => (
<ErrorBoundary message="Failed to render Reviews">
<ReviewsView userId={user.id} />
</ErrorBoundary>
)
});

View file

@ -0,0 +1,3 @@
[class|="section"]:not([class|="lastSection"]) + .vc-reviewdb-view {
margin-top: 12px;
}

View file

@ -28,7 +28,7 @@ const ReplyIcon = LazyComponent(() => findByCode("M10 8.26667V4L3 11.4667L10 18.
const replyFn = findByCodeLazy("showMentionToggle", "TEXTAREA_FOCUS", "shiftKey"); const replyFn = findByCodeLazy("showMentionToggle", "TEXTAREA_FOCUS", "shiftKey");
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => { const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => {
// make sure the message is in the selected channel // make sure the message is in the selected channel
if (SelectedChannelStore.getChannelId() !== message.channel_id) return; if (SelectedChannelStore.getChannelId() !== message.channel_id) return;
@ -61,7 +61,6 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { messag
/> />
)); ));
} }
}; };

View file

@ -0,0 +1,219 @@
/*
* 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 "./styles.css";
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
import { classNameFactory } from "@api/Styles";
import { Devs } from "@utils/constants";
import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord";
import { Margins } from "@utils/margins";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Button, ButtonLooks, ButtonWrapperClasses, Forms, Parser, Select, Tooltip, useMemo, useState } from "@webpack/common";
function parseTime(time: string) {
const cleanTime = time.slice(1, -1).replace(/(\d)(AM|PM)$/i, "$1 $2");
let ms = new Date(`${new Date().toDateString()} ${cleanTime}`).getTime() / 1000;
if (isNaN(ms)) return time;
// add 24h if time is in the past
if (Date.now() / 1000 > ms) ms += 86400;
return `<t:${Math.round(ms)}:t>`;
}
const Formats = ["", "t", "T", "d", "D", "f", "F", "R"] as const;
type Format = typeof Formats[number];
const cl = classNameFactory("vc-st-");
function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): void; }) {
const [value, setValue] = useState<string>();
const [format, setFormat] = useState<Format>("");
const time = Math.round((new Date(value!).getTime() || Date.now()) / 1000);
const formatTimestamp = (time: number, format: Format) => `<t:${time}${format && `:${format}`}>`;
const [formatted, rendered] = useMemo(() => {
const formatted = formatTimestamp(time, format);
return [formatted, Parser.parse(formatted)];
}, [time, format]);
return (
<ModalRoot {...rootProps}>
<ModalHeader className={cl("modal-header")}>
<Forms.FormTitle tag="h2">
Timestamp Picker
</Forms.FormTitle>
<ModalCloseButton onClick={close} />
</ModalHeader>
<ModalContent className={cl("modal-content")}>
<input
type="datetime-local"
value={value}
onChange={e => setValue(e.currentTarget.value)}
style={{
colorScheme: getTheme() === Theme.Light ? "light" : "dark",
}}
/>
<Forms.FormTitle>Timestamp Format</Forms.FormTitle>
<Select
options={
Formats.map(m => ({
label: m,
value: m
}))
}
isSelected={v => v === format}
select={v => setFormat(v)}
serialize={v => v}
renderOptionLabel={o => (
<div className={cl("format-label")}>
{Parser.parse(formatTimestamp(time, o.value))}
</div>
)}
renderOptionValue={() => rendered}
/>
<Forms.FormTitle className={Margins.bottom8}>Preview</Forms.FormTitle>
<Forms.FormText className={cl("preview-text")}>
{rendered} ({formatted})
</Forms.FormText>
</ModalContent>
<ModalFooter>
<Button
onClick={() => {
insertTextIntoChatInputBox(formatted + " ");
close();
}}
>Insert</Button>
</ModalFooter>
</ModalRoot>
);
}
export default definePlugin({
name: "SendTimestamps",
description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!",
authors: [Devs.Ven, Devs.Tyler],
dependencies: ["MessageEventsAPI"],
patches: [
{
find: ".activeCommandOption",
replacement: {
match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/,
replace: "$&;try{$2||$1.push($self.chatBarIcon())}catch{}",
}
},
],
start() {
this.listener = addPreSendListener((_, msg) => {
msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime);
});
},
stop() {
removePreSendListener(this.listener);
},
chatBarIcon() {
return (
<Tooltip text="Insert Timestamp">
{({ onMouseEnter, onMouseLeave }) => (
<div style={{ display: "flex" }}>
<Button
aria-haspopup="dialog"
aria-label=""
size=""
look={ButtonLooks.BLANK}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
innerClassName={ButtonWrapperClasses.button}
onClick={() => {
const key = openModal(props => (
<PickerModal
rootProps={props}
close={() => closeModal(key)}
/>
));
}}
className={cl("button")}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
<svg
aria-hidden="true"
role="img"
width="24"
height="24"
viewBox="0 0 24 24"
>
<g fill="none" fill-rule="evenodd">
<path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" />
<rect width="24" height="24" />
</g>
</svg>
</div>
</Button>
</div>
)
}
</Tooltip >
);
},
settingsAboutComponent() {
const samples = [
"12:00",
"3:51",
"17:59",
"24:00",
"12:00 AM",
"0:13PM"
].map(s => `\`${s}\``);
return (
<>
<Forms.FormText>
To quickly send send time only timestamps, include timestamps formatted as `HH:MM` (including the backticks!) in your message
</Forms.FormText>
<Forms.FormText>
See below for examples.
If you need anything more specific, use the Date button in the chat bar!
</Forms.FormText>
<Forms.FormText>
Examples:
<ul>
{samples.map(s => (
<li key={s}>
<code>{s}</code> {"->"} {Parser.parse(parseTime(s))}
</li>
))}
</ul>
</Forms.FormText>
</>
);
},
});

View file

@ -0,0 +1,51 @@
.vc-st-modal-content input {
background-color: var(--input-background);
color: var(--text-normal);
width: 95%;
padding: 8px 8px 8px 12px;
margin: 1em 0;
outline: none;
border: 1px solid var(--input-background);
border-radius: 4px;
font-weight: 500;
font-style: inherit;
font-size: 100%;
}
.vc-st-format-label,
.vc-st-format-label span {
background-color: transparent;
}
.vc-st-modal-content [class|="select"] {
margin-bottom: 1em;
}
.vc-st-modal-content [class|="select"] span {
background-color: var(--input-background);
}
.vc-st-modal-header {
justify-content: space-between;
align-content: center;
}
.vc-st-modal-header h1 {
margin: 0;
}
.vc-st-modal-header button {
padding: 0;
}
.vc-st-preview-text {
margin-bottom: 1em;
}
.vc-st-button {
padding: 0 8px;
}
.vc-st-button svg {
transform: scale(1.1) translateY(1px);
}

View file

@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/misc"; import { useForceUpdater } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher, GuildStore,PresenceStore, RelationshipStore } from "@webpack/common"; import { GuildStore, PresenceStore, RelationshipStore } from "@webpack/common";
enum IndicatorType { enum IndicatorType {
SERVER = 1 << 0, SERVER = 1 << 0,
@ -71,6 +71,24 @@ function ServersIndicator() {
); );
} }
function handlePresenceUpdate() {
onlineFriends = 0;
const relations = RelationshipStore.getRelationships();
for (const id of Object.keys(relations)) {
const type = relations[id];
// FRIEND relationship type
if (type === 1 && PresenceStore.getStatus(id) !== "offline") {
onlineFriends += 1;
}
}
forceUpdateFriendCount?.();
}
function handleGuildUpdate() {
guildCount = GuildStore.getGuildCount();
forceUpdateGuildCount?.();
}
export default definePlugin({ export default definePlugin({
name: "ServerListIndicators", name: "ServerListIndicators",
description: "Add online friend count or server count in the server list", description: "Add online friend count or server count in the server list",
@ -99,37 +117,21 @@ export default definePlugin({
</ErrorBoundary>; </ErrorBoundary>;
}, },
handlePresenceUpdate() { flux: {
onlineFriends = 0; PRESENCE_UPDATES: handlePresenceUpdate,
const relations = RelationshipStore.getRelationships(); GUILD_CREATE: handleGuildUpdate,
for (const id of Object.keys(relations)) { GUILD_DELETE: handleGuildUpdate,
const type = relations[id];
// FRIEND relationship type
if (type === 1 && PresenceStore.getStatus(id) !== "offline") {
onlineFriends += 1;
}
}
forceUpdateFriendCount?.();
}, },
handleGuildUpdate() {
guildCount = GuildStore.getGuildCount();
forceUpdateGuildCount?.();
},
start() { start() {
this.handlePresenceUpdate();
this.handleGuildUpdate();
addServerListElement(ServerListRenderPosition.Above, this.renderIndicator); addServerListElement(ServerListRenderPosition.Above, this.renderIndicator);
FluxDispatcher.subscribe("PRESENCE_UPDATES", this.handlePresenceUpdate);
FluxDispatcher.subscribe("GUILD_CREATE", this.handleGuildUpdate); handlePresenceUpdate();
FluxDispatcher.subscribe("GUILD_DELETE", this.handleGuildUpdate); handleGuildUpdate();
}, },
stop() { stop() {
removeServerListElement(ServerListRenderPosition.Above, this.renderIndicator); removeServerListElement(ServerListRenderPosition.Above, this.renderIndicator);
FluxDispatcher.unsubscribe("PRESENCE_UPDATES", this.handlePresenceUpdate);
FluxDispatcher.unsubscribe("GUILD_CREATE", this.handleGuildUpdate);
FluxDispatcher.unsubscribe("GUILD_DELETE", this.handleGuildUpdate);
} }
}); });

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/settings"; import { Settings } from "@api/settings";
import PatchHelper from "@components/PatchHelper"; import PatchHelper from "@components/PatchHelper";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
@ -33,6 +34,23 @@ export default definePlugin({
description: "Adds Settings UI and debug info", description: "Adds Settings UI and debug info",
authors: [Devs.Ven, Devs.Megu], authors: [Devs.Ven, Devs.Megu],
required: true, required: true,
start() {
// The settings shortcuts in the user settings cog context menu
// read the elements from a hardcoded map which for obvious reason
// doesn't contain our sections. This patches the actions of our
// sections to manually use SettingsRouter (which only works on desktop
// but the context menu is usually not available on mobile anyway)
addContextMenuPatch("user-settings-cog", children => () => {
const section = children.find(c => Array.isArray(c) && c.some(it => it?.props?.id === "VencordSettings")) as any;
section?.forEach(c => {
if (c?.props?.id?.startsWith("Vencord")) {
c.props.action = () => SettingsRouter.open(c.props.id);
}
});
});
},
patches: [{ patches: [{
find: ".versionHash", find: ".versionHash",
replacement: [ replacement: [
@ -69,8 +87,6 @@ export default definePlugin({
}], }],
makeSettingsCategories({ ID }: { ID: Record<string, unknown>; }) { makeSettingsCategories({ ID }: { ID: Record<string, unknown>; }) {
const makeOnClick = (tab: string) => () => SettingsRouter.open(tab);
return [ return [
{ {
section: ID.HEADER, section: ID.HEADER,
@ -79,50 +95,42 @@ export default definePlugin({
{ {
section: "VencordSettings", section: "VencordSettings",
label: "Vencord", label: "Vencord",
element: () => <SettingsComponent tab="VencordSettings" />, element: () => <SettingsComponent tab="VencordSettings" />
onClick: makeOnClick("VencordSettings")
}, },
{ {
section: "VencordPlugins", section: "VencordPlugins",
label: "Plugins", label: "Plugins",
element: () => <SettingsComponent tab="VencordPlugins" />, element: () => <SettingsComponent tab="VencordPlugins" />,
onClick: makeOnClick("VencordPlugins")
}, },
{ {
section: "VencordThemes", section: "VencordThemes",
label: "Themes", label: "Themes",
element: () => <SettingsComponent tab="VencordThemes" />, element: () => <SettingsComponent tab="VencordThemes" />,
onClick: makeOnClick("VencordThemes")
}, },
!IS_WEB && { !IS_WEB && {
section: "VencordUpdater", section: "VencordUpdater",
label: "Updater", label: "Updater",
element: () => <SettingsComponent tab="VencordUpdater" />, element: () => <SettingsComponent tab="VencordUpdater" />,
onClick: makeOnClick("VencordUpdater")
}, },
{ {
section: "VencordCloud", section: "VencordCloud",
label: "Cloud", label: "Cloud",
element: () => <SettingsComponent tab="VencordCloud" />, element: () => <SettingsComponent tab="VencordCloud" />,
onClick: makeOnClick("VencordCloud")
}, },
{ {
section: "VencordSettingsSync", section: "VencordSettingsSync",
label: "Backup & Restore", label: "Backup & Restore",
element: () => <SettingsComponent tab="VencordSettingsSync" />, element: () => <SettingsComponent tab="VencordSettingsSync" />,
onClick: makeOnClick("VencordSettingsSync")
}, },
IS_DEV && { IS_DEV && {
section: "VencordPatchHelper", section: "VencordPatchHelper",
label: "Patch Helper", label: "Patch Helper",
element: PatchHelper!, element: PatchHelper!,
onClick: makeOnClick("VencordPatchHelper")
}, },
IS_VENCORD_DESKTOP && { IS_VENCORD_DESKTOP && {
section: "VencordDesktop", section: "VencordDesktop",
label: "Desktop Settings", label: "Desktop Settings",
element: VencordDesktop.Components.Settings, element: VencordDesktop.Components.Settings,
onClick: makeOnClick("VencordDesktop")
}, },
{ {
section: ID.DIVIDER section: ID.DIVIDER

View file

@ -44,6 +44,7 @@
.shiki-btn { .shiki-btn {
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
padding: 4px 8px; padding: 4px 8px;
user-select: none;
} }
.shiki-btn ~ .shiki-btn { .shiki-btn ~ .shiki-btn {

View file

@ -19,14 +19,13 @@
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { LazyComponent } from "@utils/misc"; import { LazyComponent } from "@utils/misc";
import { formatDuration } from "@utils/text"; import { formatDuration } from "@utils/text";
import { find, findByPropsLazy } from "@webpack"; import { find, findByPropsLazy, findStoreLazy } from "@webpack";
import { FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common"; import { FluxDispatcher, GuildMemberStore, GuildStore, moment, Parser, PermissionStore, SnowflakeUtils, Text, Timestamp, Tooltip } from "@webpack/common";
import type { Channel } from "discord-types/general"; import type { Channel } from "discord-types/general";
import type { ComponentType } from "react"; import type { ComponentType } from "react";
import { VIEW_CHANNEL } from ".."; import { VIEW_CHANNEL } from "..";
enum SortOrderTypes { enum SortOrderTypes {
LATEST_ACTIVITY = 0, LATEST_ACTIVITY = 0,
CREATION_DATE = 1 CREATION_DATE = 1
@ -93,6 +92,10 @@ const TagComponent = LazyComponent(() => find(m => {
return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill"); return code.includes(".Messages.FORUM_TAG_A11Y_FILTER_BY_TAG") && !code.includes("increasedActivityPill");
})); }));
const EmojiStore = findStoreLazy("EmojiStore");
const EmojiParser = findByPropsLazy("convertSurrogateToName");
const EmojiUtils = findByPropsLazy("getURL", "buildEmojiReactionColorsPlatformed");
const ChannelTypesToChannelNames = { const ChannelTypesToChannelNames = {
[ChannelTypes.GUILD_TEXT]: "text", [ChannelTypes.GUILD_TEXT]: "text",
[ChannelTypes.GUILD_ANNOUNCEMENT]: "announcement", [ChannelTypes.GUILD_ANNOUNCEMENT]: "announcement",
@ -242,9 +245,15 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
<div className="shc-lock-screen-default-emoji-container"> <div className="shc-lock-screen-default-emoji-container">
<Text variant="text-md/normal">Default reaction emoji:</Text> <Text variant="text-md/normal">Default reaction emoji:</Text>
{Parser.defaultRules[defaultReactionEmoji.emojiName ? "emoji" : "customEmoji"].react({ {Parser.defaultRules[defaultReactionEmoji.emojiName ? "emoji" : "customEmoji"].react({
name: defaultReactionEmoji.emojiName ?? "", name: defaultReactionEmoji.emojiName
emojiId: defaultReactionEmoji.emojiId ? EmojiParser.convertSurrogateToName(defaultReactionEmoji.emojiName)
})} : EmojiStore.getCustomEmojiById(defaultReactionEmoji.emojiId)?.name ?? "",
emojiId: defaultReactionEmoji.emojiId ?? void 0,
surrogate: defaultReactionEmoji.emojiName ?? void 0,
src: defaultReactionEmoji.emojiName
? EmojiUtils.getURL(defaultReactionEmoji.emojiName)
: void 0
}, void 0, { key: "0" })}
</div> </div>
} }
{channel.hasFlag(ChannelFlags.REQUIRE_TAG) && {channel.hasFlag(ChannelFlags.REQUIRE_TAG) &&

View file

@ -25,7 +25,7 @@ import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common"; import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common";
import { Channel } from "discord-types/general"; import type { Channel, Role } from "discord-types/general";
import HiddenChannelLockScreen, { setChannelBeginHeaderComponent } from "./components/HiddenChannelLockScreen"; import HiddenChannelLockScreen, { setChannelBeginHeaderComponent } from "./components/HiddenChannelLockScreen";
@ -252,12 +252,24 @@ export default definePlugin({
match: /permissionOverwrites\[.+?\i=(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/, match: /permissionOverwrites\[.+?\i=(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/,
replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):` replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):`
}, },
{
// Include the @everyone role in the allowed roles list for Hidden Channels
match: /sortBy.{0,100}?return (?<=var (\i)=\i\.channel.+?)(?=\i\.id)/,
replace: (m, channel) => `${m}$self.isHiddenChannel(${channel})?true:`
},
{
// If the @everyone role has the required permissions, make the array only contain it
match: /computePermissionsForRoles.+?.value\(\)(?<=var (\i)=\i\.channel.+?)/,
replace: (m, channel) => `${m}.reduce(...$self.makeAllowedRolesReduce(${channel}.guild_id))`
},
{ {
// Patch the header to only return allowed users and roles if it's a hidden channel or locked channel (Like when it's used on the HiddenChannelLockScreen) // Patch the header to only return allowed users and roles if it's a hidden channel or locked channel (Like when it's used on the HiddenChannelLockScreen)
match: /MANAGE_ROLES.{0,60}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?]}\)))/, match: /MANAGE_ROLES.{0,60}?return(?=\(.+?(\(0,\i\.jsxs\)\("div",{className:\i\(\)\.members.+?guildId:(\i)\.guild_id.+?roleColor.+?]}\)))/,
replace: (m, component, channel) => { replace: (m, component, channel) => {
// Export the channel for the users allowed component patch // Export the channel for the users allowed component patch
component = component.replace(canonicalizeMatch(/(?<=users:\i)/), `,channel:${channel}`); component = component.replace(canonicalizeMatch(/(?<=users:\i)/), `,channel:${channel}`);
// Always render the component for multiple allowed users
component = component.replace(canonicalizeMatch(/1!==\i\.length/), "true");
return `${m} $self.isHiddenChannel(${channel},true)?${component}:`; return `${m} $self.isHiddenChannel(${channel},true)?${component}:`;
} }
@ -297,6 +309,11 @@ export default definePlugin({
match: /"more-options-popout"\)\);if\((?<=function \i\((\i)\).+?)/, match: /"more-options-popout"\)\);if\((?<=function \i\((\i)\).+?)/,
replace: (m, props) => `${m}!${props}.inCall&&$self.isHiddenChannel(${props}.channel,true)){}else if(` replace: (m, props) => `${m}!${props}.inCall&&$self.isHiddenChannel(${props}.channel,true)){}else if(`
}, },
{
// Remove invite users button for the HiddenChannelLockScreen
match: /"popup".{0,100}?if\((?<=(\i)\.channel.+?)/,
replace: (m, props) => `${m}(${props}.inCall||!$self.isHiddenChannel(${props}.channel,true))&&`
},
{ {
// Render our HiddenChannelLockScreen component instead of the main voice channel component // Render our HiddenChannelLockScreen component instead of the main voice channel component
match: /this\.renderVoiceChannelEffects.+?children:(?<=renderContent=function.+?)/, match: /this\.renderVoiceChannelEffects.+?children:(?<=renderContent=function.+?)/,
@ -311,6 +328,11 @@ export default definePlugin({
// Disable useless components for the HiddenChannelLockScreen of voice channels // Disable useless components for the HiddenChannelLockScreen of voice channels
match: /(?:{|,)render(?!Header|ExternalHeader).{0,30}?:(?<=renderContent=function.+?)(?!void)/g, match: /(?:{|,)render(?!Header|ExternalHeader).{0,30}?:(?<=renderContent=function.+?)(?!void)/g,
replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?null:" replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?null:"
},
{
// Disable bad CSS class which mess up hidden voice channels styling
match: /callContainer,(?<=\(\)\.callContainer,)/,
replace: '$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?"":'
} }
] ]
}, },
@ -426,6 +448,20 @@ export default definePlugin({
return res; return res;
}, },
makeAllowedRolesReduce(guildId: string) {
return [
(prev: Array<Role>, _: Role, index: number, originalArray: Array<Role>) => {
if (index !== 0) return prev;
const everyoneRole = originalArray.find(role => role.id === guildId);
if (everyoneRole) return [everyoneRole];
return originalArray;
},
[] as Array<Role>
];
},
HiddenChannelLockScreen: (channel: any) => <HiddenChannelLockScreen channel={channel} />, HiddenChannelLockScreen: (channel: any) => <HiddenChannelLockScreen channel={channel} />,
LockIcon: () => ( LockIcon: () => (

View file

@ -85,7 +85,7 @@
.shc-lock-screen-default-emoji-container > [class^="emojiContainer"] { .shc-lock-screen-default-emoji-container > [class^="emojiContainer"] {
background: var(--bg-overlay-3, var(--background-secondary)); background: var(--bg-overlay-3, var(--background-secondary));
border-radius: 8px; border-radius: 8px;
padding: 3px 4px; padding: 5px 6px;
margin-left: 5px; margin-left: 5px;
} }

View file

@ -0,0 +1,81 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Sofia Lima
*
* 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 "./styles.css";
import { definePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Message } from "discord-types/general";
interface UsernameProps {
author: { nick: string };
message: Message;
withMentionPrefix?: boolean;
isRepliedMessage: boolean;
}
const settings = definePluginSettings({
mode: {
type: OptionType.SELECT,
description: "How to display usernames and nicks",
options: [
{ label: "Username then nickname", value: "user-nick", default: true },
{ label: "Nickname then username", value: "nick-user" },
{ label: "Username only", value: "user" },
],
},
inReplies: {
type: OptionType.BOOLEAN,
default: false,
description: "Also apply functionality to reply previews",
},
});
export default definePlugin({
name: "ShowMeYourName",
description: "Display usernames next to nicks, or no nicks at all",
authors: [Devs.dzshn],
patches: [
{
find: ".withMentionPrefix",
replacement: {
match: /(?<=onContextMenu:\i,children:)\i\+\i/,
replace: "$self.renderUsername(arguments[0])"
}
},
],
settings,
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix }: UsernameProps) => {
try {
const { username } = message.author;
const { nick } = author;
const prefix = withMentionPrefix ? "@" : "";
if (username === nick || isRepliedMessage && !settings.store.inReplies)
return prefix + nick;
if (settings.store.mode === "user-nick")
return <>{prefix}{username} <span className="vc-smyn-suffix">{nick}</span></>;
if (settings.store.mode === "nick-user")
return <>{prefix}{nick} <span className="vc-smyn-suffix">{username}</span></>;
return prefix + username;
} catch {
return author?.nick;
}
},
});

View file

@ -0,0 +1,11 @@
.vc-smyn-suffix {
color: var(--text-muted);
}
.vc-smyn-suffix::before {
content: "(";
}
.vc-smyn-suffix::after {
content: ")";
}

View file

@ -17,22 +17,41 @@
*/ */
import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents";
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";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { Button, ButtonLooks, ButtonWrapperClasses, React, Tooltip } from "@webpack/common"; import { Button, ButtonLooks, ButtonWrapperClasses, React, Tooltip } from "@webpack/common";
let lastState = false;
const settings = definePluginSettings({
persistState: {
type: OptionType.BOOLEAN,
description: "Whether to persist the state of the silent message toggle when changing channels",
default: false,
onChange(newValue: boolean) {
if (newValue === false) lastState = false;
}
}
});
function SilentMessageToggle(chatBoxProps: { function SilentMessageToggle(chatBoxProps: {
type: { type: {
analyticsName: string; analyticsName: string;
}; };
}) { }) {
const [enabled, setEnabled] = React.useState(false); const [enabled, setEnabled] = React.useState(lastState);
function setEnabledValue(value: boolean) {
if (settings.store.persistState) lastState = value;
setEnabled(value);
}
React.useEffect(() => { React.useEffect(() => {
const listener: SendListener = (_, message) => { const listener: SendListener = (_, message) => {
if (enabled) { if (enabled) {
setEnabled(false); setEnabledValue(false);
if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content; if (!message.content.startsWith("@silent ")) message.content = "@silent " + message.content;
} }
}; };
@ -44,16 +63,16 @@ function SilentMessageToggle(chatBoxProps: {
if (chatBoxProps.type.analyticsName !== "normal") return null; if (chatBoxProps.type.analyticsName !== "normal") return null;
return ( return (
<Tooltip text="Toggle Silent Message"> <Tooltip text={enabled ? "Disable Silent Message" : "Enable Silent Message"}>
{tooltipProps => ( {tooltipProps => (
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<Button <Button
{...tooltipProps} {...tooltipProps}
onClick={() => setEnabled(prev => !prev)} onClick={() => setEnabledValue(!enabled)}
size="" size=""
look={ButtonLooks.BLANK} look={ButtonLooks.BLANK}
innerClassName={ButtonWrapperClasses.button} innerClassName={ButtonWrapperClasses.button}
style={{ margin: "0px 8px" }} style={{ padding: "0 8px" }}
> >
<div className={ButtonWrapperClasses.buttonWrapper}> <div className={ButtonWrapperClasses.buttonWrapper}>
<svg <svg
@ -79,6 +98,7 @@ export default definePlugin({
name: "SilentMessageToggle", name: "SilentMessageToggle",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz],
description: "Adds a button to the chat bar to toggle sending a silent message.", description: "Adds a button to the chat bar to toggle sending a silent message.",
settings,
patches: [ patches: [
{ {
find: ".activeCommandOption", find: ".activeCommandOption",

View file

@ -48,7 +48,7 @@ function SilentTypingToggle(chatBoxProps: {
if (chatBoxProps.type.analyticsName !== "normal") return null; if (chatBoxProps.type.analyticsName !== "normal") return null;
return ( return (
<Tooltip text={isEnabled ? "Disable silent typing" : "Enable silent typing"}> <Tooltip text={isEnabled ? "Disable Silent Typing" : "Enable Silent Typing"}>
{(tooltipProps: any) => ( {(tooltipProps: any) => (
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<Button <Button
@ -57,7 +57,7 @@ function SilentTypingToggle(chatBoxProps: {
size="" size=""
look={ButtonLooks.BLANK} look={ButtonLooks.BLANK}
innerClassName={ButtonWrapperClasses.button} innerClassName={ButtonWrapperClasses.button}
style={{ margin: "0 8px 0" }} style={{ padding: "0 8px" }}
> >
<div className={ButtonWrapperClasses.buttonWrapper}> <div className={ButtonWrapperClasses.buttonWrapper}>
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"> <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">

View file

@ -0,0 +1,16 @@
.vc-spotify-button-row {
height: 0;
opacity: 0;
pointer-events: none;
transition: 0.2s;
transition-property: height;
}
#vc-spotify-player:hover .vc-spotify-button-row {
opacity: 1;
height: 32px;
pointer-events: auto;
/* only transition opacity on show to prevent clipping */
transition-property: height, opacity;
}

View file

@ -17,27 +17,20 @@
*/ */
import { Settings } from "@api/settings"; import { Settings } from "@api/settings";
import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import hoverOnlyStyle from "./hoverOnly.css?managed";
import { Player } from "./PlayerComponent"; import { Player } from "./PlayerComponent";
function toggleHoverControls(value: boolean) { function toggleHoverControls(value: boolean) {
document.getElementById("vc-spotify-hover-controls")?.remove(); (value ? enableStyle : disableStyle)(hoverOnlyStyle);
if (value) {
const style = document.createElement("style");
style.id = "vc-spotify-hover-controls";
style.textContent = `
.vc-spotify-button-row { height: 0; opacity: 0; will-change: height, opacity; transition: height .2s, opacity .05s; }
#vc-spotify-player:hover .vc-spotify-button-row { opacity: 1; height: 32px; }
`;
document.head.appendChild(style);
}
} }
export default definePlugin({ export default definePlugin({
name: "SpotifyControls", name: "SpotifyControls",
description: "Spotify Controls", description: "Adds a Spotify player above the account panel",
authors: [Devs.Ven, Devs.afn, Devs.KraXen72], authors: [Devs.Ven, Devs.afn, Devs.KraXen72],
options: { options: {
hoverControls: { hoverControls: {

View file

@ -21,7 +21,7 @@ import { Devs, SUPPORT_CHANNEL_ID } from "@utils/constants";
import { makeCodeblock } from "@utils/misc"; import { makeCodeblock } from "@utils/misc";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { isOutdated } from "@utils/updater"; import { isOutdated } from "@utils/updater";
import { Alerts, FluxDispatcher, Forms, UserStore } from "@webpack/common"; import { Alerts, Forms, UserStore } from "@webpack/common";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
import plugins from "~plugins"; import plugins from "~plugins";
@ -69,18 +69,16 @@ ${makeCodeblock(Object.keys(plugins).filter(Vencord.Plugins.isPluginEnabled).joi
} }
}], }],
rememberDismiss() { flux: {
DataStore.set(REMEMBER_DISMISS_KEY, gitHash); async CHANNEL_SELECT({ channelId }) {
},
start() {
FluxDispatcher.subscribe("CHANNEL_SELECT", async ({ channelId }) => {
if (channelId !== SUPPORT_CHANNEL_ID) return; if (channelId !== SUPPORT_CHANNEL_ID) return;
const myId = BigInt(UserStore.getCurrentUser().id); const myId = BigInt(UserStore.getCurrentUser().id);
if (Object.values(Devs).some(d => d.id === myId)) return; if (Object.values(Devs).some(d => d.id === myId)) return;
if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) { if (isOutdated && gitHash !== await DataStore.get(REMEMBER_DISMISS_KEY)) {
const rememberDismiss = () => DataStore.set(REMEMBER_DISMISS_KEY, gitHash);
Alerts.show({ Alerts.show({
title: "Hold on!", title: "Hold on!",
body: <div> body: <div>
@ -90,10 +88,10 @@ ${makeCodeblock(Object.keys(plugins).filter(Vencord.Plugins.isPluginEnabled).joi
to do so, in case you can't access the Updater page. to do so, in case you can't access the Updater page.
</Forms.FormText> </Forms.FormText>
</div>, </div>,
onCancel: this.rememberDismiss, onCancel: rememberDismiss,
onConfirm: this.rememberDismiss onConfirm: rememberDismiss
}); });
} }
}); }
} }
}); });

View file

@ -24,7 +24,7 @@ import { findByCodeLazy } from "@webpack";
import { GuildMemberStore, React, RelationshipStore, SelectedChannelStore } from "@webpack/common"; import { GuildMemberStore, React, RelationshipStore, SelectedChannelStore } from "@webpack/common";
import { User } from "discord-types/general"; import { User } from "discord-types/general";
const Avatar = findByCodeLazy('"top",spacing:'); const Avatar = findByCodeLazy(".typingIndicatorRef", "svg");
const openProfile = findByCodeLazy("friendToken", "USER_PROFILE_MODAL_OPEN"); const openProfile = findByCodeLazy("friendToken", "USER_PROFILE_MODAL_OPEN");
const settings = definePluginSettings({ const settings = definePluginSettings({

View file

@ -23,7 +23,7 @@ import definePlugin from "@utils/types";
export default definePlugin({ export default definePlugin({
name: "UrbanDictionary", name: "UrbanDictionary",
description: "Searches for a word on Urban Dictionary", description: "Search for a word on Urban Dictionary via /urban slash command",
authors: [Devs.jewdev], authors: [Devs.jewdev],
dependencies: ["CommandsAPI"], dependencies: ["CommandsAPI"],
commands: [ commands: [

View file

@ -57,11 +57,13 @@ const VoiceChannelField = ErrorBoundary.wrap(({ user }: UserProps) => {
const result = `${guild.name} | ${channel.name}`; const result = `${guild.name} | ${channel.name}`;
return ( return (
<div style={{ marginBottom: 14 }}>
<VoiceChannelSection <VoiceChannelSection
channel={channel} channel={channel}
label={result} label={result}
showHeader={settings.store.showVoiceChannelSectionHeader} showHeader={settings.store.showVoiceChannelSectionHeader}
/> />
</div>
); );
}); });

View file

@ -0,0 +1,31 @@
:is([class*="userProfile"], [class*="userPopout"]) [class*="bannerPremium"] {
background: center / cover no-repeat;
position: relative;
z-index: -1;
}
[class*="userPopout"] [class*="NonPremium"] [class*="bannerPremium"] {
top: -30px;
}
[class*="NonPremium"]:has([class*="bannerPremium"]) [class*="bannerSVGWrapper"] {
min-height: 120px !important;
}
[class*="NonPremium"]:has([class*="bannerPremium"]) [class*="bannerSVGWrapper"] foreignObject {
height: 360px;
}
[class*="userPopout"] [class*="NonPremium"]:has([class*="bannerPremium"]) [class*="bannerSVGWrapper"] rect {
height: 120px;
y: -30;
}
[class*="userPopout"] [class*="NonPremium"]:has([class*="bannerPremium"]) [class*="bannerSVGWrapper"] circle {
cy: 86;
}
[class*="NonPremium"]:has([class*="bannerPremium"]) [class*="avatarPositionNormal"],
[class*="PremiumWithoutBanner"]:has([class*="bannerPremium"]) [class*="avatarPositionPremiumNoBanner"] {
top: 76px;
}

View file

@ -0,0 +1,75 @@
/*
* 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 { enableStyle } from "@api/Styles";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import style from "./index.css?managed";
const BASE_URL = "https://raw.githubusercontent.com/AutumnVN/usrbg/main/usrbg.json";
let data = {} as Record<string, string>;
const settings = definePluginSettings({
nitroFirst: {
description: "Banner to use if both Nitro and USRBG banners are present",
type: OptionType.SELECT,
options: [
{ label: "Nitro banner", value: true, default: true },
{ label: "USRBG banner", value: false },
]
}
});
export default definePlugin({
name: "USRBG",
description: "USRBG is a community maintained database of Discord banners, allowing anyone to get a banner without requiring Nitro",
authors: [Devs.AutumnVN, Devs.pylix],
settings,
patches: [
{
find: ".bannerSrc,",
replacement: {
match: /(\i)\.bannerSrc,/,
replace: "$self.useBannerHook($1),"
}
}
],
settingsAboutComponent: () => {
return (
<Link href="https://github.com/AutumnVN/usrbg#how-to-request-your-own-usrbg-banner">CLICK HERE TO GET YOUR OWN BANNER</Link>
);
},
useBannerHook({ displayProfile, user }: any) {
if (displayProfile?.banner && settings.store.nitroFirst) return;
if (data[user.id]) return data[user.id];
},
async start() {
enableStyle(style);
const res = await fetch(BASE_URL);
if (res.ok)
data = await res.json();
}
});

View file

@ -24,7 +24,7 @@ import { Margins } from "@utils/margins";
import { wordsToTitle } from "@utils/text"; import { wordsToTitle } from "@utils/text";
import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types"; import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types";
import { findByPropsLazy } from "@webpack"; import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, FluxDispatcher, Forms, SelectedChannelStore, useMemo, UserStore } from "@webpack/common"; import { Button, ChannelStore, Forms, SelectedChannelStore, useMemo, UserStore } from "@webpack/common";
interface VoiceState { interface VoiceState {
userId: string; userId: string;
@ -137,7 +137,19 @@ function updateStatuses(type: string, { deaf, mute, selfDeaf, selfMute, userId,
} }
*/ */
function handleVoiceStates({ voiceStates }: { voiceStates: VoiceState[]; }) { function playSample(tempSettings: any, type: string) {
const settings = Object.assign({}, Settings.plugins.VcNarrator, tempSettings);
speak(formatText(settings[type + "Message"], UserStore.getCurrentUser().username, "general"), settings);
}
export default definePlugin({
name: "VcNarrator",
description: "Announces when users join, leave, or move voice channels via narrator",
authors: [Devs.Ven],
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
const myChanId = SelectedChannelStore.getVoiceChannelId(); const myChanId = SelectedChannelStore.getVoiceChannelId();
const myId = UserStore.getCurrentUser().id; const myId = UserStore.getCurrentUser().id;
@ -160,18 +172,18 @@ function handleVoiceStates({ voiceStates }: { voiceStates: VoiceState[]; }) {
// updateStatuses(type, state, isMe); // updateStatuses(type, state, isMe);
} }
} },
function handleToggleSelfMute() { AUDIO_TOGGLE_SELF_MUTE() {
const chanId = SelectedChannelStore.getVoiceChannelId()!; const chanId = SelectedChannelStore.getVoiceChannelId()!;
const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState; const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState;
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));
} },
function handleToggleSelfDeafen() { AUDIO_TOGGLE_SELF_DEAF() {
const chanId = SelectedChannelStore.getVoiceChannelId()!; const chanId = SelectedChannelStore.getVoiceChannelId()!;
const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState; const s = VoiceStateStore.getVoiceStateForChannel(chanId) as VoiceState;
if (!s) return; if (!s) return;
@ -179,32 +191,16 @@ function handleToggleSelfDeafen() {
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));
} }
function playSample(tempSettings: any, type: string) {
const settings = Object.assign({}, Settings.plugins.VcNarrator, tempSettings);
speak(formatText(settings[type + "Message"], UserStore.getCurrentUser().username, "general"), settings);
}
export default definePlugin({
name: "VcNarrator",
description: "Announces when users join, leave, or move voice channels via narrator",
authors: [Devs.Ven],
start() {
if (speechSynthesis.getVoices().length === 0) {
new Logger("VcNarrator").warn("No Narrator voices found. Thus, this plugin will not work. Check my Settings for more info");
return;
}
FluxDispatcher.subscribe("VOICE_STATE_UPDATES", handleVoiceStates);
FluxDispatcher.subscribe("AUDIO_TOGGLE_SELF_MUTE", handleToggleSelfMute);
FluxDispatcher.subscribe("AUDIO_TOGGLE_SELF_DEAF", handleToggleSelfDeafen);
}, },
stop() { start() {
FluxDispatcher.unsubscribe("VOICE_STATE_UPDATES", handleVoiceStates); if (typeof speechSynthesis === "undefined" || speechSynthesis.getVoices().length === 0) {
FluxDispatcher.subscribe("AUDIO_TOGGLE_SELF_MUTE", handleToggleSelfMute); new Logger("VcNarrator").warn(
FluxDispatcher.subscribe("AUDIO_TOGGLE_SELF_DEAF", handleToggleSelfDeafen); "SpeechSynthesis not supported or no Narrator voices found. Thus, this plugin will not work. Check my Settings for more info"
);
return;
}
}, },
optionsCache: null as Record<string, PluginOptionsItem> | null, optionsCache: null as Record<string, PluginOptionsItem> | null,
@ -214,11 +210,11 @@ export default definePlugin({
voice: { voice: {
type: OptionType.SELECT, type: OptionType.SELECT,
description: "Narrator Voice", description: "Narrator Voice",
options: speechSynthesis.getVoices().map(v => ({ options: window.speechSynthesis?.getVoices().map(v => ({
label: v.name, label: v.name,
value: v.voiceURI, value: v.voiceURI,
default: v.default default: v.default
})) })) ?? []
}, },
volume: { volume: {
type: OptionType.SLIDER, type: OptionType.SLIDER,

View file

@ -16,28 +16,57 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
import { definePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { LazyComponent } from "@utils/misc"; import { LazyComponent } from "@utils/misc";
import { ModalRoot, ModalSize, openModal } from "@utils/modal"; import { ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { find, findByCode, findByPropsLazy } from "@webpack"; import { find, findByCode, findByPropsLazy } from "@webpack";
import { Menu } from "@webpack/common"; import { GuildMemberStore, Menu } from "@webpack/common";
import type { Guild } from "discord-types/general"; import type { Channel, Guild, User } from "discord-types/general";
const ImageModal = LazyComponent(() => findByCode(".MEDIA_MODAL_CLOSE,")); const ImageModal = LazyComponent(() => findByCode(".MEDIA_MODAL_CLOSE,"));
const MaskedLink = LazyComponent(() => find(m => m.type?.toString().includes("MASKED_LINK)"))); const MaskedLink = LazyComponent(() => find(m => m.type?.toString().includes("MASKED_LINK)")));
const BannerStore = findByPropsLazy("getGuildBannerURL");
const GuildBannerStore = findByPropsLazy("getGuildBannerURL"); interface UserContextProps {
channel: Channel;
guildId?: string;
user: User;
}
const OPEN_URL = "Vencord.Plugins.plugins.ViewIcons.openImage("; interface GuildContextProps {
export default definePlugin({ guild: Guild;
name: "ViewIcons", }
authors: [Devs.Ven],
description: "Makes Avatars/Banners in user profiles clickable, and adds Guild Context Menu Entries to View Banner/Icon.",
openImage(url: string) { const settings = definePluginSettings({
const u = new URL(url); format: {
type: OptionType.SELECT,
description: "Choose the image format to use for non animated images. Animated images will always use .gif",
options: [
{
label: "webp",
value: "webp",
default: true
},
{
label: "png",
value: "png",
},
{
label: "jpg",
value: "jpg",
}
]
}
});
function openImage(url: string) {
const format = url.startsWith("/") ? "png" : settings.store.format;
const u = new URL(url, window.location.href);
u.searchParams.set("size", "512"); u.searchParams.set("size", "512");
u.pathname = u.pathname.replace(/\.(png|jpe?g|webp)$/, `.${format}`);
url = u.toString(); url = u.toString();
openModal(modalProps => ( openModal(modalProps => (
@ -50,6 +79,91 @@ export default definePlugin({
/> />
</ModalRoot> </ModalRoot>
)); ));
}
const UserContext: NavContextMenuPatchCallback = (children, { user, guildId }: UserContextProps) => () => {
const memberAvatar = GuildMemberStore.getMember(guildId!, user.id)?.avatar || null;
children.splice(1, 0, (
<Menu.MenuGroup>
<Menu.MenuItem
id="view-avatar"
label="View Avatar"
action={() => openImage(BannerStore.getUserAvatarURL(user, true, 512))}
/>
{memberAvatar && (
<Menu.MenuItem
id="view-server-avatar"
label="View Server Avatar"
action={() => openImage(BannerStore.getGuildMemberAvatarURLSimple({
userId: user.id,
avatar: memberAvatar,
guildId
}, true))}
/>
)}
</Menu.MenuGroup>
));
};
const GuildContext: NavContextMenuPatchCallback = (children, { guild: { id, icon, banner } }: GuildContextProps) => () => {
if (!banner && !icon) return;
// before copy id (if it exists)
const idx = children.length +
children[children.length - 1]?.props?.children?.props?.id === "devmode-copy-id"
? -2
: -1;
children.splice(idx, 0, (
<Menu.MenuGroup>
{icon ? (
<Menu.MenuItem
id="view-icon"
label="View Icon"
action={() =>
openImage(BannerStore.getGuildIconURL({
id,
icon,
size: 512,
canAnimate: true
}))
}
/>
) : null}
{banner ? (
<Menu.MenuItem
id="view-banner"
label="View Banner"
action={() =>
openImage(BannerStore.getGuildBannerURL({
id,
banner,
}, true))
}
/>
) : null}
</Menu.MenuGroup>
));
};
export default definePlugin({
name: "ViewIcons",
authors: [Devs.Ven, Devs.TheKodeToad, Devs.Nuckyz],
description: "Makes avatars and banners in user profiles clickable, and adds View Icon/Banner entries in the user and server context menu",
settings,
openImage,
start() {
addContextMenuPatch("user-context", UserContext);
addContextMenuPatch("guild-context", GuildContext);
},
stop() {
removeContextMenuPatch("user-context", UserContext);
removeContextMenuPatch("guild-context", GuildContext);
}, },
patches: [ patches: [
@ -57,52 +171,23 @@ export default definePlugin({
find: "onAddFriend:", find: "onAddFriend:",
replacement: { replacement: {
// global because Discord has two components that are 99% identical with one small change ._. // global because Discord has two components that are 99% identical with one small change ._.
match: /\{src:(.{1,2}),avatarDecoration/g, match: /\{src:(\i),avatarDecoration/g,
replace: (_, src) => `{src:${src},onClick:()=>${OPEN_URL}${src}),avatarDecoration` replace: "{src:$1,onClick:()=>$self.openImage($1),avatarDecoration"
} }
}, { }, {
find: ".popoutNoBannerPremium", find: ".popoutNoBannerPremium",
replacement: { replacement: {
match: /style:.{0,10}\{\},(.{1,2})\)/, match: /style:.{0,10}\{\},(\i)\)/,
replace: (m, style) => replace:
`onClick:${style}.backgroundImage&&(${style}.cursor="pointer",` + "onClick:$1.backgroundImage&&($1.cursor=\"pointer\"," +
`()=>${OPEN_URL}${style}.backgroundImage.replace("url(", ""))),${m}` "()=>$self.openImage($1.backgroundImage.replace(\"url(\", \"\"))),$&"
} }
}, { }, {
find: '"GuildContextMenu:', find: "().avatarWrapperNonUserBot",
replacement: [ replacement: {
{ match: /(avatarPositionPanel.+?)onClick:(\i\|\|\i)\?void 0(?<=,(\i)=\i\.avatarSrc.+?)/,
match: /\w=(\w)\.id/, replace: "$1style:($2)?{cursor:\"pointer\"}:{},onClick:$2?()=>{$self.openImage($3)}"
replace: "_guild=$1,$&" }
},
{
match: /(id:"leave-guild".{0,200}),(\(0,.{1,3}\.jsxs?\).{0,200}function)/,
replace: "$1,$self.buildGuildContextMenuEntries(_guild),$2"
} }
] ]
}
],
buildGuildContextMenuEntries(guild: Guild) {
return (
<Menu.MenuGroup>
{guild.banner && (
<Menu.MenuItem
id="view-banner"
key="view-banner"
label="View Banner"
action={() => this.openImage(GuildBannerStore.getGuildBannerURL(guild))}
/>
)}
{guild.icon && (
<Menu.MenuItem
id="view-icon"
key="view-icon"
label="View Icon"
action={() => this.openImage(guild.getIconURL(0, true))}
/>
)}
</Menu.MenuGroup>
);
}
}); });

View file

@ -0,0 +1,185 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ContextMenu, FluxDispatcher, Menu } from "@webpack/common";
import { Channel, Message } from "discord-types/general";
interface Sticker {
id: string;
format_type: number;
description: string;
name: string;
}
enum GreetMode {
Greet = "Greet",
NormalMessage = "Message"
}
const settings = definePluginSettings({
greetMode: {
type: OptionType.SELECT,
options: [
{ label: "Greet (you can only greet 3 times)", value: GreetMode.Greet, default: true },
{ label: "Normal Message (you can greet spam)", value: GreetMode.NormalMessage }
],
description: "Choose the greet mode"
}
});
const MessageActions = findByPropsLazy("sendGreetMessage");
function greet(channel: Channel, message: Message, stickers: string[]) {
const options = MessageActions.getSendMessageOptionsForReply({
channel,
message,
shouldMention: true,
showMentionToggle: true
});
if (settings.store.greetMode === GreetMode.NormalMessage || stickers.length > 1) {
options.stickerIds = stickers;
const msg = {
content: "",
tts: false,
invalidEmojis: [],
validNonShortcutEmojis: []
};
MessageActions._sendMessage(channel.id, msg, options);
} else {
MessageActions.sendGreetMessage(channel.id, stickers[0], options);
}
}
function GreetMenu({ stickers, channel, message }: { stickers: Sticker[], message: Message, channel: Channel; }) {
const s = settings.use(["greetMode", "multiGreetChoices"] as any) as { greetMode: GreetMode, multiGreetChoices: string[]; };
const { greetMode, multiGreetChoices = [] } = s;
return (
<Menu.Menu
navId="greet-sticker-picker"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label="Greet Sticker Picker"
>
<Menu.MenuGroup
label="Greet Mode"
>
{Object.values(GreetMode).map(mode => (
<Menu.MenuRadioItem
key={mode}
group="greet-mode"
id={"greet-mode-" + mode}
label={mode}
checked={mode === greetMode}
action={() => s.greetMode = mode}
/>
))}
</Menu.MenuGroup>
<Menu.MenuSeparator />
<Menu.MenuGroup
label="Greet Stickers"
>
{stickers.map(sticker => (
<Menu.MenuItem
key={sticker.id}
id={"greet-" + sticker.id}
label={sticker.description.split(" ")[0]}
action={() => greet(channel, message, [sticker.id])}
/>
))}
</Menu.MenuGroup>
{!(settings.store as any).unholyMultiGreetEnabled ? null : (
<>
<Menu.MenuSeparator />
<Menu.MenuItem
label="Unholy Multi-Greet"
id="unholy-multi-greet"
>
{stickers.map(sticker => {
const checked = multiGreetChoices.some(s => s === sticker.id);
return (
<Menu.MenuCheckboxItem
key={sticker.id}
id={"multi-greet-" + sticker.id}
label={sticker.description.split(" ")[0]}
checked={checked}
disabled={!checked && multiGreetChoices.length >= 3}
action={() => {
s.multiGreetChoices = checked
? multiGreetChoices.filter(s => s !== sticker.id)
: [...multiGreetChoices, sticker.id];
}}
/>
);
})}
<Menu.MenuSeparator />
<Menu.MenuItem
id="multi-greet-submit"
label="Send Greets"
action={() => greet(channel, message, multiGreetChoices!)}
disabled={multiGreetChoices.length === 0}
/>
</Menu.MenuItem>
</>
)}
</Menu.Menu>
);
}
export default definePlugin({
name: "GreetStickerPicker",
description: "Allows you to use any greet sticker instead of only the random one by right-clicking the 'Wave to say hi!' button",
authors: [Devs.Ven],
settings,
patches: [
{
find: "Messages.WELCOME_CTA_LABEL",
replacement: {
match: /innerClassName:\i\(\).welcomeCTAButton,(?<=%\i\.length;return (\i)\[\i\].+?)/,
replace: "$&onContextMenu:(e)=>$self.pickSticker(e,$1,arguments[0]),"
}
}
],
pickSticker(
event: React.UIEvent,
stickers: Sticker[],
props: {
channel: Channel,
message: Message;
}
) {
if (!(props.message as any).deleted)
ContextMenu.open(event, () => <GreetMenu stickers={stickers} {...props} />);
}
});

View file

@ -257,5 +257,21 @@ export const Devs = /* #__PURE__*/ Object.freeze({
pylix: { pylix: {
name: "pylix", name: "pylix",
id: 492949202121261067n id: 492949202121261067n
},
Tyler: {
name: "\\\\GGTyler\\\\",
id: 143117463788191746n
},
RyanCaoDev: {
name: "RyanCaoDev",
id: 952235800110694471n,
},
Strencher: {
name: "Strencher",
id: 415849376598982656n
},
FieryFlames: {
name: "Fiery",
id: 890228870559698955n
} }
}); });

View file

@ -16,9 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ChannelStore, GuildStore, PrivateChannelsStore, SelectedChannelStore } from "@webpack/common"; import { findLazy } from "@webpack";
import { ChannelStore, ComponentDispatch, GuildStore, PrivateChannelsStore, SelectedChannelStore } from "@webpack/common";
import { Guild } from "discord-types/general"; import { Guild } from "discord-types/general";
const PreloadedUserSettings = findLazy(m => m.ProtoClass?.typeName.endsWith("PreloadedUserSettings"));
export function getCurrentChannel() { export function getCurrentChannel() {
return ChannelStore.getChannel(SelectedChannelStore.getChannelId()); return ChannelStore.getChannel(SelectedChannelStore.getChannelId());
} }
@ -30,3 +33,19 @@ export function getCurrentGuild(): Guild | undefined {
export function openPrivateChannel(userId: string) { export function openPrivateChannel(userId: string) {
PrivateChannelsStore.openPrivateChannel(userId); PrivateChannelsStore.openPrivateChannel(userId);
} }
export const enum Theme {
Dark = 1,
Light = 2
}
export function getTheme(): Theme {
return PreloadedUserSettings.getCurrentValue()?.appearance?.theme;
}
export function insertTextIntoChatInputBox(text: string) {
ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", {
rawText: text,
plainText: text
});
}

View file

@ -204,3 +204,7 @@ export const checkIntersecting = (el: Element) => {
export function identity<T>(value: T): T { export function identity<T>(value: T): T {
return value; return value;
} }
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_tablet_or_desktop
// "In summary, we recommend looking for the string Mobi anywhere in the User Agent to detect a mobile device."
export const isMobile = navigator.userAgent.includes("Mobi");

View file

@ -35,6 +35,7 @@ export async function importSettings(data: string) {
} }
if ("settings" in parsed && "quickCss" in parsed) { if ("settings" in parsed && "quickCss" in parsed) {
Object.assign(PlainSettings, parsed.settings);
await VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(parsed.settings, null, 4)); await VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(parsed.settings, null, 4));
await VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, parsed.quickCss); await VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, parsed.quickCss);
} else } else
@ -152,7 +153,8 @@ export async function putCloudSettings() {
showNotification({ showNotification({
title: "Cloud Settings", title: "Cloud Settings",
body: "Synchronized your settings to the cloud!", body: "Synchronized your settings to the cloud!",
color: "var(--green-360)" color: "var(--green-360)",
noPersist: true
}); });
} catch (e: any) { } catch (e: any) {
cloudSettingsLogger.error("Failed to sync up", e); cloudSettingsLogger.error("Failed to sync up", e);
@ -180,7 +182,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
if (shouldNotify) if (shouldNotify)
showNotification({ showNotification({
title: "Cloud Settings", title: "Cloud Settings",
body: "There are no settings in the cloud." body: "There are no settings in the cloud.",
noPersist: true
}); });
return false; return false;
} }
@ -190,7 +193,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
if (shouldNotify) if (shouldNotify)
showNotification({ showNotification({
title: "Cloud Settings", title: "Cloud Settings",
body: "Your settings are up to date." body: "Your settings are up to date.",
noPersist: true
}); });
return false; return false;
} }
@ -213,7 +217,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
if (shouldNotify) if (shouldNotify)
showNotification({ showNotification({
title: "Cloud Settings", title: "Cloud Settings",
body: "Your local settings are newer than the cloud ones." body: "Your local settings are newer than the cloud ones.",
noPersist: true,
}); });
return; return;
} }
@ -233,7 +238,8 @@ export async function getCloudSettings(shouldNotify = true, force = false) {
title: "Cloud Settings", title: "Cloud Settings",
body: "Your settings have been updated! Click here to restart to fully apply changes!", body: "Your settings have been updated! Click here to restart to fully apply changes!",
color: "var(--green-360)", color: "var(--green-360)",
onClick: () => window.DiscordNative.app.relaunch() onClick: () => window.DiscordNative.app.relaunch(),
noPersist: true
}); });
return true; return true;

View file

@ -17,6 +17,7 @@
*/ */
import { Command } from "@api/Commands"; import { Command } from "@api/Commands";
import { FluxEvents } from "@webpack/types";
import { Promisable } from "type-fest"; import { Promisable } from "type-fest";
// exists to export default definePlugin({...}) // exists to export default definePlugin({...})
@ -101,6 +102,12 @@ export interface PluginDef {
settingsAboutComponent?: React.ComponentType<{ settingsAboutComponent?: React.ComponentType<{
tempSettings?: Record<string, any>; tempSettings?: Record<string, any>;
}>; }>;
/**
* Allows you to subscribe to Flux events
*/
flux?: {
[E in FluxEvents]?: (event: any) => void;
};
} }
export enum OptionType { export enum OptionType {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type { ComponentType, CSSProperties, PropsWithChildren, UIEvent } from "react"; import type { ComponentType, CSSProperties, MouseEvent, PropsWithChildren, UIEvent } from "react";
type RC<C> = ComponentType<PropsWithChildren<C & Record<string, any>>>; type RC<C> = ComponentType<PropsWithChildren<C & Record<string, any>>>;
@ -30,10 +30,14 @@ export interface Menu {
onSelect?(): void; onSelect?(): void;
}>; }>;
MenuSeparator: ComponentType; MenuSeparator: ComponentType;
MenuGroup: RC<any>; MenuGroup: RC<{
label?: string;
}>;
MenuItem: RC<{ MenuItem: RC<{
id: string; id: string;
label: string; label: string;
action?(e: MouseEvent): void;
render?: ComponentType; render?: ComponentType;
onChildrenScroll?: Function; onChildrenScroll?: Function;
childRowHeight?: number; childRowHeight?: number;
@ -41,9 +45,18 @@ export interface Menu {
}>; }>;
MenuCheckboxItem: RC<{ MenuCheckboxItem: RC<{
id: string; id: string;
label: string;
checked: boolean;
action?(e: MouseEvent): void;
disabled?: boolean;
}>; }>;
MenuRadioItem: RC<{ MenuRadioItem: RC<{
id: string; id: string;
group: string;
label: string;
checked: boolean;
action?(e: MouseEvent): void;
disabled?: boolean;
}>; }>;
MenuControlItem: RC<{ MenuControlItem: RC<{
id: string; id: string;

View file

@ -68,7 +68,7 @@ export interface SnowflakeUtils {
extractTimestamp(snowflake: string): number; extractTimestamp(snowflake: string): number;
age(snowflake: string): number; age(snowflake: string): number;
atPreviousMillisecond(snowflake: string): string; atPreviousMillisecond(snowflake: string): string;
compare(snowflake1: string, snowflake2: string): number; compare(snowflake1?: string, snowflake2?: string): number;
} }
interface RestRequestData { interface RestRequestData {

View file

@ -23,6 +23,7 @@ import { _resolveReady, filters, findByCodeLazy, findByPropsLazy, findLazy, mapM
import type * as t from "./types/utils"; import type * as t from "./types/utils";
export let FluxDispatcher: t.FluxDispatcher; export let FluxDispatcher: t.FluxDispatcher;
export const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT);
export const RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get"); export const RestAPI: t.RestAPI = findByPropsLazy("getAPIBaseURL", "get");
export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear"); export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear");