From 3704c71ae1f37e4a3cd4dd3f24ad0acf78bb5d98 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 19 Jul 2024 00:46:48 -0300 Subject: [PATCH 01/21] Fix loading scientific notation chunks --- src/debug/loadLazyChunks.ts | 18 +++++++++--------- src/webpack/webpack.ts | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts index 64c3e0ea..d3484bd9 100644 --- a/src/debug/loadLazyChunks.ts +++ b/src/debug/loadLazyChunks.ts @@ -15,9 +15,9 @@ export async function loadLazyChunks() { try { LazyChunkLoaderLogger.log("Loading all chunks..."); - const validChunks = new Set(); - const invalidChunks = new Set(); - const deferredRequires = new Set(); + const validChunks = new Set(); + const invalidChunks = new Set(); + const deferredRequires = new Set(); let chunksSearchingResolve: (value: void | PromiseLike) => void; const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r); @@ -29,14 +29,14 @@ export async function loadLazyChunks() { async function searchAndLoadLazyChunks(factoryCode: string) { const lazyChunks = factoryCode.matchAll(LazyChunkRegex); - const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); + const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>(); // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before // the chunk containing the component const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { - const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : []; + const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : []; if (chunkIds.length === 0) { return; @@ -61,7 +61,7 @@ export async function loadLazyChunks() { } if (!invalidChunkGroup) { - validChunkGroups.add([chunkIds, entryPoint]); + validChunkGroups.add([chunkIds, Number(entryPoint)]); } })); @@ -131,14 +131,14 @@ export async function loadLazyChunks() { } // All chunks Discord has mapped to asset files, even if they are not used anymore - const allChunks = [] as string[]; + const allChunks = [] as number[]; // Matches "id" or id: - for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { + for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)")|(?:([\deE]+?):)/g)) { const id = currentMatch[1] ?? currentMatch[2]; if (id == null) continue; - allChunks.push(id); + allChunks.push(Number(id)); } if (allChunks.length === 0) throw new Error("Failed to get all chunks"); diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index f21a38d6..272ecd94 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -544,7 +544,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D } if (rawChunkIds) { - const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]); + const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => Number(m[1])); await Promise.all(chunkIds.map(id => wreq.e(id))); } @@ -559,7 +559,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D return false; } - wreq(entryPointId); + wreq(Number(entryPointId)); return true; } From 2044264729f6af2c9af1f236c2f3aaa00601cab3 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 23 Jul 2024 02:56:24 +0200 Subject: [PATCH 02/21] fix PictureInPicture on canary --- src/plugins/pictureInPicture/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/pictureInPicture/index.tsx b/src/plugins/pictureInPicture/index.tsx index 0a22f06d..81d75a25 100644 --- a/src/plugins/pictureInPicture/index.tsx +++ b/src/plugins/pictureInPicture/index.tsx @@ -30,7 +30,7 @@ export default definePlugin({ { find: ".nonMediaMosaicItem]", replacement: { - match: /\.nonMediaMosaicItem\]:!(\i).{0,10}children:\[(\S)/, + match: /\.nonMediaMosaicItem\]:!(\i).{0,50}?children:\[(\S)/, replace: "$&,$1&&$2&&$self.renderPiPButton()," }, }, From bc801853e21dc256e93f61c4644bf3647d0a33b9 Mon Sep 17 00:00:00 2001 From: Masterjoona <69722179+Masterjoona@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:46:00 +0300 Subject: [PATCH 03/21] Fix ImageZoom, SpotifyControls and ViewIcons (#2723) Co-authored-by: Vendicated --- src/plugins/clientTheme/index.tsx | 2 +- src/plugins/imageZoom/index.tsx | 2 +- src/plugins/spotifyControls/index.tsx | 2 +- src/plugins/viewIcons/index.tsx | 3 ++- src/utils/constants.ts | 6 +++++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx index b36a2cb8..358bae01 100644 --- a/src/plugins/clientTheme/index.tsx +++ b/src/plugins/clientTheme/index.tsx @@ -30,7 +30,7 @@ function onPickColor(color: number) { updateColorVars(hexColor); } -const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE",settings:{useSystemTheme:"system"==='); +const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"==='); function setTheme(theme: string) { saveClientTheme({ theme }); diff --git a/src/plugins/imageZoom/index.tsx b/src/plugins/imageZoom/index.tsx index 3f0e3294..da01f1a7 100644 --- a/src/plugins/imageZoom/index.tsx +++ b/src/plugins/imageZoom/index.tsx @@ -171,7 +171,7 @@ export default definePlugin({ find: ".handleImageLoad)", replacement: [ { - match: /placeholderVersion:\i,/, + match: /placeholderVersion:\i,(?=.{0,50}children:)/, replace: "...$self.makeProps(this),$&" }, diff --git a/src/plugins/spotifyControls/index.tsx b/src/plugins/spotifyControls/index.tsx index b811b2ee..ae5fbcd8 100644 --- a/src/plugins/spotifyControls/index.tsx +++ b/src/plugins/spotifyControls/index.tsx @@ -48,7 +48,7 @@ export default definePlugin({ }, patches: [ { - find: '"AccountConnected"', + find: "this.isCopiedStreakGodlike", replacement: { // react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah }) match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/, diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx index 6bde04be..927a974f 100644 --- a/src/plugins/viewIcons/index.tsx +++ b/src/plugins/viewIcons/index.tsx @@ -189,7 +189,8 @@ export default definePlugin({ replacement: { match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/, replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openImage($1)}," - } + }, + all: true }, // Old Profiles Modal pfp { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c399baaf..4de8e243 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -533,7 +533,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({ Antti: { name: "Antti", id: 312974985876471810n - } + }, + Joona: { + name: "Joona", + id: 297410829589020673n + }, } satisfies Record); // iife so #__PURE__ works correctly From 5e9a9fe836faad096d7e6b544bfd3f88e6255731 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 25 Jul 2024 13:49:01 +0200 Subject: [PATCH 04/21] bump to v1.9.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d8f9f97..720a780b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.9.5", + "version": "1.9.6", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From 5c88284ed3d089feeceb495607f5b948d51007b6 Mon Sep 17 00:00:00 2001 From: thororen <78185467+thororen1234@users.noreply.github.com> Date: Fri, 26 Jul 2024 11:55:32 -0400 Subject: [PATCH 05/21] feat(showHiddenChannels): Fix Broken Patch (#2726) --- src/plugins/showHiddenChannels/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 2d8b0c19..68778915 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -307,7 +307,7 @@ export default definePlugin({ ] }, { - find: '+1]})},"overflow"))', + find: '})},"overflow"))', replacement: [ { // Create a variable for the channel prop From 0f5cf37ef9e5661fea4c0aeb03af3cd749ac6267 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Wed, 31 Jul 2024 09:18:42 +1200 Subject: [PATCH 06/21] fix(ShowHiddenThings): always render highest role in ModView (#2709) --- src/plugins/showHiddenThings/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/plugins/showHiddenThings/index.ts b/src/plugins/showHiddenThings/index.ts index 90bb345e..b6c8e410 100644 --- a/src/plugins/showHiddenThings/index.ts +++ b/src/plugins/showHiddenThings/index.ts @@ -66,6 +66,15 @@ export default definePlugin({ replace: "return true", } }, + // 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", + predicate: () => settings.store.showModView, + replacement: { + match: /(role:)\i(?=,guildId.{0,100}role:(\i\[))/, + replace: "$1$2arguments[0].member.highestRoleId]", + } + }, { find: "prod_discoverable_guilds", predicate: () => settings.store.disableDiscoveryFilters, From 51ae019cd5c59a321caee2c3ab84a8dc88f0fbd6 Mon Sep 17 00:00:00 2001 From: Surge <112782958+surgedevs@users.noreply.github.com> Date: Tue, 30 Jul 2024 23:42:57 +0200 Subject: [PATCH 07/21] feat(plugins/openInApp) Refactor code and add Apple Music support (#2744) Co-authored-by: v Co-authored-by: Shiggy <136832773+shiggybot@users.noreply.github.com> --- src/main/utils/constants.ts | 3 +- src/plugins/openInApp/README.md | 11 +++ src/plugins/openInApp/index.ts | 148 +++++++++++++++----------------- src/utils/constants.ts | 4 + 4 files changed, 84 insertions(+), 82 deletions(-) create mode 100644 src/plugins/openInApp/README.md diff --git a/src/main/utils/constants.ts b/src/main/utils/constants.ts index 9513da51..42875202 100644 --- a/src/main/utils/constants.ts +++ b/src/main/utils/constants.ts @@ -35,7 +35,8 @@ export const ALLOWED_PROTOCOLS = [ "steam:", "spotify:", "com.epicgames.launcher:", - "tidal:" + "tidal:", + "itunes:", ]; export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla"); diff --git a/src/plugins/openInApp/README.md b/src/plugins/openInApp/README.md new file mode 100644 index 00000000..2b1385c6 --- /dev/null +++ b/src/plugins/openInApp/README.md @@ -0,0 +1,11 @@ +# OpenInApp + +Open links in their respective apps instead of your browser + +## Currently supports: + +- Spotify +- Steam +- EpicGames +- Tidal +- Apple Music (iTunes) diff --git a/src/plugins/openInApp/index.ts b/src/plugins/openInApp/index.ts index cb05324a..1a68e8f5 100644 --- a/src/plugins/openInApp/index.ts +++ b/src/plugins/openInApp/index.ts @@ -18,46 +18,70 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import definePlugin, { OptionType, PluginNative } from "@utils/types"; +import definePlugin, { OptionType, PluginNative, SettingsDefinition } from "@utils/types"; import { showToast, Toasts } from "@webpack/common"; import type { MouseEvent } from "react"; -const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/; -const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/; -const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/; -const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/; -const TidalMatcher = /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/; +interface URLReplacementRule { + match: RegExp; + replace: (...matches: string[]) => string; + description: string; + shortlinkMatch?: RegExp; + accountViewReplace?: (userId: string) => string; +} -const settings = definePluginSettings({ +// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant +const UrlReplacementRules: Record = { spotify: { - type: OptionType.BOOLEAN, + match: /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/, + replace: (_, type, id) => `spotify://${type}/${id}`, description: "Open Spotify links in the Spotify app", - default: true, + shortlinkMatch: /^https:\/\/spotify\.link\/.+$/, + accountViewReplace: userId => `spotify:user:${userId}`, }, steam: { - type: OptionType.BOOLEAN, + match: /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/, + replace: match => `steam://openurl/${match}`, description: "Open Steam links in the Steam app", - default: true, + shortlinkMatch: /^https:\/\/s.team\/.+$/, + accountViewReplace: userId => `steam://openurl/https://steamcommunity.com/profiles/${userId}`, }, epic: { - type: OptionType.BOOLEAN, + match: /^https:\/\/store\.epicgames\.com\/(.+)$/, + replace: (_, id) => `com.epicgames.launcher://store/${id}`, description: "Open Epic Games links in the Epic Games Launcher", - default: true, }, tidal: { - type: OptionType.BOOLEAN, + match: /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/, + replace: (_, type, id) => `tidal://${type}/${id}`, description: "Open Tidal links in the Tidal app", - default: true, - } -}); + }, + itunes: { + match: /^https:\/\/music\.apple\.com\/([a-z]{2}\/)?(album|artist|playlist|song|curator)\/([^/?#]+)\/?([^/?#]+)?(?:\?.*)?(?:#.*)?$/, + replace: (_, lang, type, name, id) => id ? `itunes://music.apple.com/us/${type}/${name}/${id}` : `itunes://music.apple.com/us/${type}/${name}`, + description: "Open Apple Music links in the iTunes app" + }, +}; + +const pluginSettings = definePluginSettings( + Object.entries(UrlReplacementRules).reduce((acc, [key, rule]) => { + acc[key] = { + type: OptionType.BOOLEAN, + description: rule.description, + default: true, + }; + return acc; + }, {} as SettingsDefinition) +); + const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative; export default definePlugin({ name: "OpenInApp", - description: "Open Spotify, Tidal, Steam and Epic Games URLs in their respective apps instead of your browser", - authors: [Devs.Ven], - settings, + description: "Open links in their respective apps instead of your browser", + authors: [Devs.Ven, Devs.surgedevs], + settings: pluginSettings, patches: [ { @@ -70,7 +94,7 @@ export default definePlugin({ // Make Spotify profile activity links open in app on web { find: "WEB_OPEN(", - predicate: () => !IS_DISCORD_DESKTOP && settings.store.spotify, + predicate: () => !IS_DISCORD_DESKTOP && pluginSettings.store.spotify, replacement: { match: /\i\.\i\.isProtocolRegistered\(\)(.{0,100})window.open/g, replace: "true$1VencordNative.native.openExternal" @@ -79,8 +103,8 @@ export default definePlugin({ { find: ".CONNECTED_ACCOUNT_VIEWED,", replacement: { - match: /(?<=href:\i,onClick:\i=>\{)(?=.{0,10}\i=(\i)\.type,.{0,100}CONNECTED_ACCOUNT_VIEWED)/, - replace: "$self.handleAccountView(arguments[0],$1.type,$1.id);" + match: /(?<=href:\i,onClick:(\i)=>\{)(?=.{0,10}\i=(\i)\.type,.{0,100}CONNECTED_ACCOUNT_VIEWED)/, + replace: "if($self.handleAccountView($1,$2.type,$2.id)) return;" } } ], @@ -89,61 +113,25 @@ export default definePlugin({ if (!data) return false; let url = data.href; - if (!IS_WEB && ShortUrlMatcher.test(url)) { - event?.preventDefault(); - // CORS jumpscare - url = await Native.resolveRedirect(url); - } + if (!url) return false; - spotify: { - if (!settings.store.spotify) break spotify; + for (const [key, rule] of Object.entries(UrlReplacementRules)) { + if (!pluginSettings.store[key]) continue; - const match = SpotifyMatcher.exec(url); - if (!match) break spotify; + if (rule.shortlinkMatch?.test(url)) { + event?.preventDefault(); + url = await Native.resolveRedirect(url); + } - const [, type, id] = match; - VencordNative.native.openExternal(`spotify:${type}:${id}`); + if (rule.match.test(url)) { + showToast("Opened link in native app", Toasts.Type.SUCCESS); - event?.preventDefault(); - return true; - } + const newUrl = url.replace(rule.match, rule.replace); + VencordNative.native.openExternal(newUrl); - steam: { - if (!settings.store.steam) break steam; - - if (!SteamMatcher.test(url)) break steam; - - VencordNative.native.openExternal(`steam://openurl/${url}`); - event?.preventDefault(); - - // Steam does not focus itself so show a toast so it's slightly less confusing - showToast("Opened link in Steam", Toasts.Type.SUCCESS); - return true; - } - - epic: { - if (!settings.store.epic) break epic; - - const match = EpicMatcher.exec(url); - if (!match) break epic; - - VencordNative.native.openExternal(`com.epicgames.launcher://store/${match[1]}`); - event?.preventDefault(); - - return true; - } - - tidal: { - if (!settings.store.tidal) break tidal; - - const match = TidalMatcher.exec(url); - if (!match) break tidal; - - const [, type, id] = match; - VencordNative.native.openExternal(`tidal://${type}/${id}`); - - event?.preventDefault(); - return true; + event?.preventDefault(); + return true; + } } // in case short url didn't end up being something we can handle @@ -155,14 +143,12 @@ export default definePlugin({ return false; }, - handleAccountView(event: { preventDefault(): void; }, platformType: string, userId: string) { - if (platformType === "spotify" && settings.store.spotify) { - VencordNative.native.openExternal(`spotify:user:${userId}`); - event.preventDefault(); - } else if (platformType === "steam" && settings.store.steam) { - VencordNative.native.openExternal(`steam://openurl/https://steamcommunity.com/profiles/${userId}`); - showToast("Opened link in Steam", Toasts.Type.SUCCESS); - event.preventDefault(); + handleAccountView(e: MouseEvent, platformType: string, userId: string) { + const rule = UrlReplacementRules[platformType]; + if (rule?.accountViewReplace && pluginSettings.store[platformType]) { + VencordNative.native.openExternal(rule.accountViewReplace(userId)); + e.preventDefault(); + return true; } } }); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 4de8e243..f46618e2 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -538,6 +538,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Joona", id: 297410829589020673n }, + surgedevs: { + name: "Chloe", + id: 1084592643784331324n + } } satisfies Record); // iife so #__PURE__ works correctly From 902a86c3b24ee1b64af5a51b404ae4fb7c0ad95c Mon Sep 17 00:00:00 2001 From: Nyako <24845294+nyakowint@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:48:11 -0400 Subject: [PATCH 08/21] XSOverlay: Update to new API (#2736) Co-authored-by: v --- src/plugins/xsOverlay.desktop/README.md | 15 --- src/plugins/xsOverlay.desktop/native.ts | 16 ---- src/plugins/xsOverlay/README.md | 14 +++ .../index.ts => xsOverlay/index.tsx} | 95 ++++++++++++++++--- 4 files changed, 94 insertions(+), 46 deletions(-) delete mode 100644 src/plugins/xsOverlay.desktop/README.md delete mode 100644 src/plugins/xsOverlay.desktop/native.ts create mode 100644 src/plugins/xsOverlay/README.md rename src/plugins/{xsOverlay.desktop/index.ts => xsOverlay/index.tsx} (83%) diff --git a/src/plugins/xsOverlay.desktop/README.md b/src/plugins/xsOverlay.desktop/README.md deleted file mode 100644 index 477e30bf..00000000 --- a/src/plugins/xsOverlay.desktop/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# XSOverlay Notifier - -Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR. - -## Preview - -![](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40) - -![](https://github.com/Vendicated/Vencord/assets/24845294/f15eff61-2d52-4620-bcab-808ecb1606d2) - -## Usage -- Enable this plugin -- Set plugin settings as desired -- Open XSOverlay -- get ping spammed diff --git a/src/plugins/xsOverlay.desktop/native.ts b/src/plugins/xsOverlay.desktop/native.ts deleted file mode 100644 index 82809383..00000000 --- a/src/plugins/xsOverlay.desktop/native.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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.icon = Buffer.from(data.icon).toString("base64"); - const json = JSON.stringify(data); - xsoSocket ??= createSocket("udp4"); - xsoSocket.send(json, 42069, "127.0.0.1"); -} diff --git a/src/plugins/xsOverlay/README.md b/src/plugins/xsOverlay/README.md new file mode 100644 index 00000000..7f517c17 --- /dev/null +++ b/src/plugins/xsOverlay/README.md @@ -0,0 +1,14 @@ +# XSOverlay Notifier + +Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR. + +## Preview + +![Resulting notification inside XSOverlay](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40) + +![Test notification inside XSOverlay](https://github.com/user-attachments/assets/d3b0c387-1d67-4697-a470-d4a927e228f4) + +## Usage +- Enable this plugin +- Set port and plugin settings as desired (defaults should work fine) +- Open SteamVR and XSOverlay diff --git a/src/plugins/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay/index.tsx similarity index 83% rename from src/plugins/xsOverlay.desktop/index.ts rename to src/plugins/xsOverlay/index.tsx index 8b06475c..ab76a0e7 100644 --- a/src/plugins/xsOverlay.desktop/index.ts +++ b/src/plugins/xsOverlay/index.tsx @@ -8,9 +8,9 @@ import { definePluginSettings } from "@api/Settings"; import { makeRange } from "@components/PluginSettings/components"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; -import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types"; +import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; import { findByCodeLazy, findLazy } from "@webpack"; -import { ChannelStore, GuildStore, UserStore } from "@webpack/common"; +import { Button, ChannelStore, GuildStore, UserStore } from "@webpack/common"; import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general"; const ChannelTypes = findLazy(m => m.ANNOUNCEMENT_THREAD === 10); @@ -68,10 +68,40 @@ interface Call { ringing: string[]; } +interface ApiObject { + sender: string, + target: string, + command: string, + jsonData: string, + rawData: string | null, +} + +interface NotificationObject { + type: number; + timeout: number; + height: number; + opacity: number; + volume: number; + audioPath: string; + title: string; + content: string; + useBase64Icon: boolean; + icon: ArrayBuffer | string; + sourceApp: string; +} + const notificationsShouldNotify = findByCodeLazy(".SUPPRESS_NOTIFICATIONS))return!1"); -const XSLog = new Logger("XSOverlay"); +const logger = new Logger("XSOverlay"); const settings = definePluginSettings({ + webSocketPort: { + type: OptionType.NUMBER, + description: "Websocket port", + default: 42070, + async onChange() { + await start(); + } + }, botNotifications: { type: OptionType.BOOLEAN, description: "Allow bot notifications", @@ -136,7 +166,17 @@ const settings = definePluginSettings({ }, }); -const Native = VencordNative.pluginHelpers.XSOverlay as PluginNative; +let socket: WebSocket; + +async function start() { + if (socket) socket.close(); + socket = new WebSocket(`ws://127.0.0.1:${settings.store.webSocketPort ?? 42070}/?client=Vencord`); + return new Promise((resolve, reject) => { + socket.onopen = resolve; + socket.onerror = reject; + setTimeout(reject, 3000); + }); +} export default definePlugin({ name: "XSOverlay", @@ -248,7 +288,21 @@ export default definePlugin({ if (shouldIgnoreForChannelType(channel)) return; sendMsgNotif(titleString, finalMsg, message); } - } + }, + + start, + + stop() { + socket.close(); + }, + + settingsAboutComponent: () => ( + <> + + + ) }); function shouldIgnoreForChannelType(channel: Channel) { @@ -259,9 +313,8 @@ function shouldIgnoreForChannelType(channel: Channel) { function sendMsgNotif(titleString: string, content: string, message: Message) { fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => { - const msgData = { - messageType: 1, - index: 0, + const msgData: NotificationObject = { + type: 1, timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout, height: calculateHeight(content), opacity: settings.store.opacity, @@ -270,17 +323,17 @@ function sendMsgNotif(titleString: string, content: string, message: Message) { title: titleString, content: content, useBase64Icon: true, - icon: result, + icon: new TextDecoder().decode(result), sourceApp: "Vencord" }; - Native.sendToOverlay(msgData); + + sendToOverlay(msgData); }); } function sendOtherNotif(content: string, titleString: string) { - const msgData = { - messageType: 1, - index: 0, + const msgData: NotificationObject = { + type: 1, timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout, height: calculateHeight(content), opacity: settings.store.opacity, @@ -289,10 +342,22 @@ function sendOtherNotif(content: string, titleString: string) { title: titleString, content: content, useBase64Icon: false, - icon: null, + icon: "default", sourceApp: "Vencord" }; - Native.sendToOverlay(msgData); + sendToOverlay(msgData); +} + +async function sendToOverlay(notif: NotificationObject) { + const apiObject: ApiObject = { + sender: "Vencord", + target: "xsoverlay", + command: "SendNotification", + jsonData: JSON.stringify(notif), + rawData: null + }; + if (socket.readyState !== socket.OPEN) await start(); + socket.send(JSON.stringify(apiObject)); } function shouldNotify(message: Message, channel: string) { From e460b5efb675e507225b9fd9d9a8b8ae02befde5 Mon Sep 17 00:00:00 2001 From: fres621 <126067139+fres621@users.noreply.github.com> Date: Wed, 31 Jul 2024 00:26:58 +0100 Subject: [PATCH 09/21] Fix MessagePopoverAPI patch (#2746) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> --- src/plugins/_api/messagePopover.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/plugins/_api/messagePopover.ts b/src/plugins/_api/messagePopover.ts index be4b6393..42a1bb76 100644 --- a/src/plugins/_api/messagePopover.ts +++ b/src/plugins/_api/messagePopover.ts @@ -27,12 +27,8 @@ export default definePlugin({ 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"/, - replace: (m, makeElement) => { - const msg = m.match(/message:(.{1,3}),/)?.[1]; - if (!msg) throw new Error("Could not find message variable"); - return `...Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}),${m}`; - } + 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}` } }], }); From 3013c669c035a9e6447aaa87d16c0f6a985c8f5f Mon Sep 17 00:00:00 2001 From: Vendicated Date: Wed, 31 Jul 2024 03:02:10 +0200 Subject: [PATCH 10/21] Fix ShowConnections Co-Authored-By: Masterjoona <69722179+Masterjoona@users.noreply.github.com> --- src/plugins/showConnections/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/showConnections/index.tsx b/src/plugins/showConnections/index.tsx index d6909dc9..f9f3d9eb 100644 --- a/src/plugins/showConnections/index.tsx +++ b/src/plugins/showConnections/index.tsx @@ -211,9 +211,9 @@ export default definePlugin({ } }, { - find: /\.BITE_SIZE,onOpenProfile:\i,usernameIcon:/, + find: '"BiteSizeProfileBody"', replacement: { - match: /currentUser:\i,guild:\i,onOpenProfile:.+?}\)(?=])(?<=user:(\i),bio:null==(\i)\?.+?)/, + match: /currentUser:\i,guild:\i}\)(?<=user:(\i),bio:null==(\i)\?.+?)/, replace: "$&,$self.profilePopoutComponent({ user: $1, displayProfile: $2, simplified: true })" } } From 1bfdcf2697f2fc09d4ec5b1caf4d2be95d02534f Mon Sep 17 00:00:00 2001 From: Vendicated Date: Wed, 31 Jul 2024 03:08:57 +0200 Subject: [PATCH 11/21] fix BetterUploadButton on canary --- src/plugins/betterUploadButton/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/plugins/betterUploadButton/index.ts b/src/plugins/betterUploadButton/index.ts index 511d12a4..2406b71e 100644 --- a/src/plugins/betterUploadButton/index.ts +++ b/src/plugins/betterUploadButton/index.ts @@ -25,11 +25,9 @@ export default definePlugin({ description: "Upload with a single click, open menu with right click", patches: [ { - find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE", + find: '"ChannelAttachButton"', replacement: { - // Discord merges multiple props here with Object.assign() - // This patch passes a third object to it with which we override onClick and onContextMenu - match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:(.+?:void 0),\.\.\.(\i),/, + match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),\.\.\.(\i),/, replace: "$&onClick:$1,onContextMenu:$2.onClick,", }, }, From 9cc42bf4574cf98867d8974c0e3199c71828d6a8 Mon Sep 17 00:00:00 2001 From: Scab <104103312+ZoamIl@users.noreply.github.com> Date: Thu, 1 Aug 2024 10:10:30 +0300 Subject: [PATCH 12/21] MutualGroupDms: make display consistent with Mutual Servers (#2727) Co-authored-by: Cookie <52550063+Covkie@users.noreply.github.com> --- src/plugins/mutualGroupDMs/index.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx index 0fbf41e9..27850143 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 => ( Date: Thu, 1 Aug 2024 00:12:02 -0700 Subject: [PATCH 13/21] fix(BetterFolders): Close folders when switching accounts (#2748) --- src/plugins/betterFolders/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx index d0e8cf34..072f4ee6 100644 --- a/src/plugins/betterFolders/index.tsx +++ b/src/plugins/betterFolders/index.tsx @@ -249,6 +249,10 @@ export default definePlugin({ dispatchingFoldersClose = false; }); } + }, + + LOGOUT() { + closeFolders(); } }, From d47be6c017b98a11c8a52f69c5d3cdcfd60f3690 Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 1 Aug 2024 09:20:00 +0200 Subject: [PATCH 14/21] MentionAvatars: Add option to hide @ symbol(#2725) Co-authored-by: v --- src/plugins/mentionAvatars/index.tsx | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/plugins/mentionAvatars/index.tsx b/src/plugins/mentionAvatars/index.tsx index 54969314..311303ab 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; +} From 2382294e8baa0a014a6a642f0f7642dcdd9e2bdf Mon Sep 17 00:00:00 2001 From: jenku <80538856+jenkuuuuuu@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:57:00 +0100 Subject: [PATCH 15/21] Decor: add copy preset id button (#2737) Co-authored-by: v --- .../decor/ui/modals/ChangeDecorationModal.tsx | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx index 5fbe165c..6501e0fe 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 && ( + + )}
From f8b01c1a310b589962eaa94645b11268a934e2dd Mon Sep 17 00:00:00 2001 From: Ashton <53633401+AshtonMemer@users.noreply.github.com> Date: Thu, 1 Aug 2024 07:10:27 -0500 Subject: [PATCH 16/21] Translate: Add DeepL support (#2721) Co-authored-by: v --- src/plugins/translate/TranslateIcon.tsx | 37 ++++-- src/plugins/translate/TranslateModal.tsx | 5 +- .../translate/TranslationAccessory.tsx | 3 +- src/plugins/translate/index.tsx | 15 ++- src/plugins/translate/languages.ts | 59 +++++++++- src/plugins/translate/native.ts | 29 +++++ src/plugins/translate/settings.ts | 52 +++++++-- src/plugins/translate/utils.ts | 110 ++++++++++++++++-- src/utils/constants.ts | 4 + 9 files changed, 277 insertions(+), 37 deletions(-) create mode 100644 src/plugins/translate/native.ts diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx index b22c488e..fa1d9abf 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 7628a31e..7a32d1b7 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 72b35940..8e8f4c17 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 f602d125..de61cef9 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 4bf370b5..3e2e7c71 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 00000000..3415e95e --- /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 65d84535..916c70bd 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/utils.ts b/src/plugins/translate/utils.ts index 493fb2ca..aff64e8a 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/utils/constants.ts b/src/utils/constants.ts index f46618e2..35525cd1 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -538,6 +538,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Joona", id: 297410829589020673n }, + AshtonMemer: { + name: "AshtonMemer", + id: 373657230530052099n + }, surgedevs: { name: "Chloe", id: 1084592643784331324n From 2658459a984cd1f6551e36cff3544e8e422cd458 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Fri, 2 Aug 2024 00:12:44 +1200 Subject: [PATCH 17/21] fix: Add ViewRaw & MessageLogger context menu options on threads (#2750) --- src/plugins/messageLogger/index.tsx | 1 + src/plugins/viewRaw/index.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 9181306a..a08aeccc 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/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index 56c285ec..83c560df 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") }, From 0f8d21a8461b716d9aac9a5f58c18c9a4d64dd27 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 1 Aug 2024 14:49:18 +0200 Subject: [PATCH 18/21] new plugin YoutubeAdblock: blocks ads in embeds (formerly WatchTogetherAdblock) --- .../README.md | 3 ++- .../adguard.js | 0 .../index.ts | 8 +++++--- .../native.ts | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) rename src/plugins/{watchTogetherAdblock.desktop => youtubeAdblock.desktop}/README.md (76%) rename src/plugins/{watchTogetherAdblock.desktop => youtubeAdblock.desktop}/adguard.js (100%) rename src/plugins/{watchTogetherAdblock.desktop => youtubeAdblock.desktop}/index.ts (53%) rename src/plugins/{watchTogetherAdblock.desktop => youtubeAdblock.desktop}/native.ts (69%) 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 4c64df67..d6235f36 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 100% rename from src/plugins/watchTogetherAdblock.desktop/adguard.js rename to src/plugins/youtubeAdblock.desktop/adguard.js 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 2dbc13d4..708b908d 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 c4106c34..8cc6a323 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); } }); From 83d90f03ee5f9ae8eb876be676646d4e15ff6d94 Mon Sep 17 00:00:00 2001 From: ingobeans <127751491+ingobeans@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:56:51 +0200 Subject: [PATCH 19/21] FakeProfileThemes: fix crash when encountering invalid colours (#2714) --- src/plugins/fakeProfileThemes/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/fakeProfileThemes/index.tsx b/src/plugins/fakeProfileThemes/index.tsx index 9e784da6..ab240837 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]] From c185f47f4d4e3ae1aa13c0f9be781cfc3661d207 Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Fri, 2 Aug 2024 01:04:52 +1200 Subject: [PATCH 20/21] ShowHiddenThings: Fix ModView highest role fix (#2747) --- src/plugins/showHiddenThings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/showHiddenThings/index.ts b/src/plugins/showHiddenThings/index.ts index b6c8e410..f01dc210 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\[))/, From d919cd6bf1e48f3669d577d8e53d857f90f5733b Mon Sep 17 00:00:00 2001 From: Vendicated Date: Thu, 1 Aug 2024 15:08:01 +0200 Subject: [PATCH 21/21] bump to v1.9.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 720a780b..b1057b72 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.9.6", + "version": "1.9.7", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": {