,
+ message: Message
) {
- const items = [] as React.ComponentType[];
+ const items: React.ReactNode[] = [];
for (const [identifier, getItem] of buttons.entries()) {
try {
- const item = getItem(msg);
+ const item = getItem(message);
if (item) {
item.key ??= identifier;
- items.push(makeButton(item));
+ items.push(
+
+
+
+ );
}
} catch (err) {
logger.error(`[${identifier}]`, err);
}
}
- return items;
+ return <>{items}>;
}
diff --git a/src/components/ExpandableHeader.css b/src/components/ExpandableHeader.css
index 14e291b06..a556e36be 100644
--- a/src/components/ExpandableHeader.css
+++ b/src/components/ExpandableHeader.css
@@ -1,7 +1,6 @@
.vc-expandableheader-center-flex {
display: flex;
- justify-items: center;
- align-items: center;
+ place-items: center;
}
.vc-expandableheader-btn {
diff --git a/src/main/updater/http.ts b/src/main/updater/http.ts
index 0738a8c24..9d42b5c69 100644
--- a/src/main/updater/http.ts
+++ b/src/main/updater/http.ts
@@ -16,6 +16,7 @@
* along with this program. If not, see .
*/
+import { get } from "@main/utils/simpleGet";
import { IpcEvents } from "@shared/IpcEvents";
import { VENCORD_USER_AGENT } from "@shared/vencordUserAgent";
import { ipcMain } from "electron";
@@ -25,7 +26,6 @@ import { join } from "path";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
-import { get } from "../utils/simpleGet";
import { serializeErrors, VENCORD_FILES } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
diff --git a/src/modules.d.ts b/src/modules.d.ts
index 7566a5bf4..b6e6b2476 100644
--- a/src/modules.d.ts
+++ b/src/modules.d.ts
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-// eslint-disable-next-line spaced-comment
///
declare module "~plugins" {
diff --git a/src/plugins/_api/messagePopover.ts b/src/plugins/_api/messagePopover.ts
index 42a1bb765..57b9b1193 100644
--- a/src/plugins/_api/messagePopover.ts
+++ b/src/plugins/_api/messagePopover.ts
@@ -26,9 +26,8 @@ export default definePlugin({
patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
- // foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
- match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"(?<=message:(\i).+?)/,
- replace: (m, makeElement, msg) => `...Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}),${m}`
+ match: /\.jsx\)\((\i\.\i),\{label:\i\.\i\.Messages\.MESSAGE_ACTION_REPLY.{0,200}?"reply-self".{0,50}?\}\):null(?=,.+?message:(\i))/,
+ replace: "$&,Vencord.Api.MessagePopover._buildPopoverElements($1,$2)"
}
}],
});
diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx
index d0e8cf34c..072f4ee66 100644
--- a/src/plugins/betterFolders/index.tsx
+++ b/src/plugins/betterFolders/index.tsx
@@ -249,6 +249,10 @@ export default definePlugin({
dispatchingFoldersClose = false;
});
}
+ },
+
+ LOGOUT() {
+ closeFolders();
}
},
diff --git a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx
index 5fbe165ce..6501e0feb 100644
--- a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx
+++ b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx
@@ -8,7 +8,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
-import { classes } from "@utils/misc";
+import { classes, copyWithToast } from "@utils/misc";
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findComponentByCodeLazy } from "@webpack";
import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common";
@@ -45,7 +45,11 @@ interface Section {
authorIds?: string[];
}
-function SectionHeader({ section }: { section: Section; }) {
+interface SectionHeaderProps {
+ section: Section;
+}
+
+function SectionHeader({ section }: SectionHeaderProps) {
const hasSubtitle = typeof section.subtitle !== "undefined";
const hasAuthorIds = typeof section.authorIds !== "undefined";
@@ -62,6 +66,7 @@ function SectionHeader({ section }: { section: Section; }) {
})();
}, [section.authorIds]);
+
return
{section.title}
@@ -74,8 +79,7 @@ function SectionHeader({ section }: { section: Section; }) {
size={16}
showUserPopout
className={Margins.bottom8}
- />
- }
+ />}
{hasSubtitle &&
@@ -204,7 +208,16 @@ function ChangeDecorationModal(props: ModalProps) {
{activeSelectedDecoration?.alt}
}
- {activeDecorationHasAuthor && Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}}
+ {activeDecorationHasAuthor && (
+
+ Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}
+
+ )}
+ {isActiveDecorationPreset && (
+
+ )}
diff --git a/src/plugins/fakeProfileThemes/index.tsx b/src/plugins/fakeProfileThemes/index.tsx
index 9e784da68..ab240837b 100644
--- a/src/plugins/fakeProfileThemes/index.tsx
+++ b/src/plugins/fakeProfileThemes/index.tsx
@@ -57,7 +57,7 @@ function decode(bio: string): Array | null {
if (bio == null) return null;
const colorString = bio.match(
- /\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u,
+ /\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e005d}/u,
);
if (colorString != null) {
const parsed = [...colorString[0]]
diff --git a/src/plugins/forceOwnerCrown/index.ts b/src/plugins/forceOwnerCrown/index.ts
index 771583fe7..444bfab34 100644
--- a/src/plugins/forceOwnerCrown/index.ts
+++ b/src/plugins/forceOwnerCrown/index.ts
@@ -27,7 +27,7 @@ export default definePlugin({
authors: [Devs.D3SOX, Devs.Nickyux],
patches: [
{
- find: ".PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP",
+ find: ".Messages.GUILD_OWNER,",
replacement: {
match: /,isOwner:(\i),/,
replace: ",_isOwner:$1=$self.isGuildOwner(e),"
diff --git a/src/plugins/maskedLinkPaste/README.md b/src/plugins/maskedLinkPaste/README.md
deleted file mode 100644
index 30beccb64..000000000
--- a/src/plugins/maskedLinkPaste/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# MaskedLinkPaste
-
-Pasting a link while you have text selected will paste your link as a masked link at that location
-
-![](https://github.com/Vendicated/Vencord/assets/78964224/1d3be2c6-7957-44c9-92ec-551069d46c02)
diff --git a/src/plugins/maskedLinkPaste/index.ts b/src/plugins/maskedLinkPaste/index.ts
deleted file mode 100644
index bcd622edb..000000000
--- a/src/plugins/maskedLinkPaste/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Vencord, a Discord client mod
- * Copyright (c) 2023 Vendicated and contributors
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-import { Devs } from "@utils/constants.js";
-import definePlugin from "@utils/types";
-import { findByPropsLazy } from "@webpack";
-
-const linkRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
-
-const SlateTransforms = findByPropsLazy("insertText", "selectCommandOption");
-
-export default definePlugin({
- name: "MaskedLinkPaste",
- authors: [Devs.TheSun],
- description: "Pasting a link while having text selected will paste a hyperlink",
- patches: [
- {
- find: ".selection,preventEmojiSurrogates:",
- replacement: {
- match: /(?<=\i.delete.{0,50})(\i)\.insertText\((\i)\)/,
- replace: "$self.handlePaste($1, $2, () => $&)"
- }
- }
- ],
-
- handlePaste(editor, content: string, originalBehavior: () => void) {
- if (content && linkRegex.test(content) && editor.operations?.[0]?.type === "remove_text") {
- SlateTransforms.insertText(
- editor,
- `[${editor.operations[0].text}](${content})`
- );
- }
- else originalBehavior();
- }
-});
diff --git a/src/plugins/mentionAvatars/index.tsx b/src/plugins/mentionAvatars/index.tsx
index 549693142..311303ab9 100644
--- a/src/plugins/mentionAvatars/index.tsx
+++ b/src/plugins/mentionAvatars/index.tsx
@@ -6,12 +6,21 @@
import "./styles.css";
+import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
-import definePlugin from "@utils/types";
+import definePlugin, { OptionType } from "@utils/types";
import { SelectedGuildStore, useState } from "@webpack/common";
import { User } from "discord-types/general";
+const settings = definePluginSettings({
+ showAtSymbol: {
+ type: OptionType.BOOLEAN,
+ description: "Whether the the @ symbol should be displayed",
+ default: true
+ }
+});
+
export default definePlugin({
name: "MentionAvatars",
description: "Shows user avatars inside mentions",
@@ -25,11 +34,13 @@ export default definePlugin({
}
}],
+ settings,
+
renderUsername: ErrorBoundary.wrap((props: { user: User, username: string; }) => {
const { user, username } = props;
const [isHovering, setIsHovering] = useState(false);
- if (!user) return <>@{username}>;
+ if (!user) return <>{getUsernameString(username)}>;
return (
setIsHovering(false)}
>
- @{username}
+ {getUsernameString(username)}
);
}, { noop: true })
+
});
+
+function getUsernameString(username: string) {
+ return settings.store.showAtSymbol
+ ? `@${username}`
+ : username;
+}
diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx
index 9181306ad..a08aeccce 100644
--- a/src/plugins/messageLogger/index.tsx
+++ b/src/plugins/messageLogger/index.tsx
@@ -151,6 +151,7 @@ export default definePlugin({
contextMenus: {
"message": patchMessageContextMenu,
"channel-context": patchChannelContextMenu,
+ "thread-context": patchChannelContextMenu,
"user-context": patchChannelContextMenu,
"gdm-context": patchChannelContextMenu
},
diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx
index 0fbf41e93..278501438 100644
--- a/src/plugins/mutualGroupDMs/index.tsx
+++ b/src/plugins/mutualGroupDMs/index.tsx
@@ -49,7 +49,7 @@ export default definePlugin({
find: ".Messages.MUTUAL_GUILDS_WITH_END_COUNT", // Note: the module is lazy-loaded
replacement: {
match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/,
- replace: '$self.isBotOrSelf(arguments[0].user)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),'
+ replace: '$self.isBotOrSelf(arguments[0].user)?null:$1"MUTUAL_GDMS",children:$self.getMutualGDMCountText(arguments[0].user)}),'
}
},
{
@@ -64,7 +64,7 @@ export default definePlugin({
replacement: [
{
match: /(?<=onItemSelect:\i,children:)(\i)\.map/,
- replace: "[...$1, ...($self.isBotOrSelf(arguments[0].user) ? [] : [{section:'MUTUAL_GDMS',text:'Mutual Groups'}])].map"
+ replace: "[...$1, ...($self.isBotOrSelf(arguments[0].user) ? [] : [{section:'MUTUAL_GDMS',text:$self.getMutualGDMCountText(arguments[0].user)}])].map"
},
{
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
@@ -76,6 +76,11 @@ export default definePlugin({
isBotOrSelf: (user: User) => user.bot || user.id === UserStore.getCurrentUser().id,
+ getMutualGDMCountText: (user: User) => {
+ const count = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).length;
+ return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
+ },
+
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
`${m}let{shcChannel}=${props};`
},
{
diff --git a/src/plugins/showHiddenThings/index.ts b/src/plugins/showHiddenThings/index.ts
index b6c8e4104..f01dc2107 100644
--- a/src/plugins/showHiddenThings/index.ts
+++ b/src/plugins/showHiddenThings/index.ts
@@ -68,7 +68,7 @@ export default definePlugin({
},
// fixes a bug where Members page must be loaded to see highest role, why is Discord depending on MemberSafetyStore.getEnhancedMember for something that can be obtained here?
{
- find: "Messages.GUILD_MEMBER_MOD_VIEW_PERMISSION_GRANTED_BY_ARIA_LABEL,tooltipContentClassName",
+ find: "Messages.GUILD_MEMBER_MOD_VIEW_PERMISSION_GRANTED_BY_ARIA_LABEL,allowOverflow",
predicate: () => settings.store.showModView,
replacement: {
match: /(role:)\i(?=,guildId.{0,100}role:(\i\[))/,
diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx
index 105b3b18a..aef0c7362 100644
--- a/src/plugins/spotifyControls/PlayerComponent.tsx
+++ b/src/plugins/spotifyControls/PlayerComponent.tsx
@@ -165,7 +165,6 @@ function SeekBar() {
const [position, setPosition] = useState(storePosition);
- // eslint-disable-next-line consistent-return
useEffect(() => {
if (isPlaying && !isSettingPosition) {
setPosition(SpotifyStore.position);
@@ -358,7 +357,7 @@ export function Player() {
const [shouldHide, setShouldHide] = useState(false);
// Hide player after 5 minutes of inactivity
- // eslint-disable-next-line consistent-return
+
React.useEffect(() => {
setShouldHide(false);
if (!isPlaying) {
diff --git a/src/plugins/spotifyControls/spotifyStyles.css b/src/plugins/spotifyControls/spotifyStyles.css
index 72383c3e8..893dc8175 100644
--- a/src/plugins/spotifyControls/spotifyStyles.css
+++ b/src/plugins/spotifyControls/spotifyStyles.css
@@ -101,9 +101,8 @@
display: flex;
flex-direction: column;
padding: 0.2rem;
- justify-content: center;
align-items: flex-start;
- align-content: flex-start;
+ place-content: flex-start center;
overflow: hidden;
}
diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx
index b22c488eb..fa1d9abf6 100644
--- a/src/plugins/translate/TranslateIcon.tsx
+++ b/src/plugins/translate/TranslateIcon.tsx
@@ -17,10 +17,9 @@
*/
import { ChatBarButton } from "@api/ChatButtons";
-import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
-import { Alerts, Forms } from "@webpack/common";
+import { Alerts, Forms, Tooltip, useEffect, useState } from "@webpack/common";
import { settings } from "./settings";
import { TranslateModal } from "./TranslateModal";
@@ -39,9 +38,17 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
);
}
+export let setShouldShowTranslateEnabledTooltip: undefined | ((show: boolean) => void);
+
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
+ const [shouldShowTranslateEnabledTooltip, setter] = useState(false);
+ useEffect(() => {
+ setShouldShowTranslateEnabledTooltip = setter;
+ return () => setShouldShowTranslateEnabledTooltip = undefined;
+ }, []);
+
if (!isMainChat || !showChatBarButton) return null;
const toggle = () => {
@@ -52,21 +59,20 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
title: "Vencord Auto-Translate Enabled",
body: <>
- You just enabled auto translate (by right clicking the Translate icon). Any message you send will automatically be translated before being sent.
-
-
- If this was an accident, disable it again, or it will change your message content before sending.
+ You just enabled Auto Translate! Any message will automatically be translated before being sent.
>,
- cancelText: "Disable Auto-Translate",
- confirmText: "Got it",
+ confirmText: "Disable Auto-Translate",
+ cancelText: "Got it",
secondaryConfirmText: "Don't show again",
onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false,
- onCancel: () => settings.store.autoTranslate = false
+ onConfirm: () => settings.store.autoTranslate = false,
+ // troll
+ confirmColor: "vc-notification-log-danger-btn",
});
};
- return (
+ const button = (
{
@@ -76,7 +82,7 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
));
}}
- onContextMenu={() => toggle()}
+ onContextMenu={toggle}
buttonProps={{
"aria-haspopup": "dialog"
}}
@@ -84,4 +90,13 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
);
+
+ if (shouldShowTranslateEnabledTooltip && settings.store.showAutoTranslateTooltip)
+ return (
+
+ {() => button}
+
+ );
+
+ return button;
};
diff --git a/src/plugins/translate/TranslateModal.tsx b/src/plugins/translate/TranslateModal.tsx
index 7628a31e0..7a32d1b75 100644
--- a/src/plugins/translate/TranslateModal.tsx
+++ b/src/plugins/translate/TranslateModal.tsx
@@ -20,9 +20,8 @@ import { Margins } from "@utils/margins";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { Forms, SearchableSelect, Switch, useMemo } from "@webpack/common";
-import { Languages } from "./languages";
import { settings } from "./settings";
-import { cl } from "./utils";
+import { cl, getLanguages } from "./utils";
const LanguageSettingKeys = ["receivedInput", "receivedOutput", "sentInput", "sentOutput"] as const;
@@ -31,7 +30,7 @@ function LanguageSelect({ settingsKey, includeAuto }: { settingsKey: typeof Lang
const options = useMemo(
() => {
- const options = Object.entries(Languages).map(([value, label]) => ({ value, label }));
+ const options = Object.entries(getLanguages()).map(([value, label]) => ({ value, label }));
if (!includeAuto)
options.shift();
diff --git a/src/plugins/translate/TranslationAccessory.tsx b/src/plugins/translate/TranslationAccessory.tsx
index 72b35940d..8e8f4c174 100644
--- a/src/plugins/translate/TranslationAccessory.tsx
+++ b/src/plugins/translate/TranslationAccessory.tsx
@@ -19,7 +19,6 @@
import { Parser, useEffect, useState } from "@webpack/common";
import { Message } from "discord-types/general";
-import { Languages } from "./languages";
import { TranslateIcon } from "./TranslateIcon";
import { cl, TranslationValue } from "./utils";
@@ -59,7 +58,7 @@ export function TranslationAccessory({ message }: { message: Message; }) {
{Parser.parse(translation.text)}
{" "}
- (translated from {Languages[translation.src] ?? translation.src} - setTranslation(undefined)} />)
+ (translated from {translation.sourceLanguage} - setTranslation(undefined)} />)
);
}
diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx
index f602d1255..de61cef9c 100644
--- a/src/plugins/translate/index.tsx
+++ b/src/plugins/translate/index.tsx
@@ -28,7 +28,7 @@ import definePlugin from "@utils/types";
import { ChannelStore, Menu } from "@webpack/common";
import { settings } from "./settings";
-import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
+import { setShouldShowTranslateEnabledTooltip, TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
import { handleTranslate, TranslationAccessory } from "./TranslationAccessory";
import { translate } from "./utils";
@@ -53,8 +53,8 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) =>
export default definePlugin({
name: "Translate",
- description: "Translate messages with Google Translate",
- authors: [Devs.Ven],
+ description: "Translate messages with Google Translate or DeepL",
+ authors: [Devs.Ven, Devs.AshtonMemer],
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
settings,
contextMenus: {
@@ -83,11 +83,18 @@ export default definePlugin({
};
});
+ let tooltipTimeout: any;
this.preSend = addPreSendListener(async (_, message) => {
if (!settings.store.autoTranslate) return;
if (!message.content) return;
- message.content = (await translate("sent", message.content)).text;
+ setShouldShowTranslateEnabledTooltip?.(true);
+ clearTimeout(tooltipTimeout);
+ tooltipTimeout = setTimeout(() => setShouldShowTranslateEnabledTooltip?.(false), 2000);
+
+ const trans = await translate("sent", message.content);
+ message.content = trans.text;
+
});
},
diff --git a/src/plugins/translate/languages.ts b/src/plugins/translate/languages.ts
index 4bf370b5c..3e2e7c711 100644
--- a/src/plugins/translate/languages.ts
+++ b/src/plugins/translate/languages.ts
@@ -31,9 +31,10 @@ copy(Object.fromEntries(
))
*/
-export type Language = keyof typeof Languages;
+export type GoogleLanguage = keyof typeof GoogleLanguages;
+export type DeeplLanguage = keyof typeof DeeplLanguages;
-export const Languages = {
+export const GoogleLanguages = {
"auto": "Detect language",
"af": "Afrikaans",
"sq": "Albanian",
@@ -169,3 +170,57 @@ export const Languages = {
"yo": "Yoruba",
"zu": "Zulu"
} as const;
+
+export const DeeplLanguages = {
+ "": "Detect language",
+ "ar": "Arabic",
+ "bg": "Bulgarian",
+ "zh-hans": "Chinese (Simplified)",
+ "zh-hant": "Chinese (Traditional)",
+ "cs": "Czech",
+ "da": "Danish",
+ "nl": "Dutch",
+ "en-us": "English (American)",
+ "en-gb": "English (British)",
+ "et": "Estonian",
+ "fi": "Finnish",
+ "fr": "French",
+ "de": "German",
+ "el": "Greek",
+ "hu": "Hungarian",
+ "id": "Indonesian",
+ "it": "Italian",
+ "ja": "Japanese",
+ "ko": "Korean",
+ "lv": "Latvian",
+ "lt": "Lithuanian",
+ "nb": "Norwegian",
+ "pl": "Polish",
+ "pt-br": "Portuguese (Brazilian)",
+ "pt-pt": "Portuguese (European)",
+ "ro": "Romanian",
+ "ru": "Russian",
+ "sk": "Slovak",
+ "sl": "Slovenian",
+ "es": "Spanish",
+ "sv": "Swedish",
+ "tr": "Turkish",
+ "uk": "Ukrainian"
+} as const;
+
+export function deeplLanguageToGoogleLanguage(language: string) {
+ switch (language) {
+ case "": return "auto";
+ case "nb": return "no";
+ case "zh-hans": return "zh-CN";
+ case "zh-hant": return "zh-TW";
+ case "en-us":
+ case "en-gb":
+ return "en";
+ case "pt-br":
+ case "pt-pt":
+ return "pt";
+ default:
+ return language;
+ }
+}
diff --git a/src/plugins/translate/native.ts b/src/plugins/translate/native.ts
new file mode 100644
index 000000000..3415e95e9
--- /dev/null
+++ b/src/plugins/translate/native.ts
@@ -0,0 +1,29 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { IpcMainInvokeEvent } from "electron";
+
+export async function makeDeeplTranslateRequest(_: IpcMainInvokeEvent, pro: boolean, apiKey: string, payload: string) {
+ const url = pro
+ ? "https://api.deepl.com/v2/translate"
+ : "https://api-free.deepl.com/v2/translate";
+
+ try {
+ const res = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": `DeepL-Auth-Key ${apiKey}`
+ },
+ body: payload
+ });
+
+ const data = await res.text();
+ return { status: res.status, data };
+ } catch (e) {
+ return { status: -1, data: String(e) };
+ }
+}
diff --git a/src/plugins/translate/settings.ts b/src/plugins/translate/settings.ts
index 65d845353..916c70bd2 100644
--- a/src/plugins/translate/settings.ts
+++ b/src/plugins/translate/settings.ts
@@ -22,38 +22,76 @@ import { OptionType } from "@utils/types";
export const settings = definePluginSettings({
receivedInput: {
type: OptionType.STRING,
- description: "Input language for received messages",
+ description: "Language that received messages should be translated from",
default: "auto",
hidden: true
},
receivedOutput: {
type: OptionType.STRING,
- description: "Output language for received messages",
+ description: "Language that received messages should be translated to",
default: "en",
hidden: true
},
sentInput: {
type: OptionType.STRING,
- description: "Input language for sent messages",
+ description: "Language that your own messages should be translated from",
default: "auto",
hidden: true
},
sentOutput: {
type: OptionType.STRING,
- description: "Output language for sent messages",
+ description: "Language that your own messages should be translated to",
default: "en",
hidden: true
},
+
+ showChatBarButton: {
+ type: OptionType.BOOLEAN,
+ description: "Show translate button in chat bar",
+ default: true
+ },
+ service: {
+ type: OptionType.SELECT,
+ description: IS_WEB ? "Translation service (Not supported on Web!)" : "Translation service",
+ disabled: () => IS_WEB,
+ options: [
+ { label: "Google Translate", value: "google", default: true },
+ { label: "DeepL Free", value: "deepl" },
+ { label: "DeepL Pro", value: "deepl-pro" }
+ ] as const,
+ onChange: resetLanguageDefaults
+ },
+ deeplApiKey: {
+ type: OptionType.STRING,
+ description: "DeepL API key",
+ default: "",
+ placeholder: "Get your API key from https://deepl.com/your-account",
+ disabled: () => IS_WEB
+ },
autoTranslate: {
type: OptionType.BOOLEAN,
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
default: false
},
- showChatBarButton: {
+ showAutoTranslateTooltip: {
type: OptionType.BOOLEAN,
- description: "Show translate button in chat bar",
+ description: "Show a tooltip on the ChatBar button whenever a message is automatically translated",
default: true
- }
+ },
}).withPrivateSettings<{
showAutoTranslateAlert: boolean;
}>();
+
+export function resetLanguageDefaults() {
+ if (IS_WEB || settings.store.service === "google") {
+ settings.store.receivedInput = "auto";
+ settings.store.receivedOutput = "en";
+ settings.store.sentInput = "auto";
+ settings.store.sentOutput = "en";
+ } else {
+ settings.store.receivedInput = "";
+ settings.store.receivedOutput = "en-us";
+ settings.store.sentInput = "";
+ settings.store.sentOutput = "en-us";
+ }
+}
diff --git a/src/plugins/translate/styles.css b/src/plugins/translate/styles.css
index e9085b4ee..64b6c9b9f 100644
--- a/src/plugins/translate/styles.css
+++ b/src/plugins/translate/styles.css
@@ -3,8 +3,7 @@
}
.vc-trans-modal-header {
- justify-content: space-between;
- align-content: center;
+ place-content: center space-between;
}
.vc-trans-modal-header h1 {
diff --git a/src/plugins/translate/utils.ts b/src/plugins/translate/utils.ts
index 493fb2ca2..aff64e8a6 100644
--- a/src/plugins/translate/utils.ts
+++ b/src/plugins/translate/utils.ts
@@ -17,12 +17,18 @@
*/
import { classNameFactory } from "@api/Styles";
+import { onlyOnce } from "@utils/onlyOnce";
+import { PluginNative } from "@utils/types";
+import { showToast, Toasts } from "@webpack/common";
-import { settings } from "./settings";
+import { DeeplLanguages, deeplLanguageToGoogleLanguage, GoogleLanguages } from "./languages";
+import { resetLanguageDefaults, settings } from "./settings";
export const cl = classNameFactory("vc-trans-");
-interface TranslationData {
+const Native = VencordNative.pluginHelpers.Translate as PluginNative;
+
+interface GoogleData {
src: string;
sentences: {
// 🏳️⚧️
@@ -30,15 +36,47 @@ interface TranslationData {
}[];
}
+interface DeeplData {
+ translations: {
+ detected_source_language: string;
+ text: string;
+ }[];
+}
+
export interface TranslationValue {
- src: string;
+ sourceLanguage: string;
text: string;
}
-export async function translate(kind: "received" | "sent", text: string): Promise {
- const sourceLang = settings.store[kind + "Input"];
- const targetLang = settings.store[kind + "Output"];
+export const getLanguages = () => IS_WEB || settings.store.service === "google"
+ ? GoogleLanguages
+ : DeeplLanguages;
+export async function translate(kind: "received" | "sent", text: string): Promise {
+ const translate = IS_WEB || settings.store.service === "google"
+ ? googleTranslate
+ : deeplTranslate;
+
+ try {
+ return await translate(
+ text,
+ settings.store[`${kind}Input`],
+ settings.store[`${kind}Output`]
+ );
+ } catch (e) {
+ const userMessage = typeof e === "string"
+ ? e
+ : "Something went wrong. If this issue persists, please check the console or ask for help in the support server.";
+
+ showToast(userMessage, Toasts.Type.FAILURE);
+
+ throw e instanceof Error
+ ? e
+ : new Error(userMessage);
+ }
+}
+
+async function googleTranslate(text: string, sourceLang: string, targetLang: string): Promise {
const url = "https://translate.googleapis.com/translate_a/single?" + new URLSearchParams({
// see https://stackoverflow.com/a/29537590 for more params
// holy shidd nvidia
@@ -63,13 +101,69 @@ export async function translate(kind: "received" | "sent", text: string): Promis
+ `\n${res.status} ${res.statusText}`
);
- const { src, sentences }: TranslationData = await res.json();
+ const { src, sentences }: GoogleData = await res.json();
return {
- src,
+ sourceLanguage: GoogleLanguages[src] ?? src,
text: sentences.
map(s => s?.trans).
filter(Boolean).
join("")
};
}
+
+function fallbackToGoogle(text: string, sourceLang: string, targetLang: string): Promise {
+ return googleTranslate(
+ text,
+ deeplLanguageToGoogleLanguage(sourceLang),
+ deeplLanguageToGoogleLanguage(targetLang)
+ );
+}
+
+const showDeeplApiQuotaToast = onlyOnce(
+ () => showToast("Deepl API quota exceeded. Falling back to Google Translate", Toasts.Type.FAILURE)
+);
+
+async function deeplTranslate(text: string, sourceLang: string, targetLang: string): Promise {
+ if (!settings.store.deeplApiKey) {
+ showToast("DeepL API key is not set. Resetting to Google", Toasts.Type.FAILURE);
+
+ settings.store.service = "google";
+ resetLanguageDefaults();
+
+ return fallbackToGoogle(text, sourceLang, targetLang);
+ }
+
+ // CORS jumpscare
+ const { status, data } = await Native.makeDeeplTranslateRequest(
+ settings.store.service === "deepl-pro",
+ settings.store.deeplApiKey,
+ JSON.stringify({
+ text: [text],
+ target_lang: targetLang,
+ source_lang: sourceLang.split("-")[0]
+ })
+ );
+
+ switch (status) {
+ case 200:
+ break;
+ case -1:
+ throw "Failed to connect to DeepL API: " + data;
+ case 403:
+ throw "Invalid DeepL API key or version";
+ case 456:
+ showDeeplApiQuotaToast();
+ return fallbackToGoogle(text, sourceLang, targetLang);
+ default:
+ throw new Error(`Failed to translate "${text}" (${sourceLang} -> ${targetLang})\n${status} ${data}`);
+ }
+
+ const { translations }: DeeplData = JSON.parse(data);
+ const src = translations[0].detected_source_language;
+
+ return {
+ sourceLanguage: DeeplLanguages[src] ?? src,
+ text: translations[0].text
+ };
+}
diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx
index 56c285ecd..83c560df2 100644
--- a/src/plugins/viewRaw/index.tsx
+++ b/src/plugins/viewRaw/index.tsx
@@ -153,6 +153,7 @@ export default definePlugin({
contextMenus: {
"guild-context": MakeContextCallback("Guild"),
"channel-context": MakeContextCallback("Channel"),
+ "thread-context": MakeContextCallback("Channel"),
"user-context": MakeContextCallback("User")
},
diff --git a/src/plugins/xsOverlay/index.tsx b/src/plugins/xsOverlay/index.tsx
index ab76a0e79..12dbf6b61 100644
--- a/src/plugins/xsOverlay/index.tsx
+++ b/src/plugins/xsOverlay/index.tsx
@@ -8,7 +8,7 @@ import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
-import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
+import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
import { findByCodeLazy, findLazy } from "@webpack";
import { Button, ChannelStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
@@ -102,6 +102,12 @@ const settings = definePluginSettings({
await start();
}
},
+ preferUDP: {
+ type: OptionType.BOOLEAN,
+ description: "Enable if you use an older build of XSOverlay unable to connect through websockets. This setting is ignored on web.",
+ default: false,
+ disabled: () => IS_WEB
+ },
botNotifications: {
type: OptionType.BOOLEAN,
description: "Allow bot notifications",
@@ -178,6 +184,8 @@ async function start() {
});
}
+const Native = VencordNative.pluginHelpers.XSOverlay as PluginNative;
+
export default definePlugin({
name: "XSOverlay",
description: "Forwards discord notifications to XSOverlay, for easy viewing in VR",
@@ -349,6 +357,10 @@ function sendOtherNotif(content: string, titleString: string) {
}
async function sendToOverlay(notif: NotificationObject) {
+ if (!IS_WEB && settings.store.preferUDP) {
+ Native.sendToOverlay(notif);
+ return;
+ }
const apiObject: ApiObject = {
sender: "Vencord",
target: "xsoverlay",
diff --git a/src/plugins/xsOverlay/native.ts b/src/plugins/xsOverlay/native.ts
new file mode 100644
index 000000000..75ddfeea5
--- /dev/null
+++ b/src/plugins/xsOverlay/native.ts
@@ -0,0 +1,16 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2023 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { createSocket, Socket } from "dgram";
+
+let xsoSocket: Socket;
+
+export function sendToOverlay(_, data: any) {
+ data.messageType = data.type;
+ const json = JSON.stringify(data);
+ xsoSocket ??= createSocket("udp4");
+ xsoSocket.send(json, 42069, "127.0.0.1");
+}
diff --git a/src/plugins/watchTogetherAdblock.desktop/README.md b/src/plugins/youtubeAdblock.desktop/README.md
similarity index 76%
rename from src/plugins/watchTogetherAdblock.desktop/README.md
rename to src/plugins/youtubeAdblock.desktop/README.md
index 4c64df67b..d6235f36e 100644
--- a/src/plugins/watchTogetherAdblock.desktop/README.md
+++ b/src/plugins/youtubeAdblock.desktop/README.md
@@ -1,6 +1,7 @@
# WatchTogetherAdblock
-Block ads in the YouTube WatchTogether activity via AdGuard
+Block ads in YouTube embeds and the WatchTogether activity via AdGuard
Note that this only works for yourself, other users in the activity will still see ads.
+
Powered by a modified version of [Adguard's BlockYoutubeAdsShortcut](https://github.com/AdguardTeam/BlockYouTubeAdsShortcut)
diff --git a/src/plugins/watchTogetherAdblock.desktop/adguard.js b/src/plugins/youtubeAdblock.desktop/adguard.js
similarity index 77%
rename from src/plugins/watchTogetherAdblock.desktop/adguard.js
rename to src/plugins/youtubeAdblock.desktop/adguard.js
index 945f76bd5..8f809c3d3 100644
--- a/src/plugins/watchTogetherAdblock.desktop/adguard.js
+++ b/src/plugins/youtubeAdblock.desktop/adguard.js
@@ -19,7 +19,6 @@
* along with AdGuard's Block YouTube Ads. If not, see .
*/
-const LOGO_ID = "block-youtube-ads-logo";
const hiddenCSS = [
"#__ffYoutube1",
"#__ffYoutube2",
@@ -98,7 +97,7 @@ const hideElements = () => {
}
const rule = selectors.join(", ") + " { display: none!important; }";
const style = document.createElement("style");
- style.innerHTML = rule;
+ style.textContent = rule;
document.head.appendChild(style);
};
/**
@@ -165,11 +164,9 @@ const overrideObject = (obj, propertyName, overrideValue) => {
}
let overriden = false;
for (const key in obj) {
- // eslint-disable-next-line no-prototype-builtins
if (obj.hasOwnProperty(key) && key === propertyName) {
obj[key] = overrideValue;
overriden = true;
- // eslint-disable-next-line no-prototype-builtins
} else if (obj.hasOwnProperty(key) && typeof obj[key] === "object") {
if (overrideObject(obj[key], propertyName, overrideValue)) {
overriden = true;
@@ -195,68 +192,25 @@ const jsonOverride = (propertyName, overrideValue) => {
return obj;
};
// Override Response.prototype.json
- const nativeResponseJson = Response.prototype.json;
- Response.prototype.json = new Proxy(nativeResponseJson, {
- apply(...args) {
+ Response.prototype.json = new Proxy(Response.prototype.json, {
+ async apply(...args) {
// Call the target function, get the original Promise
- const promise = Reflect.apply(...args);
+ const result = await Reflect.apply(...args);
// Create a new one and override the JSON inside
- return new Promise((resolve, reject) => {
- promise.then(data => {
- overrideObject(data, propertyName, overrideValue);
- resolve(data);
- }).catch(error => reject(error));
- });
+ overrideObject(result, propertyName, overrideValue);
+ return result;
},
});
};
-const addAdGuardLogoStyle = () => { };
-const addAdGuardLogo = () => {
- if (document.getElementById(LOGO_ID)) {
- return;
- }
- const logo = document.createElement("span");
- logo.innerHTML = "__logo_text__";
- logo.setAttribute("id", LOGO_ID);
- if (window.location.hostname === "m.youtube.com") {
- const btn = document.querySelector("header.mobile-topbar-header > button");
- if (btn) {
- btn.parentNode?.insertBefore(logo, btn.nextSibling);
- addAdGuardLogoStyle();
- }
- } else if (window.location.hostname === "www.youtube.com") {
- const code = document.getElementById("country-code");
- if (code) {
- code.innerHTML = "";
- code.appendChild(logo);
- addAdGuardLogoStyle();
- }
- } else if (window.location.hostname === "music.youtube.com") {
- const el = document.querySelector(".ytmusic-nav-bar#left-content");
- if (el) {
- el.appendChild(logo);
- addAdGuardLogoStyle();
- }
- } else if (window.location.hostname === "www.youtube-nocookie.com") {
- const code = document.querySelector("#yt-masthead #logo-container .content-region");
- if (code) {
- code.innerHTML = "";
- code.appendChild(logo);
- addAdGuardLogoStyle();
- }
- }
-};
// Removes ads metadata from YouTube XHR requests
jsonOverride("adPlacements", []);
jsonOverride("playerAds", []);
// Applies CSS that hides YouTube ad elements
hideElements();
// Some changes should be re-evaluated on every page change
-addAdGuardLogo();
hideDynamicAds();
autoSkipAds();
observeDomChanges(() => {
- addAdGuardLogo();
hideDynamicAds();
autoSkipAds();
});
diff --git a/src/plugins/watchTogetherAdblock.desktop/index.ts b/src/plugins/youtubeAdblock.desktop/index.ts
similarity index 53%
rename from src/plugins/watchTogetherAdblock.desktop/index.ts
rename to src/plugins/youtubeAdblock.desktop/index.ts
index 2dbc13d4a..708b908d9 100644
--- a/src/plugins/watchTogetherAdblock.desktop/index.ts
+++ b/src/plugins/youtubeAdblock.desktop/index.ts
@@ -4,12 +4,14 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
+import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
// The entire code of this plugin can be found in native.ts
+migratePluginSettings("YoutubeAdblock", "WatchTogetherAdblock");
export default definePlugin({
- name: "WatchTogetherAdblock",
- description: "Block ads in the YouTube WatchTogether activity via AdGuard",
- authors: [Devs.ImLvna],
+ name: "YoutubeAdblock",
+ description: "Block ads in YouTube embeds and the WatchTogether activity via AdGuard",
+ authors: [Devs.ImLvna, Devs.Ven],
});
diff --git a/src/plugins/watchTogetherAdblock.desktop/native.ts b/src/plugins/youtubeAdblock.desktop/native.ts
similarity index 69%
rename from src/plugins/watchTogetherAdblock.desktop/native.ts
rename to src/plugins/youtubeAdblock.desktop/native.ts
index c4106c349..8cc6a3232 100644
--- a/src/plugins/watchTogetherAdblock.desktop/native.ts
+++ b/src/plugins/youtubeAdblock.desktop/native.ts
@@ -11,9 +11,9 @@ import adguard from "file://adguard.js?minify";
app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => {
- if (frame.url.includes("discordsays") && frame.url.includes("youtube.com")) {
- if (!RendererSettings.store.plugins?.WatchTogetherAdblock?.enabled) return;
+ if (!RendererSettings.store.plugins?.YoutubeAdblock?.enabled) return;
+ if (frame.url.includes("youtube.com/embed/") || (frame.url.includes("discordsays") && frame.url.includes("youtube.com"))) {
frame.executeJavaScript(adguard);
}
});
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 18cad3a7a..95e4fdaff 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -542,6 +542,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Joona",
id: 297410829589020673n
},
+ AshtonMemer: {
+ name: "AshtonMemer",
+ id: 373657230530052099n
+ },
surgedevs: {
name: "Chloe",
id: 1084592643784331324n
diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts
index 58c279f61..8a2807ffe 100644
--- a/src/webpack/common/components.ts
+++ b/src/webpack/common/components.ts
@@ -16,7 +16,6 @@
* along with this program. If not, see .
*/
-// eslint-disable-next-line path-alias/no-relative
import { filters, findByPropsLazy, waitFor } from "@webpack";
import { waitForComponent } from "./internal";
diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts
index d61a95d84..74813357a 100644
--- a/src/webpack/common/stores.ts
+++ b/src/webpack/common/stores.ts
@@ -66,7 +66,6 @@ export let DraftStore: t.DraftStore;
*
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
*/
-// eslint-disable-next-line prefer-destructuring
export const useStateFromStores: t.useStateFromStores = findByCodeLazy("useStateFromStores");
waitForStore("DraftStore", s => DraftStore = s);
diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts
index 272ecd94f..fcd4adb8a 100644
--- a/src/webpack/webpack.ts
+++ b/src/webpack/webpack.ts
@@ -299,7 +299,7 @@ export const lazyWebpackSearchHistory = [] as Array<["find" | "findByProps" | "f
* Note that the example below exists already as an api, see {@link findByPropsLazy}
* @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah);
*/
-export function proxyLazyWebpack(factory: () => any, attempts?: number) {
+export function proxyLazyWebpack(factory: () => T, attempts?: number) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]);
return proxyLazy(factory, attempts);
diff --git a/tsconfig.json b/tsconfig.json
index 8db0ab3c1..db6d0918d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,6 +4,7 @@
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
+ "allowJs": true,
"lib": [
"DOM",
"DOM.Iterable",
@@ -37,7 +38,8 @@
"transform": "typescript-transform-paths",
"afterDeclarations": true
}
- ]
+ ],
+ "outDir": "who-fucking-cares-dude"
},
- "include": ["src/**/*", "browser/**/*", "scripts/**/*"]
+ "include": ["src/**/*", "browser/**/*", "scripts/**/*", "eslint.config.mjs"],
}