From b9e9d9bd64ae5873e4406e2c4093fb9772b61783 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 2 Dec 2022 14:10:40 +0100 Subject: [PATCH 01/22] Add --vanilla flag, strip csp on mainFrame only --- src/patcher.ts | 164 +++++++++++++++++++++++++------------------------ 1 file changed, 84 insertions(+), 80 deletions(-) diff --git a/src/patcher.ts b/src/patcher.ts index 12cefc018..0849e5a8c 100644 --- a/src/patcher.ts +++ b/src/patcher.ts @@ -42,98 +42,102 @@ require.main!.filename = join(asarPath, discordPkg.main); // @ts-ignore Untyped method? Dies from cringe app.setAppPath(asarPath); -// Repatch after host updates on Windows -if (process.platform === "win32") - require("./patchWin32Updater"); +if (!process.argv.includes("--vanilla")) { + // Repatch after host updates on Windows + if (process.platform === "win32") + require("./patchWin32Updater"); -class BrowserWindow extends electron.BrowserWindow { - constructor(options: BrowserWindowConstructorOptions) { - if (options?.webPreferences?.preload && options.title) { - const original = options.webPreferences.preload; - options.webPreferences.preload = join(__dirname, "preload.js"); - options.webPreferences.sandbox = false; + class BrowserWindow extends electron.BrowserWindow { + constructor(options: BrowserWindowConstructorOptions) { + if (options?.webPreferences?.preload && options.title) { + const original = options.webPreferences.preload; + options.webPreferences.preload = join(__dirname, "preload.js"); + options.webPreferences.sandbox = false; - process.env.DISCORD_PRELOAD = original; + process.env.DISCORD_PRELOAD = original; - super(options); - initIpc(this); - } else super(options); - } -} -Object.assign(BrowserWindow, electron.BrowserWindow); -// esbuild may rename our BrowserWindow, which leads to it being excluded -// from getFocusedWindow(), so this is necessary -// https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62 -Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true }); - -// Replace electrons exports with our custom BrowserWindow -const electronPath = require.resolve("electron"); -delete require.cache[electronPath]!.exports; -require.cache[electronPath]!.exports = { - ...electron, - BrowserWindow -}; - -// Patch appSettings to force enable devtools -onceDefined(global, "appSettings", s => - s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true) -); - -process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord"); - -electron.app.whenReady().then(() => { - // Source Maps! Maybe there's a better way but since the renderer is executed - // from a string I don't think any other form of sourcemaps would work - electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => { - let url = unsafeUrl.slice("vencord://".length); - if (url.endsWith("/")) url = url.slice(0, -1); - switch (url) { - case "renderer.js.map": - case "preload.js.map": - case "patcher.js.map": // doubt - cb(join(__dirname, url)); - break; - default: - cb({ statusCode: 403 }); + super(options); + initIpc(this); + } else super(options); } - }); + } + Object.assign(BrowserWindow, electron.BrowserWindow); + // esbuild may rename our BrowserWindow, which leads to it being excluded + // from getFocusedWindow(), so this is necessary + // https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62 + Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true }); - try { - const settings = JSON.parse(readSettings()); - if (settings.enableReactDevtools) - installExt("fmkadmapgofadopljbjfkapdkoienihi") - .then(() => console.info("[Vencord] Installed React Developer Tools")) - .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err)); - } catch { } + // Replace electrons exports with our custom BrowserWindow + const electronPath = require.resolve("electron"); + delete require.cache[electronPath]!.exports; + require.cache[electronPath]!.exports = { + ...electron, + BrowserWindow + }; + // Patch appSettings to force enable devtools + onceDefined(global, "appSettings", s => + s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true) + ); - // Remove CSP - function patchCsp(headers: Record, header: string) { - if (header in headers) { - let patchedHeader = headers[header][0]; - for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src"]) { - patchedHeader = patchedHeader.replace(new RegExp(`${directive}.+?;`), `${directive} * blob: data: 'unsafe-inline';`); + process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord"); + + electron.app.whenReady().then(() => { + // Source Maps! Maybe there's a better way but since the renderer is executed + // from a string I don't think any other form of sourcemaps would work + electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => { + let url = unsafeUrl.slice("vencord://".length); + if (url.endsWith("/")) url = url.slice(0, -1); + switch (url) { + case "renderer.js.map": + case "preload.js.map": + case "patcher.js.map": // doubt + cb(join(__dirname, url)); + break; + default: + cb({ statusCode: 403 }); } - // TODO: Restrict this to only imported packages with fixed version. - // Perhaps auto generate with esbuild - patchedHeader = patchedHeader.replace(/script-src.+?(?=;)/, "$& 'unsafe-eval' https://unpkg.com https://cdnjs.cloudflare.com"); - headers[header] = [patchedHeader]; - } - } + }); - electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, url }, cb) => { - if (responseHeaders) { - patchCsp(responseHeaders, "content-security-policy"); - patchCsp(responseHeaders, "content-security-policy-report-only"); + try { + const settings = JSON.parse(readSettings()); + if (settings.enableReactDevtools) + installExt("fmkadmapgofadopljbjfkapdkoienihi") + .then(() => console.info("[Vencord] Installed React Developer Tools")) + .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err)); + } catch { } - // Fix hosts that don't properly set the content type, such as - // raw.githubusercontent.com - if (url.endsWith(".css")) - responseHeaders["content-type"] = ["text/css"]; + + // Remove CSP + function patchCsp(headers: Record, header: string) { + if (header in headers) { + let patchedHeader = headers[header][0]; + for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src"]) { + patchedHeader = patchedHeader.replace(new RegExp(`${directive}.+?;`), `${directive} * blob: data: 'unsafe-inline';`); + } + // TODO: Restrict this to only imported packages with fixed version. + // Perhaps auto generate with esbuild + patchedHeader = patchedHeader.replace(/script-src.+?(?=;)/, "$& 'unsafe-eval' https://unpkg.com https://cdnjs.cloudflare.com"); + headers[header] = [patchedHeader]; + } } - cb({ cancel: false, responseHeaders }); + + electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => { + if (responseHeaders) { + if (resourceType === "mainFrame") + patchCsp(responseHeaders, "content-security-policy"); + + // Fix hosts that don't properly set the css content type, such as + // raw.githubusercontent.com + if (resourceType === "stylesheet") + responseHeaders["content-type"] = ["text/css"]; + } + cb({ cancel: false, responseHeaders }); + }); }); -}); +} else { + console.log("[Vencord] Running in vanilla mode. Not loading Vencord"); +} console.log("[Vencord] Loading original Discord app.asar"); // Legacy Vencord Injector requires "../app.asar". However, because we From 5e97cc0fc366acc268665c359a35e7c085364576 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 2 Dec 2022 14:11:20 +0100 Subject: [PATCH 02/22] QuickCss: Hide MenuBar; explicitly enable contextIsolation Closes #260 --- src/ipcMain/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ipcMain/index.ts b/src/ipcMain/index.ts index ba85eeb0b..86a233c71 100644 --- a/src/ipcMain/index.ts +++ b/src/ipcMain/index.ts @@ -89,8 +89,12 @@ export function initIpc(mainWindow: BrowserWindow) { ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { const win = new BrowserWindow({ title: "QuickCss Editor", + autoHideMenuBar: true, + darkTheme: true, webPreferences: { preload: join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false } }); await win.loadURL(`data:text/html;base64,${monacoHtml}`); From 2564ab73f55bd74a1cf34f2c34802605c997727f Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 2 Dec 2022 14:21:44 +0100 Subject: [PATCH 03/22] ci: unlisted firefox builds for now --- .github/workflows/build.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7c7363f6..301f7879a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build latest +name: Build DevBuild on: push: branches: @@ -9,6 +9,7 @@ on: - browser/** - scripts/build/** - package.json + - pnpm-lock.yaml env: FORCE_COLOR: true @@ -21,7 +22,7 @@ jobs: - uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json - - name: Use Node.js 18 + - name: Use Node.js 19 uses: actions/setup-node@v3 with: node-version: 19 @@ -35,7 +36,7 @@ jobs: - name: Sign firefox extension run: | - pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=listed + pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=unlisted env: WEBEXT_USER: ${{ secrets.WEBEXT_USER }} WEBEXT_SECRET: ${{ secrets.WEBEXT_SECRET }} From 06d32ae414bbd160d355fe62c643a20976193d49 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 2 Dec 2022 14:24:23 +0100 Subject: [PATCH 04/22] browser: remove firefox extension id --- browser/manifestv2.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/browser/manifestv2.json b/browser/manifestv2.json index c27e9e38b..405b2dc99 100644 --- a/browser/manifestv2.json +++ b/browser/manifestv2.json @@ -21,11 +21,5 @@ "web_accessible_resources": ["dist/Vencord.js"], "background": { "scripts": ["background.js"] - }, - "browser_specific_settings": { - "gecko": { - "id": "vencord-firefox@vendicated.dev", - "strict_min_version": "92.0" - } } } From 4760af7f0ee275caa1eee440f4945032057d2b56 Mon Sep 17 00:00:00 2001 From: 12944qwerty Date: Fri, 2 Dec 2022 09:38:52 -0600 Subject: [PATCH 05/22] add ViewRaw plugin & MiniPopover API (#275) Co-authored-by: Vendicated --- .gitignore | 3 + src/api/MessagePopover.ts | 69 ++++++++ src/api/index.ts | 7 +- src/plugins/apiMessagePopover.ts | 33 ++++ ...ideAttachments.tsx => hideAttachments.tsx} | 46 ++---- src/plugins/quickMention.tsx | 41 ++--- src/plugins/viewRaw.tsx | 147 ++++++++++++++++++ src/utils/constants.ts | 4 + src/utils/misc.tsx | 24 ++- 9 files changed, 318 insertions(+), 56 deletions(-) create mode 100644 src/api/MessagePopover.ts create mode 100644 src/plugins/apiMessagePopover.ts rename src/plugins/{HideAttachments.tsx => hideAttachments.tsx} (83%) create mode 100644 src/plugins/viewRaw.tsx diff --git a/.gitignore b/.gitignore index f24a72180..7bd751cb9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ lerna-debug.log* *.tsbuildinfo src/userplugins + +ExtensionCache/ +settings/ diff --git a/src/api/MessagePopover.ts b/src/api/MessagePopover.ts new file mode 100644 index 000000000..85dff9cf5 --- /dev/null +++ b/src/api/MessagePopover.ts @@ -0,0 +1,69 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import Logger from "@utils/Logger"; +import { Channel, Message } from "discord-types/general"; +import type { MouseEventHandler } from "react"; + +const logger = new Logger("MessagePopover"); + +export interface ButtonItem { + key?: string, + label: string, + icon: React.ComponentType, + message: Message, + channel: Channel, + onClick?: MouseEventHandler, + onContextMenu?: MouseEventHandler; +} + +export type getButtonItem = (message: Message) => ButtonItem | null; + +export const buttons = new Map(); + +export function addButton( + identifier: string, + item: getButtonItem, +) { + buttons.set(identifier, item); +} + +export function removeButton(identifier: string) { + buttons.delete(identifier); +} + +export function _buildPopoverElements( + msg: Message, + makeButton: (item: ButtonItem) => React.ComponentType +) { + const items = [] as React.ComponentType[]; + + for (const [identifier, getItem] of buttons.entries()) { + try { + const item = getItem(msg); + if (item) { + item.key ??= identifier; + items.push(makeButton(item)); + } + } catch (err) { + logger.error(`[${identifier}]`, err); + } + } + + return items; +} diff --git a/src/api/index.ts b/src/api/index.ts index 98fc6a4ac..b74da6e38 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -21,6 +21,7 @@ import * as $Commands from "./Commands"; import * as $DataStore from "./DataStore"; import * as $MessageAccessories from "./MessageAccessories"; import * as $MessageEventsAPI from "./MessageEvents"; +import * as $MessagePopover from "./MessagePopover"; import * as $Notices from "./Notices"; import * as $ServerList from "./ServerList"; @@ -59,6 +60,10 @@ const DataStore = $DataStore; * An API allowing you to add custom components as message accessories */ const MessageAccessories = $MessageAccessories; +/** + * An API allowing you to add custom buttons in the message popover + */ +const MessagePopover = $MessagePopover; /** * An API allowing you to add badges to user profiles */ @@ -68,4 +73,4 @@ const Badges = $Badges; */ const ServerList = $ServerList; -export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, Notices, ServerList }; +export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList }; diff --git a/src/plugins/apiMessagePopover.ts b/src/plugins/apiMessagePopover.ts new file mode 100644 index 000000000..95814e05f --- /dev/null +++ b/src/plugins/apiMessagePopover.ts @@ -0,0 +1,33 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "MessagePopoverAPI", + description: "API to add buttons to message popovers.", + authors: [Devs.KingFish], + patches: [{ + find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", + replacement: { + match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,90}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/, + replace: "$1...Vencord.Api.MessagePopover._buildPopoverElements($2,$4),$3" + } + }], +}); diff --git a/src/plugins/HideAttachments.tsx b/src/plugins/hideAttachments.tsx similarity index 83% rename from src/plugins/HideAttachments.tsx rename to src/plugins/hideAttachments.tsx index 2c1a0d4de..944da6539 100644 --- a/src/plugins/HideAttachments.tsx +++ b/src/plugins/hideAttachments.tsx @@ -17,11 +17,10 @@ */ import { get, set } from "@api/DataStore"; +import { addButton, removeButton } from "@api/MessagePopover"; import { Devs } from "@utils/constants"; -import Logger from "@utils/Logger"; import definePlugin from "@utils/types"; import { ChannelStore, FluxDispatcher } from "@webpack/common"; -import { Message } from "discord-types/general"; let style: HTMLStyleElement; @@ -49,13 +48,7 @@ export default definePlugin({ name: "HideAttachments", description: "Hide attachments and Embeds for individual messages via hover button", authors: [Devs.Ven], - patches: [{ - find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", - replacement: { - match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,40}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/, - replace: "$1Vencord.Plugins.plugins.HideAttachments.renderButton($2, $4),$3" - } - }], + dependencies: ["MessagePopoverAPI"], async start() { style = document.createElement("style"); @@ -64,11 +57,26 @@ export default definePlugin({ await getHiddenMessages(); await this.buildCss(); + + addButton("HideAttachments", msg => { + if (!msg.attachments.length && !msg.embeds.length) return null; + + const isHidden = hiddenMessages.has(msg.id); + + return { + label: isHidden ? "Show Attachments" : "Hide Attachments", + icon: isHidden ? ImageVisible : ImageInvisible, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: () => this.toggleHide(msg.id) + }; + }); }, stop() { style.remove(); hiddenMessages.clear(); + removeButton("HideAttachments"); }, async buildCss() { @@ -86,26 +94,6 @@ export default definePlugin({ `; }, - renderButton(msg: Message, makeItem: (data: any) => React.ComponentType) { - try { - if (!msg.attachments.length && !msg.embeds.length) return null; - - const isHidden = hiddenMessages.has(msg.id); - - return makeItem({ - key: "HideAttachments", - label: isHidden ? "Show Attachments" : "Hide Attachments", - icon: isHidden ? ImageVisible : ImageInvisible, - message: msg, - channel: ChannelStore.getChannel(msg.channel_id), - onClick: () => this.toggleHide(msg.id) - }); - } catch (err) { - new Logger("HideAttachments").error(err); - return null; - } - }, - async toggleHide(id: string) { const ids = await getHiddenMessages(); if (!ids.delete(id)) diff --git a/src/plugins/quickMention.tsx b/src/plugins/quickMention.tsx index 1c0a6c6ca..6e00dd0c7 100644 --- a/src/plugins/quickMention.tsx +++ b/src/plugins/quickMention.tsx @@ -16,9 +16,11 @@ * along with this program. If not, see . */ +import { addButton, removeButton } from "@api/MessagePopover"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { findLazy } from "@webpack"; +import { ChannelStore } from "@webpack/common"; const ComponentDispatch = findLazy(m => m.emitter?._events?.INSERT_TEXT); @@ -26,29 +28,22 @@ export default definePlugin({ name: "QuickMention", authors: [Devs.kemo], description: "Adds a quick mention button to the message actions bar", + dependencies: ["MessagePopoverAPI"], - patches: [ - { - find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL", - replacement: { - match: /(null,)(.{1,3}&&!.{1,3}\?(.{1,3})\(\{key:"reply",label:.{1,10}\.Messages\.MESSAGE_ACTION_REPLY,icon:.{1,10},channel:(.+?),message:(.+?),onClick:.+?\}\))/, - replace: (m, post, og, functionName, channelVar, messageVar) => { - - const functionSig = - `${functionName}({ - key: "QuickMention", - label: "Mention", - icon: Vencord.Plugins.plugins.QuickMention.Icon, - channel: ${channelVar}, - message: ${messageVar}, - onClick: ()=> Vencord.Plugins.plugins.QuickMention.onClick(${messageVar}) - })`; - - return `${post}${functionSig},${og}`; - } - } - } - ], + start() { + addButton("QuickMention", msg => { + return { + label: "Quick Mention", + icon: this.Icon, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: () => ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: `<@${msg.author.id}> ` }) + }; + }); + }, + stop() { + removeButton("QuickMention"); + }, Icon: () => ( ), - - onClick: (message: any) => ComponentDispatch.dispatchToLastSubscribed("INSERT_TEXT", { rawText: `<@${message.author.id}> ` }) }); diff --git a/src/plugins/viewRaw.tsx b/src/plugins/viewRaw.tsx new file mode 100644 index 000000000..c49180b8e --- /dev/null +++ b/src/plugins/viewRaw.tsx @@ -0,0 +1,147 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { addButton, removeButton } from "@api/MessagePopover"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Flex } from "@components/Flex"; +import { Devs } from "@utils/constants"; +import { copyWithToast } from "@utils/misc"; +import { closeModal, ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import definePlugin from "@utils/types"; +import { Button, ChannelStore, Forms, Margins, Parser } from "@webpack/common"; +import { Message } from "discord-types/general"; + + +const CopyIcon = () => { + return ; +}; + +function sortObject(obj: T): T { + return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T; +} + +function cleanMessage(msg: Message) { + const clone = sortObject(JSON.parse(JSON.stringify(msg))); + for (const key in clone.author) { + switch (key) { + case "id": + case "username": + case "usernameNormalized": + case "discriminator": + case "avatar": + case "bot": + case "system": + case "publicFlags": + break; + default: + // phone number, email, etc + delete clone.author[key]; + } + } + + // message logger added properties + const cloneAny = clone as any; + delete cloneAny.editHistory; + delete cloneAny.deleted; + cloneAny.attachments?.forEach(a => delete a.deleted); + + return clone; +} + +function CodeBlock(props: { content: string, lang: string; }) { + return ( + // make text selectable +
+ {Parser.defaultRules.codeBlock.react(props, null, {})} +
+ ); +} + +function openViewRawModal(msg: Message) { + msg = cleanMessage(msg); + const msgJson = JSON.stringify(msg, null, 4); + + const key = openModal(props => ( + + + + View Raw + closeModal(key)} /> + + + + + + + + {!!msg.content && ( + <> + Content + + + + )} + + Message Data + + + + + )); +} + +export default definePlugin({ + name: "ViewRaw", + description: "Copy and view the raw content/data of any message.", + authors: [Devs.KingFish, Devs.Ven], + dependencies: ["MessagePopoverAPI"], + + start() { + addButton("ViewRaw", msg => { + return { + label: "View Raw (Left Click) / Copy Raw (Right Click)", + icon: CopyIcon, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: () => openViewRawModal(msg), + onContextMenu: e => { + e.preventDefault(); + e.stopPropagation(); + copyWithToast(msg.content); + } + }; + }); + }, + + stop() { + removeButton("CopyRawMessage"); + } +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3766c74f8..eead2a3b3 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -156,5 +156,9 @@ export const Devs = Object.freeze({ Luna: { name: "Luny", id: 821472922140803112n + }, + KingFish: { + name: "King Fish", + id: 499400512559382538n } }); diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 4ae3fd504..d9164a0da 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { React } from "@webpack/common"; +import { Clipboard, React, Toasts } from "@webpack/common"; /** * Makes a lazy function. On first call, the value is computed. @@ -175,5 +175,25 @@ export function suppressErrors(name: string, func: F, thisOb */ export function makeCodeblock(text: string, language?: string) { const chars = "```"; - return `${chars}${language || ""}\n${text}\n${chars}`; + return `${chars}${language || ""}\n${text.replaceAll("```", "\\`\\`\\`")}\n${chars}`; +} + +export function copyWithToast(text: string, toastMessage = "Copied to clipboard!") { + if (Clipboard.SUPPORTS_COPY) { + Clipboard.copy(text); + } else { + toastMessage = "Your browser does not support copying to clipboard"; + } + Toasts.show({ + message: toastMessage, + id: Toasts.genId(), + type: Toasts.Type.SUCCESS + }); +} + +/** + * Check if obj is a true object: of type "object" and not null or array + */ +export function isObject(obj: unknown): obj is object { + return typeof obj === "object" && obj !== null && !Array.isArray(obj); } From 41dddc9eee6f19fb5055545811aff1e282790a9c Mon Sep 17 00:00:00 2001 From: Justice Almanzar Date: Fri, 2 Dec 2022 10:43:37 -0500 Subject: [PATCH 06/22] feat(plugin): ShikiCodeblocks (#267) Co-authored-by: ArjixWasTaken <53124886+ArjixWasTaken@users.noreply.github.com> Co-authored-by: Ven --- package.json | 3 + pnpm-lock.yaml | 182 +++++++++++------- src/api/settings.ts | 11 +- src/components/VencordSettings/Updater.tsx | 2 +- src/components/VencordSettings/VencordTab.tsx | 4 +- src/patcher.ts | 30 ++- .../components/PronounsChatComponent.tsx | 9 +- .../components/PronounsProfileWrapper.tsx | 9 +- .../reviewDB/components/ReviewsView.tsx | 14 +- src/plugins/shikiCodeblocks/api/languages.ts | 74 +++++++ src/plugins/shikiCodeblocks/api/shiki.ts | 119 ++++++++++++ src/plugins/shikiCodeblocks/api/themes.ts | 67 +++++++ .../shikiCodeblocks/components/ButtonRow.tsx | 46 +++++ .../shikiCodeblocks/components/Code.tsx | 92 +++++++++ .../shikiCodeblocks/components/CopyButton.tsx | 41 ++++ .../shikiCodeblocks/components/Header.tsx | 42 ++++ .../components/Highlighter.tsx | 123 ++++++++++++ .../shikiCodeblocks/hooks/useCopyCooldown.ts | 34 ++++ .../shikiCodeblocks/hooks/useShikiSettings.ts | 25 +++ src/plugins/shikiCodeblocks/hooks/useTheme.ts | 49 +++++ src/plugins/shikiCodeblocks/index.ts | 154 +++++++++++++++ src/plugins/shikiCodeblocks/style.css | 100 ++++++++++ src/plugins/shikiCodeblocks/types.ts | 78 ++++++++ src/plugins/shikiCodeblocks/utils/color.ts | 32 +++ .../shikiCodeblocks/utils/createStyle.ts | 36 ++++ src/plugins/shikiCodeblocks/utils/misc.ts | 50 +++++ src/utils/constants.ts | 6 +- src/utils/dependencies.ts | 4 + src/utils/misc.tsx | 49 ++++- src/utils/react.ts | 62 ++++++ src/utils/text.ts | 36 ++++ src/webpack/common.tsx | 2 + 32 files changed, 1480 insertions(+), 105 deletions(-) create mode 100644 src/plugins/shikiCodeblocks/api/languages.ts create mode 100644 src/plugins/shikiCodeblocks/api/shiki.ts create mode 100644 src/plugins/shikiCodeblocks/api/themes.ts create mode 100644 src/plugins/shikiCodeblocks/components/ButtonRow.tsx create mode 100644 src/plugins/shikiCodeblocks/components/Code.tsx create mode 100644 src/plugins/shikiCodeblocks/components/CopyButton.tsx create mode 100644 src/plugins/shikiCodeblocks/components/Header.tsx create mode 100644 src/plugins/shikiCodeblocks/components/Highlighter.tsx create mode 100644 src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts create mode 100644 src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts create mode 100644 src/plugins/shikiCodeblocks/hooks/useTheme.ts create mode 100644 src/plugins/shikiCodeblocks/index.ts create mode 100644 src/plugins/shikiCodeblocks/style.css create mode 100644 src/plugins/shikiCodeblocks/types.ts create mode 100644 src/plugins/shikiCodeblocks/utils/color.ts create mode 100644 src/plugins/shikiCodeblocks/utils/createStyle.ts create mode 100644 src/plugins/shikiCodeblocks/utils/misc.ts create mode 100644 src/utils/react.ts create mode 100644 src/utils/text.ts diff --git a/package.json b/package.json index 839e87426..f0c31035f 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "@types/yazl": "^2.4.2", "@typescript-eslint/eslint-plugin": "^5.44.0", "@typescript-eslint/parser": "^5.44.0", + "@vap/core": "0.0.12", + "@vap/shiki": "0.10.3", "console-menu": "^0.1.0", "diff": "^5.1.0", "discord-types": "^1.3.26", @@ -50,6 +52,7 @@ "eslint-plugin-path-alias": "^1.0.0", "eslint-plugin-simple-import-sort": "^8.0.0", "eslint-plugin-unused-imports": "^2.0.0", + "highlight.js": "10.6.0", "moment": "^2.29.4", "puppeteer-core": "^19.3.0", "standalone-electron-types": "^1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eaf90a1d4..6f76ff3fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,8 @@ specifiers: '@types/yazl': ^2.4.2 '@typescript-eslint/eslint-plugin': ^5.44.0 '@typescript-eslint/parser': ^5.44.0 + '@vap/core': 0.0.12 + '@vap/shiki': 0.10.3 console-menu: ^0.1.0 diff: ^5.1.0 discord-types: ^1.3.26 @@ -24,6 +26,7 @@ specifiers: eslint-plugin-simple-import-sort: ^8.0.0 eslint-plugin-unused-imports: ^2.0.0 fflate: ^0.7.4 + highlight.js: 10.6.0 moment: ^2.29.4 puppeteer-core: ^19.3.0 standalone-electron-types: ^1.0.0 @@ -39,8 +42,10 @@ devDependencies: '@types/react': 18.0.25 '@types/react-dom': 18.0.9 '@types/yazl': 2.4.2 - '@typescript-eslint/eslint-plugin': 5.44.0_fnsv2sbzcckq65bwfk7a5xwslu - '@typescript-eslint/parser': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a + '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au + '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + '@vap/core': 0.0.12 + '@vap/shiki': 0.10.3 console-menu: 0.1.0 diff: 5.1.0 discord-types: 1.3.26 @@ -50,7 +55,8 @@ devDependencies: eslint-plugin-header: 3.1.1_eslint@8.28.0 eslint-plugin-path-alias: 1.0.0_m6sma4g6bh67km3q6igf6uxaja_eslint@8.28.0 eslint-plugin-simple-import-sort: 8.0.0_eslint@8.28.0 - eslint-plugin-unused-imports: 2.0.0_aucl44mjeutxyzmt4nvo2cczya + eslint-plugin-unused-imports: 2.0.0_5am2datodjm2qi4eijrjrnoz54 + highlight.js: 10.6.0 moment: 2.29.4 puppeteer-core: 19.3.0 standalone-electron-types: 1.0.0 @@ -83,9 +89,9 @@ packages: dependencies: ajv: 6.12.6 debug: 4.3.4 - espree: 9.4.1 - globals: 13.18.0 - ignore: 5.2.1 + espree: 9.4.0 + globals: 13.17.0 + ignore: 5.2.0 import-fresh: 3.3.0 js-yaml: 4.1.0 minimatch: 3.1.2 @@ -161,7 +167,7 @@ packages: resolution: {integrity: sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==} dependencies: '@types/prop-types': 15.7.5 - csstype: 3.1.1 + csstype: 3.1.0 dev: true /@types/react/18.0.25: @@ -169,7 +175,7 @@ packages: dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2 - csstype: 3.1.1 + csstype: 3.1.0 dev: true /@types/scheduler/0.16.2: @@ -194,8 +200,8 @@ packages: '@types/node': 18.11.9 dev: true - /@typescript-eslint/eslint-plugin/5.44.0_fnsv2sbzcckq65bwfk7a5xwslu: - resolution: {integrity: sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw==} + /@typescript-eslint/eslint-plugin/5.45.0_czs5uoqkd3podpy6vgtsxfc7au: + resolution: {integrity: sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/parser': ^5.0.0 @@ -205,24 +211,24 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a - '@typescript-eslint/scope-manager': 5.44.0 - '@typescript-eslint/type-utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a - '@typescript-eslint/utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a + '@typescript-eslint/parser': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + '@typescript-eslint/scope-manager': 5.45.0 + '@typescript-eslint/type-utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a + '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a debug: 4.3.4 eslint: 8.28.0 - ignore: 5.2.1 + ignore: 5.2.0 natural-compare-lite: 1.4.0 regexpp: 3.2.0 - semver: 7.3.8 + semver: 7.3.7 tsutils: 3.21.0_typescript@4.9.3 typescript: 4.9.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a: - resolution: {integrity: sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA==} + /@typescript-eslint/parser/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a: + resolution: {integrity: sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -231,9 +237,9 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 5.44.0 - '@typescript-eslint/types': 5.44.0 - '@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3 + '@typescript-eslint/scope-manager': 5.45.0 + '@typescript-eslint/types': 5.45.0 + '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 debug: 4.3.4 eslint: 8.28.0 typescript: 4.9.3 @@ -241,16 +247,16 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager/5.44.0: - resolution: {integrity: sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g==} + /@typescript-eslint/scope-manager/5.45.0: + resolution: {integrity: sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.44.0 - '@typescript-eslint/visitor-keys': 5.44.0 + '@typescript-eslint/types': 5.45.0 + '@typescript-eslint/visitor-keys': 5.45.0 dev: true - /@typescript-eslint/type-utils/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a: - resolution: {integrity: sha512-A1u0Yo5wZxkXPQ7/noGkRhV4J9opcymcr31XQtOzcc5nO/IHN2E2TPMECKWYpM3e6olWEM63fq/BaL1wEYnt/w==} + /@typescript-eslint/type-utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a: + resolution: {integrity: sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -259,8 +265,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3 - '@typescript-eslint/utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a + '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 + '@typescript-eslint/utils': 5.45.0_hsf322ms6xhhd4b5ne6lb74y4a debug: 4.3.4 eslint: 8.28.0 tsutils: 3.21.0_typescript@4.9.3 @@ -269,13 +275,13 @@ packages: - supports-color dev: true - /@typescript-eslint/types/5.44.0: - resolution: {integrity: sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==} + /@typescript-eslint/types/5.45.0: + resolution: {integrity: sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree/5.44.0_typescript@4.9.3: - resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==} + /@typescript-eslint/typescript-estree/5.45.0_typescript@4.9.3: + resolution: {integrity: sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -283,56 +289,70 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 5.44.0 - '@typescript-eslint/visitor-keys': 5.44.0 + '@typescript-eslint/types': 5.45.0 + '@typescript-eslint/visitor-keys': 5.45.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.3.8 + semver: 7.3.7 tsutils: 3.21.0_typescript@4.9.3 typescript: 4.9.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a: - resolution: {integrity: sha512-fMzA8LLQ189gaBjS0MZszw5HBdZgVwxVFShCO3QN+ws3GlPkcy9YuS3U4wkT6su0w+Byjq3mS3uamy9HE4Yfjw==} + /@typescript-eslint/utils/5.45.0_hsf322ms6xhhd4b5ne6lb74y4a: + resolution: {integrity: sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: '@types/json-schema': 7.0.11 '@types/semver': 7.3.13 - '@typescript-eslint/scope-manager': 5.44.0 - '@typescript-eslint/types': 5.44.0 - '@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3 + '@typescript-eslint/scope-manager': 5.45.0 + '@typescript-eslint/types': 5.45.0 + '@typescript-eslint/typescript-estree': 5.45.0_typescript@4.9.3 eslint: 8.28.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0_eslint@8.28.0 - semver: 7.3.8 + semver: 7.3.7 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/visitor-keys/5.44.0: - resolution: {integrity: sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==} + /@typescript-eslint/visitor-keys/5.45.0: + resolution: {integrity: sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': 5.44.0 + '@typescript-eslint/types': 5.45.0 eslint-visitor-keys: 3.3.0 dev: true - /acorn-jsx/5.3.2_acorn@8.8.1: + /@vap/core/0.0.12: + resolution: {integrity: sha512-3csHpkE1zUSRTZwl7xIf2uXg1cD4IhhtUm0F6K/dWydc95R5Nj+krB4OTNATuqkewIv/ViCbwjPfkafAgvZQSg==} + dependencies: + eventemitter3: 4.0.7 + dev: true + + /@vap/shiki/0.10.3: + resolution: {integrity: sha512-tZPHZxDKEBlorQ2BaprytGfkbo5yKBvdxdAF144p94HCTpjO3ScJk/f319wi7GtV1NE4DV8HBQo/0XpldixWQA==} + dependencies: + jsonc-parser: 3.2.0 + vscode-oniguruma: 1.7.0 + vscode-textmate: 5.2.0 + dev: true + + /acorn-jsx/5.3.2_acorn@8.8.0: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.8.1 + acorn: 8.8.0 dev: true - /acorn/8.8.1: - resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} + /acorn/8.8.0: + resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} engines: {node: '>=0.4.0'} hasBin: true dev: true @@ -522,7 +542,7 @@ packages: dev: true /concat-map/0.0.1: - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true /console-menu/0.1.0: @@ -553,8 +573,8 @@ packages: which: 2.0.2 dev: true - /csstype/3.1.1: - resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + /csstype/3.1.0: + resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==} dev: true /debug/2.6.9: @@ -897,7 +917,7 @@ packages: eslint: 8.28.0 dev: true - /eslint-plugin-unused-imports/2.0.0_aucl44mjeutxyzmt4nvo2cczya: + /eslint-plugin-unused-imports/2.0.0_5am2datodjm2qi4eijrjrnoz54: resolution: {integrity: sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -907,7 +927,7 @@ packages: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.44.0_fnsv2sbzcckq65bwfk7a5xwslu + '@typescript-eslint/eslint-plugin': 5.45.0_czs5uoqkd3podpy6vgtsxfc7au eslint: 8.28.0 eslint-rule-composer: 0.3.0 dev: true @@ -971,21 +991,21 @@ packages: eslint-scope: 7.1.1 eslint-utils: 3.0.0_eslint@8.28.0 eslint-visitor-keys: 3.3.0 - espree: 9.4.1 + espree: 9.4.0 esquery: 1.4.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.18.0 + globals: 13.17.0 grapheme-splitter: 1.0.4 - ignore: 5.2.1 + ignore: 5.2.0 import-fresh: 3.3.0 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 - js-sdsl: 4.2.0 + js-sdsl: 4.1.5 js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 @@ -1001,12 +1021,12 @@ packages: - supports-color dev: true - /espree/9.4.1: - resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} + /espree/9.4.0: + resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.8.1 - acorn-jsx: 5.3.2_acorn@8.8.1 + acorn: 8.8.0 + acorn-jsx: 5.3.2_acorn@8.8.0 eslint-visitor-keys: 3.3.0 dev: true @@ -1039,6 +1059,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /eventemitter3/4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: true + /extend-shallow/2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -1198,8 +1222,8 @@ packages: path-is-absolute: 1.0.1 dev: true - /globals/13.18.0: - resolution: {integrity: sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==} + /globals/13.17.0: + resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} engines: {node: '>=8'} dependencies: type-fest: 0.20.2 @@ -1212,7 +1236,7 @@ packages: array-union: 2.1.0 dir-glob: 3.0.1 fast-glob: 3.2.12 - ignore: 5.2.1 + ignore: 5.2.0 merge2: 1.4.1 slash: 3.0.0 dev: true @@ -1257,6 +1281,10 @@ packages: kind-of: 4.0.0 dev: true + /highlight.js/10.6.0: + resolution: {integrity: sha512-8mlRcn5vk/r4+QcqerapwBYTe+iPL5ih6xrNylxrnBdHQiijDETfXX7VIxC3UiCRiINBJfANBAsPzAvRQj8RpQ==} + dev: true + /https-proxy-agent/5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -1271,8 +1299,8 @@ packages: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true - /ignore/5.2.1: - resolution: {integrity: sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==} + /ignore/5.2.0: + resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} engines: {node: '>= 4'} dev: true @@ -1423,8 +1451,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /js-sdsl/4.2.0: - resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} + /js-sdsl/4.1.5: + resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==} dev: true /js-yaml/4.1.0: @@ -1442,6 +1470,10 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true + /jsonc-parser/3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + /keypress/0.2.1: resolution: {integrity: sha512-HjorDJFNhnM4SicvaUXac0X77NiskggxJdesG72+O5zBKpSqKFCrqmndKVqpu3pFqkla0St6uGk8Ju0sCurrmg==} dev: true @@ -1797,8 +1829,8 @@ packages: ret: 0.1.15 dev: true - /semver/7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} + /semver/7.3.7: + resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} engines: {node: '>=10'} hasBin: true dependencies: @@ -2053,6 +2085,14 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /vscode-oniguruma/1.7.0: + resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} + dev: true + + /vscode-textmate/5.2.0: + resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==} + dev: true + /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true diff --git a/src/api/settings.ts b/src/api/settings.ts index b7c143a3c..2617903a5 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -141,14 +141,19 @@ export const Settings = makeProxy(settings); * Settings hook for React components. Returns a smart settings * object that automagically triggers a rerender if any properties * are altered + * @param paths An optional list of paths to whitelist for rerenders * @returns Settings */ -export function useSettings() { +export function useSettings(paths?: string[]) { const [, forceUpdate] = React.useReducer(() => ({}), {}); + const onUpdate: SubscriptionCallback = paths + ? (value, path) => paths.includes(path) && forceUpdate() + : forceUpdate; + React.useEffect(() => { - subscriptions.add(forceUpdate); - return () => void subscriptions.delete(forceUpdate); + subscriptions.add(onUpdate); + return () => void subscriptions.delete(onUpdate); }, []); return Settings; diff --git a/src/components/VencordSettings/Updater.tsx b/src/components/VencordSettings/Updater.tsx index bb344f5e8..33690697f 100644 --- a/src/components/VencordSettings/Updater.tsx +++ b/src/components/VencordSettings/Updater.tsx @@ -179,7 +179,7 @@ function Newer(props: CommonProps) { } function Updater() { - const [repo, err, repoPending] = useAwaiter(getRepo, "Loading..."); + const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." }); React.useEffect(() => { if (err) diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index 746fcf03e..ad8fe142c 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -27,7 +27,9 @@ import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common"; const st = (style: string) => `vcSettings${style}`; function VencordSettings() { - const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke(IpcEvents.GET_SETTINGS_DIR), "Loading..."); + const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke(IpcEvents.GET_SETTINGS_DIR), { + fallbackValue: "Loading..." + }); const settings = useSettings(); const [donateImage] = React.useState( diff --git a/src/patcher.ts b/src/patcher.ts index 0849e5a8c..0cf7e24ce 100644 --- a/src/patcher.ts +++ b/src/patcher.ts @@ -109,16 +109,36 @@ if (!process.argv.includes("--vanilla")) { // Remove CSP + type PolicyResult = Record; + + const parsePolicy = (policy: string): PolicyResult => { + const result: PolicyResult = {}; + policy.split(";").forEach(directive => { + const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g); + if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) { + result[directiveKey] = directiveValue; + } + }); + return result; + }; + const stringifyPolicy = (policy: PolicyResult): string => + Object.entries(policy) + .filter(([, values]) => values?.length) + .map(directive => directive.flat().join(" ")) + .join("; "); + function patchCsp(headers: Record, header: string) { if (header in headers) { - let patchedHeader = headers[header][0]; - for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src"]) { - patchedHeader = patchedHeader.replace(new RegExp(`${directive}.+?;`), `${directive} * blob: data: 'unsafe-inline';`); + const csp = parsePolicy(headers[header][0]); + + for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) { + csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"]; } // TODO: Restrict this to only imported packages with fixed version. // Perhaps auto generate with esbuild - patchedHeader = patchedHeader.replace(/script-src.+?(?=;)/, "$& 'unsafe-eval' https://unpkg.com https://cdnjs.cloudflare.com"); - headers[header] = [patchedHeader]; + csp["script-src"] ??= []; + csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com"); + headers[header] = [stringifyPolicy(csp)]; } } diff --git a/src/plugins/pronoundb/components/PronounsChatComponent.tsx b/src/plugins/pronoundb/components/PronounsChatComponent.tsx index 9225fc52b..ce67754e4 100644 --- a/src/plugins/pronoundb/components/PronounsChatComponent.tsx +++ b/src/plugins/pronoundb/components/PronounsChatComponent.tsx @@ -39,11 +39,10 @@ export default function PronounsChatComponentWrapper({ message }: { message: Mes } function PronounsChatComponent({ message }: { message: Message; }) { - const [result, , isPending] = useAwaiter( - () => fetchPronouns(message.author.id), - null, - e => console.error("Fetching pronouns failed: ", e) - ); + const [result, , isPending] = useAwaiter(() => fetchPronouns(message.author.id), { + fallbackValue: null, + onError: e => console.error("Fetching pronouns failed: ", e) + }); // If the promise completed, the result was not "unspecified", and there is a mapping for the code, then return a span with the pronouns if (!isPending && result && result !== "unspecified" && PronounMapping[result]) { diff --git a/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx b/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx index 9540bb9e6..79fce23a1 100644 --- a/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx +++ b/src/plugins/pronoundb/components/PronounsProfileWrapper.tsx @@ -45,11 +45,10 @@ function ProfilePronouns( leProps: UserProfilePronounsProps; } ) { - const [result, , isPending] = useAwaiter( - () => fetchPronouns(userId), - null, - e => console.error("Fetching pronouns failed: ", e) - ); + const [result, , isPending] = useAwaiter(() => fetchPronouns(userId), { + fallbackValue: null, + onError: e => console.error("Fetching pronouns failed: ", e), + }); // If the promise completed, the result was not "unspecified", and there is a mapping for the code, then render if (!isPending && result && result !== "unspecified" && PronounMapping[result]) { diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx index 999b97005..57f974e72 100644 --- a/src/plugins/reviewDB/components/ReviewsView.tsx +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -18,7 +18,7 @@ import { classes, useAwaiter } from "@utils/misc"; import { findLazy } from "@webpack"; -import { Forms, Text, UserStore } from "@webpack/common"; +import { Forms, React, Text, UserStore } from "@webpack/common"; import type { KeyboardEvent } from "react"; import { addReview, getReviews } from "../Utils/ReviewDBAPI"; @@ -27,7 +27,13 @@ import ReviewComponent from "./ReviewComponent"; const Classes = findLazy(m => typeof m.textarea === "string"); export default function ReviewsView({ userId }: { userId: string; }) { - const [reviews, _, isLoading, refetch] = useAwaiter(() => getReviews(userId), []); + const [refetchCount, setRefetchCount] = React.useState(0); + const [reviews, _, isLoading] = useAwaiter(() => getReviews(userId), { + fallbackValue: [], + deps: [refetchCount], + }); + + const dirtyRefetch = () => setRefetchCount(refetchCount + 1); if (isLoading) return null; @@ -40,7 +46,7 @@ export default function ReviewsView({ userId }: { userId: string; }) { }).then(res => { if (res === 0 || res === 1) { (target as HTMLInputElement).value = ""; // clear the input - refetch(); + dirtyRefetch(); } }); } @@ -64,7 +70,7 @@ export default function ReviewsView({ userId }: { userId: string; }) { )} {reviews?.length === 0 && ( diff --git a/src/plugins/shikiCodeblocks/api/languages.ts b/src/plugins/shikiCodeblocks/api/languages.ts new file mode 100644 index 000000000..f14a4dc25 --- /dev/null +++ b/src/plugins/shikiCodeblocks/api/languages.ts @@ -0,0 +1,74 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { ILanguageRegistration } from "@vap/shiki"; + +export const VPC_REPO = "Vap0r1ze/vapcord"; +export const VPC_REPO_COMMIT = "88a7032a59cca40da170926651b08201ea3b965a"; +export const vpcRepoAssets = `https://raw.githubusercontent.com/${VPC_REPO}/${VPC_REPO_COMMIT}/assets/shiki-codeblocks`; +export const vpcRepoGrammar = (fileName: string) => `${vpcRepoAssets}/${fileName}`; +export const vpcRepoLanguages = `${vpcRepoAssets}/languages.json`; + +export interface Language { + name: string; + id: string; + devicon?: string; + grammarUrl: string, + grammar?: ILanguageRegistration["grammar"]; + scopeName: string; + aliases?: string[]; + custom?: boolean; +} +export interface LanguageJson { + name: string; + id: string; + fileName: string; + devicon?: string; + scopeName: string; + aliases?: string[]; +} + +export const languages: Record = {}; + +export const loadLanguages = async () => { + const langsJson: LanguageJson[] = await fetch(vpcRepoLanguages).then(res => res.json()); + const loadedLanguages = Object.fromEntries( + langsJson.map(lang => [lang.id, { + ...lang, + grammarUrl: vpcRepoGrammar(lang.fileName), + }]) + ); + Object.assign(languages, loadedLanguages); +}; + +export const getGrammar = (lang: Language): Promise> => { + if (lang.grammar) return Promise.resolve(lang.grammar); + return fetch(lang.grammarUrl).then(res => res.json()); +}; + +const aliasCache = new Map(); +export function resolveLang(idOrAlias: string) { + if (Object.prototype.hasOwnProperty.call(languages, idOrAlias)) return languages[idOrAlias]; + + const lang = Object.values(languages).find(lang => lang.aliases?.includes(idOrAlias)); + + if (!lang) return null; + + aliasCache.set(idOrAlias, lang); + return lang; +} diff --git a/src/plugins/shikiCodeblocks/api/shiki.ts b/src/plugins/shikiCodeblocks/api/shiki.ts new file mode 100644 index 000000000..e7691ce32 --- /dev/null +++ b/src/plugins/shikiCodeblocks/api/shiki.ts @@ -0,0 +1,119 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + + +import { shikiOnigasmSrc, shikiWorkerSrc } from "@utils/dependencies"; +import { WorkerClient } from "@vap/core/ipc"; +import type { IShikiTheme, IThemedToken } from "@vap/shiki"; + +import { dispatchTheme } from "../hooks/useTheme"; +import type { ShikiSpec } from "../types"; +import { getGrammar, languages, loadLanguages, resolveLang } from "./languages"; +import { themes } from "./themes"; + +const themeUrls = Object.values(themes); + +let resolveClient: (client: WorkerClient) => void; + +export const shiki = { + client: null as WorkerClient | null, + currentTheme: null as IShikiTheme | null, + currentThemeUrl: null as string | null, + timeoutMs: 10000, + languages, + themes, + loadedThemes: new Set(), + loadedLangs: new Set(), + clientPromise: new Promise>(resolve => resolveClient = resolve), + + init: async (initThemeUrl: string | undefined) => { + /** https://stackoverflow.com/q/58098143 */ + const workerBlob = await fetch(shikiWorkerSrc).then(res => res.blob()); + + const client = shiki.client = new WorkerClient( + "shiki-client", + "shiki-host", + workerBlob, + { name: "ShikiWorker" }, + ); + await client.init(); + + const themeUrl = initThemeUrl || themeUrls[0]; + + await loadLanguages(); + await client.run("setOnigasm", { wasm: shikiOnigasmSrc }); + await client.run("setHighlighter", { theme: themeUrl, langs: [] }); + shiki.loadedThemes.add(themeUrl); + await shiki._setTheme(themeUrl); + resolveClient(client); + }, + _setTheme: async (themeUrl: string) => { + shiki.currentThemeUrl = themeUrl; + const { themeData } = await shiki.client!.run("getTheme", { theme: themeUrl }); + shiki.currentTheme = JSON.parse(themeData); + dispatchTheme({ id: themeUrl, theme: shiki.currentTheme }); + }, + loadTheme: async (themeUrl: string) => { + const client = await shiki.clientPromise; + if (shiki.loadedThemes.has(themeUrl)) return; + + await client.run("loadTheme", { theme: themeUrl }); + + shiki.loadedThemes.add(themeUrl); + }, + setTheme: async (themeUrl: string) => { + await shiki.clientPromise; + themeUrl ||= themeUrls[0]; + if (!shiki.loadedThemes.has(themeUrl)) await shiki.loadTheme(themeUrl); + + await shiki._setTheme(themeUrl); + }, + loadLang: async (langId: string) => { + const client = await shiki.clientPromise; + const lang = resolveLang(langId); + + if (!lang || shiki.loadedLangs.has(lang.id)) return; + + await client.run("loadLanguage", { + lang: { + ...lang, + grammar: lang.grammar ?? await getGrammar(lang), + } + }); + shiki.loadedLangs.add(lang.id); + }, + tokenizeCode: async (code: string, langId: string): Promise => { + const client = await shiki.clientPromise; + const lang = resolveLang(langId); + if (!lang) return []; + + if (!shiki.loadedLangs.has(lang.id)) await shiki.loadLang(lang.id); + + return await client.run("codeToThemedTokens", { + code, + lang: langId, + theme: shiki.currentThemeUrl ?? themeUrls[0], + }); + }, + destroy() { + shiki.currentTheme = null; + shiki.currentThemeUrl = null; + dispatchTheme({ id: null, theme: null }); + shiki.client?.destroy(); + } +}; diff --git a/src/plugins/shikiCodeblocks/api/themes.ts b/src/plugins/shikiCodeblocks/api/themes.ts new file mode 100644 index 000000000..f31ce60b3 --- /dev/null +++ b/src/plugins/shikiCodeblocks/api/themes.ts @@ -0,0 +1,67 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { IShikiTheme } from "@vap/shiki"; + +export const SHIKI_REPO = "shikijs/shiki"; +export const SHIKI_REPO_COMMIT = "0b28ad8ccfbf2615f2d9d38ea8255416b8ac3043"; +export const shikiRepoTheme = (name: string) => `https://raw.githubusercontent.com/${SHIKI_REPO}/${SHIKI_REPO_COMMIT}/packages/shiki/themes/${name}.json`; + +export const themes = { + // Default + DarkPlus: shikiRepoTheme("dark-plus"), + + // Dev Choices + MaterialCandy: "https://raw.githubusercontent.com/millsp/material-candy/master/material-candy.json", + + // More from Shiki repo + DraculaSoft: shikiRepoTheme("dracula-soft"), + Dracula: shikiRepoTheme("dracula"), + GithubDarkDimmed: shikiRepoTheme("github-dark-dimmed"), + GithubDark: shikiRepoTheme("github-dark"), + GithubLight: shikiRepoTheme("github-light"), + LightPlus: shikiRepoTheme("light-plus"), + MaterialDarker: shikiRepoTheme("material-darker"), + MaterialDefault: shikiRepoTheme("material-default"), + MaterialLighter: shikiRepoTheme("material-lighter"), + MaterialOcean: shikiRepoTheme("material-ocean"), + MaterialPalenight: shikiRepoTheme("material-palenight"), + MinDark: shikiRepoTheme("min-dark"), + MinLight: shikiRepoTheme("min-light"), + Monokai: shikiRepoTheme("monokai"), + Nord: shikiRepoTheme("nord"), + OneDarkPro: shikiRepoTheme("one-dark-pro"), + Poimandres: shikiRepoTheme("poimandres"), + RosePineDawn: shikiRepoTheme("rose-pine-dawn"), + RosePineMoon: shikiRepoTheme("rose-pine-moon"), + RosePine: shikiRepoTheme("rose-pine"), + SlackDark: shikiRepoTheme("slack-dark"), + SlackOchin: shikiRepoTheme("slack-ochin"), + SolarizedDark: shikiRepoTheme("solarized-dark"), + SolarizedLight: shikiRepoTheme("solarized-light"), + VitesseDark: shikiRepoTheme("vitesse-dark"), + VitesseLight: shikiRepoTheme("vitesse-light"), + CssVariables: shikiRepoTheme("css-variables"), +}; + +export const themeCache = new Map(); + +export const getTheme = (url: string): Promise => { + if (themeCache.has(url)) return Promise.resolve(themeCache.get(url)!); + return fetch(url).then(res => res.json()); +}; diff --git a/src/plugins/shikiCodeblocks/components/ButtonRow.tsx b/src/plugins/shikiCodeblocks/components/ButtonRow.tsx new file mode 100644 index 000000000..e73eb72bd --- /dev/null +++ b/src/plugins/shikiCodeblocks/components/ButtonRow.tsx @@ -0,0 +1,46 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Clipboard } from "@webpack/common"; + +import { cl } from "../utils/misc"; +import { CopyButton } from "./CopyButton"; + +export interface ButtonRowProps { + theme: import("./Highlighter").ThemeBase; + content: string; +} + +export function ButtonRow({ content, theme }: ButtonRowProps) { + const buttons: JSX.Element[] = []; + + if (Clipboard.SUPPORTS_COPY) { + buttons.push( + + ); + } + + return
{buttons}
; +} diff --git a/src/plugins/shikiCodeblocks/components/Code.tsx b/src/plugins/shikiCodeblocks/components/Code.tsx new file mode 100644 index 000000000..ae41113c9 --- /dev/null +++ b/src/plugins/shikiCodeblocks/components/Code.tsx @@ -0,0 +1,92 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import type { IThemedToken } from "@vap/shiki"; + +import { cl } from "../utils/misc"; +import { ThemeBase } from "./Highlighter"; + +export interface CodeProps { + theme: ThemeBase; + useHljs: boolean; + lang?: string; + content: string; + tokens: IThemedToken[][] | null; +} + +export const Code = ({ + theme, + useHljs, + lang, + content, + tokens, +}: CodeProps) => { + let lines!: JSX.Element[]; + + if (useHljs) { + try { + const { value: hljsHtml } = hljs.highlight(lang!, content, true); + lines = hljsHtml + .split("\n") + .map((line, i) => ); + } catch { + lines = content.split("\n").map(line => {line}); + } + } else { + const renderTokens = + tokens ?? + content + .split("\n") + .map(line => [{ color: theme.plainColor, content: line } as IThemedToken]); + + lines = renderTokens.map(line => { + // [Cynthia] this makes it so when you highlight the codeblock + // empty lines are also selected and copied when you Ctrl+C. + if (line.length === 0) { + return {"\n"}; + } + + return ( + <> + {line.map(({ content, color, fontStyle }, i) => ( + + {content} + + ))} + + ); + }); + } + + const codeTableRows = lines.map((line, i) => ( + + {i + 1} + {line} + + )); + + return {...codeTableRows}
; +}; diff --git a/src/plugins/shikiCodeblocks/components/CopyButton.tsx b/src/plugins/shikiCodeblocks/components/CopyButton.tsx new file mode 100644 index 000000000..153b3cddb --- /dev/null +++ b/src/plugins/shikiCodeblocks/components/CopyButton.tsx @@ -0,0 +1,41 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { useCopyCooldown } from "../hooks/useCopyCooldown"; + +export interface CopyButtonProps extends React.DetailedHTMLProps, HTMLButtonElement> { + content: string; +} + +export function CopyButton({ content, ...props }: CopyButtonProps) { + const [copyCooldown, copy] = useCopyCooldown(1000); + + return ( + + + ); +} diff --git a/src/plugins/shikiCodeblocks/components/Header.tsx b/src/plugins/shikiCodeblocks/components/Header.tsx new file mode 100644 index 000000000..c2db38693 --- /dev/null +++ b/src/plugins/shikiCodeblocks/components/Header.tsx @@ -0,0 +1,42 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Language } from "../api/languages"; +import { DeviconSetting } from "../types"; +import { cl } from "../utils/misc"; + +export interface HeaderProps { + langName?: string; + useDevIcon: DeviconSetting; + shikiLang: Language | null; +} + +export function Header({ langName, useDevIcon, shikiLang }: HeaderProps) { + if (!langName) return <>; + + return ( +
+ {useDevIcon !== DeviconSetting.Disabled && shikiLang?.devicon && ( + + )} + {langName} +
+ ); +} diff --git a/src/plugins/shikiCodeblocks/components/Highlighter.tsx b/src/plugins/shikiCodeblocks/components/Highlighter.tsx new file mode 100644 index 000000000..6067fd836 --- /dev/null +++ b/src/plugins/shikiCodeblocks/components/Highlighter.tsx @@ -0,0 +1,123 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { useAwaiter } from "@utils/misc"; +import { useIntersection } from "@utils/react"; +import { hljs, React } from "@webpack/common"; + +import { resolveLang } from "../api/languages"; +import { shiki } from "../api/shiki"; +import { useShikiSettings } from "../hooks/useShikiSettings"; +import { useTheme } from "../hooks/useTheme"; +import { hex2Rgb } from "../utils/color"; +import { cl, shouldUseHljs } from "../utils/misc"; +import { ButtonRow } from "./ButtonRow"; +import { Code } from "./Code"; +import { Header } from "./Header"; + +export interface ThemeBase { + plainColor: string; + accentBgColor: string; + accentFgColor: string; + backgroundColor: string; +} + +export interface HighlighterProps { + lang?: string; + content: string; + isPreview: boolean; +} + +export const createHighlighter = (props: HighlighterProps) => ( + + + +); +export const Highlighter = ({ + lang, + content, + isPreview, +}: HighlighterProps) => { + const { tryHljs, useDevIcon, bgOpacity } = useShikiSettings(["tryHljs", "useDevIcon", "bgOpacity"]); + const { id: currentThemeId, theme: currentTheme } = useTheme(); + + const shikiLang = lang ? resolveLang(lang) : null; + const useHljs = shouldUseHljs({ lang, tryHljs }); + + const [preRef, isIntersecting] = useIntersection(true); + + const [tokens] = useAwaiter(async () => { + if (!shikiLang || useHljs || !isIntersecting) return null; + return await shiki.tokenizeCode(content, lang!); + }, { + fallbackValue: null, + deps: [lang, content, currentThemeId, isIntersecting], + }); + + const themeBase: ThemeBase = { + plainColor: currentTheme?.fg || "var(--text-normal)", + accentBgColor: + currentTheme?.colors?.["statusBar.background"] || (useHljs ? "#7289da" : "#007BC8"), + accentFgColor: currentTheme?.colors?.["statusBar.foreground"] || "#FFF", + backgroundColor: + currentTheme?.colors?.["editor.background"] || "var(--background-secondary)", + }; + + let langName; + if (lang) langName = useHljs ? hljs?.getLanguage?.(lang)?.name : shikiLang?.name; + + const preClasses = [cl("root")]; + if (!langName) preClasses.push(cl("plain")); + if (isPreview) preClasses.push(cl("preview")); + + return ( +
+            
+                
+ + {!isPreview && } + +
+ ); +}; + diff --git a/src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts b/src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts new file mode 100644 index 000000000..414500bdc --- /dev/null +++ b/src/plugins/shikiCodeblocks/hooks/useCopyCooldown.ts @@ -0,0 +1,34 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Clipboard, React } from "@webpack/common"; + +export function useCopyCooldown(cooldown: number) { + const [copyCooldown, setCopyCooldown] = React.useState(false); + + function copy(text: string) { + Clipboard.copy(text); + setCopyCooldown(true); + + setTimeout(() => { + setCopyCooldown(false); + }, cooldown); + } + + return [copyCooldown, copy] as const; +} diff --git a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts new file mode 100644 index 000000000..416f8e9ba --- /dev/null +++ b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts @@ -0,0 +1,25 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { useSettings } from "@api/settings"; + +import { ShikiSettings } from "../types"; + +export function useShikiSettings(settings: (keyof ShikiSettings)[]) { + return useSettings(settings.map(setting => `plugins.ShikiCodeblocks.${setting}`)).plugins.ShikiCodeblocks as ShikiSettings; +} diff --git a/src/plugins/shikiCodeblocks/hooks/useTheme.ts b/src/plugins/shikiCodeblocks/hooks/useTheme.ts new file mode 100644 index 000000000..fae579611 --- /dev/null +++ b/src/plugins/shikiCodeblocks/hooks/useTheme.ts @@ -0,0 +1,49 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { React } from "@webpack/common"; + +type Shiki = typeof import("../api/shiki").shiki; +interface ThemeState { + id: Shiki["currentThemeUrl"], + theme: Shiki["currentTheme"], +} + +const currentTheme: ThemeState = { + id: null, + theme: null, +}; + +const themeSetters = new Set>>(); + +export const useTheme = (): ThemeState => { + const [, setTheme] = React.useState(currentTheme); + + React.useEffect(() => { + themeSetters.add(setTheme); + return () => void themeSetters.delete(setTheme); + }, []); + + return currentTheme; +}; + +export function dispatchTheme(state: ThemeState) { + if (currentTheme.id === state.id) return; + Object.assign(currentTheme, state); + themeSetters.forEach(setTheme => setTheme(state)); +} diff --git a/src/plugins/shikiCodeblocks/index.ts b/src/plugins/shikiCodeblocks/index.ts new file mode 100644 index 000000000..a8be92af4 --- /dev/null +++ b/src/plugins/shikiCodeblocks/index.ts @@ -0,0 +1,154 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import { parseUrl } from "@utils/misc"; +import { wordsFromPascal, wordsToTitle } from "@utils/text"; +import definePlugin, { OptionType } from "@utils/types"; + +import cssText from "~fileContent/style.css"; + +import { Settings } from "../../Vencord"; +import { shiki } from "./api/shiki"; +import { themes } from "./api/themes"; +import { createHighlighter } from "./components/Highlighter"; +import { DeviconSetting, HljsSetting, ShikiSettings, StyleSheets } from "./types"; +import { clearStyles, removeStyle, setStyle } from "./utils/createStyle"; + +const themeNames = Object.keys(themes); +const devIconCss = "@import url('https://cdn.jsdelivr.net/gh/devicons/devicon@v2.10.1/devicon.min.css');"; + +const getSettings = () => Settings.plugins.ShikiCodeblocks as ShikiSettings; + +export default definePlugin({ + name: "ShikiCodeblocks", + description: "Brings vscode-style codeblocks into Discord, powered by Shiki", + authors: [Devs.Vap], + patches: [ + { + find: "codeBlock:{react:function", + replacement: { + match: /codeBlock:\{react:function\((.),(.),(.)\)\{/, + replace: "$&return Vencord.Plugins.plugins.ShikiCodeblocks.renderHighlighter($1,$2,$3);", + }, + }, + ], + start: async () => { + setStyle(cssText, StyleSheets.Main); + if (getSettings().useDevIcon !== DeviconSetting.Disabled) + setStyle(devIconCss, StyleSheets.DevIcons); + + await shiki.init(getSettings().customTheme || getSettings().theme); + }, + stop: () => { + shiki.destroy(); + clearStyles(); + }, + options: { + theme: { + type: OptionType.SELECT, + description: "Default themes", + options: themeNames.map(themeName => ({ + label: wordsToTitle(wordsFromPascal(themeName)), + value: themes[themeName], + default: themes[themeName] === themes.DarkPlus, + })), + disabled: () => !!getSettings().customTheme, + onChange: shiki.setTheme, + }, + customTheme: { + type: OptionType.STRING, + description: "A link to a custom vscode theme", + placeholder: themes.MaterialCandy, + isValid: value => { + if (!value) return true; + const url = parseUrl(value); + if (!url) return "Must be a valid URL"; + + if (!url.pathname.endsWith(".json")) return "Must be a json file"; + + return true; + }, + onChange: value => shiki.setTheme(value || getSettings().theme), + }, + tryHljs: { + type: OptionType.SELECT, + description: "Use the more lightweight default Discord highlighter and theme.", + options: [ + { + label: "Never", + value: HljsSetting.Never, + }, + { + label: "Prefer Shiki instead of Highlight.js", + value: HljsSetting.Secondary, + default: true, + }, + { + label: "Prefer Highlight.js instead of Shiki", + value: HljsSetting.Primary, + }, + { + label: "Always", + value: HljsSetting.Always, + }, + ], + }, + useDevIcon: { + type: OptionType.SELECT, + description: "How to show language icons on codeblocks", + options: [ + { + label: "Disabled", + value: DeviconSetting.Disabled, + }, + { + label: "Colorless", + value: DeviconSetting.Greyscale, + default: true, + }, + { + label: "Colored", + value: DeviconSetting.Color, + }, + ], + onChange: (newValue: DeviconSetting) => { + if (newValue === DeviconSetting.Disabled) removeStyle(StyleSheets.DevIcons); + else setStyle(devIconCss, StyleSheets.DevIcons); + }, + }, + bgOpacity: { + type: OptionType.SLIDER, + description: "Background opacity", + markers: [0, 20, 40, 60, 80, 100], + default: 100, + stickToMarkers: false, + }, + }, + + // exports + shiki, + createHighlighter, + renderHighlighter: ({ lang, content }: { lang: string; content: string; }) => { + return createHighlighter({ + lang, + content, + isPreview: false, + }); + }, +}); diff --git a/src/plugins/shikiCodeblocks/style.css b/src/plugins/shikiCodeblocks/style.css new file mode 100644 index 000000000..119ff80eb --- /dev/null +++ b/src/plugins/shikiCodeblocks/style.css @@ -0,0 +1,100 @@ +.shiki-root { + border-radius: 4px; +} + +.shiki-root code { + display: block; + overflow-x: auto; + padding: 0.5em; + position: relative; + + font-size: 0.875rem; + line-height: 1.125rem; + text-indent: 0; + white-space: pre-wrap; + background: transparent; + border: none; +} + +.shiki-root [class^='devicon-'], +.shiki-root [class*=' devicon-'] { + margin-right: 8px; + user-select: none; +} + +.shiki-plain code { + padding-top: 8px; +} + +.shiki-btns { + font-size: 1em; + position: absolute; + right: 0; + bottom: 0; + opacity: 0; +} + +.shiki-root:hover .shiki-btns { + opacity: 1; +} + +.shiki-btn { + border-radius: 4px 4px 0 0; + padding: 4px 8px; +} + +.shiki-btn~.shiki-btn { + margin-left: 4px; +} + +.shiki-btn:last-child { + border-radius: 4px 0; +} + +.shiki-spinner-container { + align-items: center; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + position: absolute; + justify-content: center; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.shiki-preview { + margin-bottom: 2em; +} + +.shiki-lang { + padding: 0 5px; + margin-bottom: 6px; + font-weight: bold; + text-transform: capitalize; + display: flex; + align-items: center; +} + +.shiki-table { + border-collapse: collapse; + width: 100%; +} + +.shiki-table tr { + height: 19px; + width: 100%; +} + +.shiki-root td:first-child { + border-right: 1px solid transparent; + padding-left: 5px; + padding-right: 8px; + user-select: none; +} + +.shiki-root td:last-child { + padding-left: 8px; + word-break: break-word; + width: 100%; +} diff --git a/src/plugins/shikiCodeblocks/types.ts b/src/plugins/shikiCodeblocks/types.ts new file mode 100644 index 000000000..ee5aa9e64 --- /dev/null +++ b/src/plugins/shikiCodeblocks/types.ts @@ -0,0 +1,78 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import type { + ILanguageRegistration, + IShikiTheme, + IThemedToken, + IThemeRegistration, +} from "@vap/shiki"; + +import type { Settings } from "../../Vencord"; + +/** This must be atleast a subset of the `@vap/shiki-worker` spec */ +export type ShikiSpec = { + setOnigasm: ({ wasm }: { wasm: string; }) => Promise; + setHighlighter: ({ theme, langs }: { + theme: IThemeRegistration | void; + langs: ILanguageRegistration[]; + }) => Promise; + loadTheme: ({ theme }: { + theme: string | IShikiTheme; + }) => Promise; + getTheme: ({ theme }: { theme: string; }) => Promise<{ themeData: string; }>; + loadLanguage: ({ lang }: { lang: ILanguageRegistration; }) => Promise; + codeToThemedTokens: ({ + code, + lang, + theme, + }: { + code: string; + lang?: string; + theme?: string; + }) => Promise; +}; + +export enum StyleSheets { + Main = "MAIN", + DevIcons = "DEVICONS", +} + +export enum HljsSetting { + Never = "NEVER", + Secondary = "SECONDARY", + Primary = "PRIMARY", + Always = "ALWAYS", +} +export enum DeviconSetting { + Disabled = "DISABLED", + Greyscale = "GREYSCALE", + Color = "COLOR" +} + +type CommonSettings = { + [K in keyof Settings["plugins"][string]as K extends `${infer V}` ? K : never]: Settings["plugins"][string][K]; +}; + +export interface ShikiSettings extends CommonSettings { + theme: string; + customTheme: string; + tryHljs: HljsSetting; + useDevIcon: DeviconSetting; + bgOpacity: number; +} diff --git a/src/plugins/shikiCodeblocks/utils/color.ts b/src/plugins/shikiCodeblocks/utils/color.ts new file mode 100644 index 000000000..e74ec526d --- /dev/null +++ b/src/plugins/shikiCodeblocks/utils/color.ts @@ -0,0 +1,32 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +export function hex2Rgb(hex: string) { + hex = hex.slice(1); + if (hex.length < 6) + hex = hex + .split("") + .map(c => c + c) + .join(""); + if (hex.length === 6) hex += "ff"; + if (hex.length > 6) hex = hex.slice(0, 6); + return hex + .split(/(..)/) + .filter(Boolean) + .map(c => parseInt(c, 16)); +} diff --git a/src/plugins/shikiCodeblocks/utils/createStyle.ts b/src/plugins/shikiCodeblocks/utils/createStyle.ts new file mode 100644 index 000000000..734f7dcb3 --- /dev/null +++ b/src/plugins/shikiCodeblocks/utils/createStyle.ts @@ -0,0 +1,36 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +const styles = new Map(); + +export function setStyle(css: string, id: string) { + const style = document.createElement("style"); + style.innerText = css; + document.head.appendChild(style); + styles.set(id, style); +} + +export function removeStyle(id: string) { + styles.get(id)?.remove(); + return styles.delete(id); +} + +export const clearStyles = () => { + styles.forEach(style => style.remove()); + styles.clear(); +}; diff --git a/src/plugins/shikiCodeblocks/utils/misc.ts b/src/plugins/shikiCodeblocks/utils/misc.ts new file mode 100644 index 000000000..1342ff5d8 --- /dev/null +++ b/src/plugins/shikiCodeblocks/utils/misc.ts @@ -0,0 +1,50 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { hljs } from "@webpack/common"; + +import { resolveLang } from "../api/languages"; +import { HighlighterProps } from "../components/Highlighter"; +import { HljsSetting, ShikiSettings } from "../types"; + +export const cl = (className: string) => `shiki-${className}`; + +export const shouldUseHljs = ({ + lang, + tryHljs, +}: { + lang: HighlighterProps["lang"], + tryHljs: ShikiSettings["tryHljs"], +}) => { + const hljsLang = lang ? hljs?.getLanguage?.(lang) : null; + const shikiLang = lang ? resolveLang(lang) : null; + const langName = shikiLang?.name; + + switch (tryHljs) { + case HljsSetting.Always: + return true; + case HljsSetting.Primary: + return !!hljsLang || lang === ""; + case HljsSetting.Secondary: + return !langName && !!hljsLang; + case HljsSetting.Never: + return false; + } + + return false; +}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index eead2a3b3..f45e8b02a 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -157,8 +157,12 @@ export const Devs = Object.freeze({ name: "Luny", id: 821472922140803112n }, + Vap: { + name: "Vap0r1ze", + id: 454072114492866560n + }, KingFish: { name: "King Fish", id: 499400512559382538n - } + }, }); diff --git a/src/utils/dependencies.ts b/src/utils/dependencies.ts index a7766de2e..ed26644ea 100644 --- a/src/utils/dependencies.ts +++ b/src/utils/dependencies.ts @@ -74,3 +74,7 @@ export interface ApngFrameData { frames: ApngFrame[]; playTime: number; } + +const shikiWorkerDist = "https://unpkg.com/@vap/shiki-worker@0.0.8/dist"; +export const shikiWorkerSrc = `${shikiWorkerDist}/${IS_DEV ? "index.js" : "index.min.js"}`; +export const shikiOnigasmSrc = "https://unpkg.com/@vap/shiki@0.10.3/dist/onig.wasm"; diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index d9164a0da..8b7cea2da 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -28,7 +28,12 @@ export function makeLazy(factory: () => T): () => T { return () => cache ?? (cache = factory()); } -type AwaiterRes = [T, any, boolean, () => void]; +type AwaiterRes = [T, any, boolean]; +interface AwaiterOpts { + fallbackValue: T, + deps?: unknown[], + onError?(e: any): void, +} /** * Await a promise * @param factory Factory @@ -36,26 +41,31 @@ type AwaiterRes = [T, any, boolean, () => void]; * @returns [value, error, isPending] */ export function useAwaiter(factory: () => Promise): AwaiterRes; -export function useAwaiter(factory: () => Promise, fallbackValue: T): AwaiterRes; -export function useAwaiter(factory: () => Promise, fallbackValue: null, onError: (e: unknown) => unknown): AwaiterRes; -export function useAwaiter(factory: () => Promise, fallbackValue: T | null = null, onError?: (e: unknown) => unknown): AwaiterRes { +export function useAwaiter(factory: () => Promise, providedOpts: AwaiterOpts): AwaiterRes; +export function useAwaiter(factory: () => Promise, providedOpts?: AwaiterOpts): AwaiterRes { + const opts: Required> = Object.assign({ + fallbackValue: null, + deps: [], + onError: null, + }, providedOpts); const [state, setState] = React.useState({ - value: fallbackValue, + value: opts.fallbackValue, error: null, pending: true }); - const [signal, setSignal] = React.useState(0); React.useEffect(() => { let isAlive = true; + if (!state.pending) setState({ ...state, pending: true }); + factory() .then(value => isAlive && setState({ value, error: null, pending: false })) - .catch(error => isAlive && (setState({ value: null, error, pending: false }), onError?.(error))); + .catch(error => isAlive && (setState({ value: null, error, pending: false }), opts.onError?.(error))); return () => void (isAlive = false); - }, [signal]); + }, opts.deps); - return [state.value, state.error, state.pending, () => setSignal(signal + 1)]; + return [state.value, state.error, state.pending]; } /** @@ -197,3 +207,24 @@ export function copyWithToast(text: string, toastMessage = "Copied to clipboard! export function isObject(obj: unknown): obj is object { return typeof obj === "object" && obj !== null && !Array.isArray(obj); } + +/** + * Returns null if value is not a URL, otherwise return URL object. + * Avoids having to wrap url checks in a try/catch + */ +export function parseUrl(urlString: string): URL | null { + try { + return new URL(urlString); + } catch { + return null; + } +} + +/** + * Checks whether an element is on screen + */ +export const checkIntersecting = (el: Element) => { + const elementBox = el.getBoundingClientRect(); + const documentHeight = Math.max(document.documentElement.clientHeight, window.innerHeight); + return !(elementBox.bottom < 0 || elementBox.top - documentHeight >= 0); +}; diff --git a/src/utils/react.ts b/src/utils/react.ts new file mode 100644 index 000000000..8585846c4 --- /dev/null +++ b/src/utils/react.ts @@ -0,0 +1,62 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { React } from "@webpack/common"; + +import { checkIntersecting } from "./misc"; + +/** + * Check if an element is on screen + * @param intersectOnly If `true`, will only update the state when the element comes into view + * @returns [refCallback, isIntersecting] + */ +export const useIntersection = (intersectOnly = false): [ + refCallback: React.RefCallback, + isIntersecting: boolean, +] => { + const observerRef = React.useRef(null); + const [isIntersecting, setIntersecting] = React.useState(false); + + const refCallback = (element: Element | null) => { + observerRef.current?.disconnect(); + observerRef.current = null; + + if (!element) return; + + if (checkIntersecting(element)) { + setIntersecting(true); + if (intersectOnly) return; + } + + observerRef.current = new IntersectionObserver(entries => { + for (const entry of entries) { + if (entry.target !== element) continue; + if (entry.isIntersecting && intersectOnly) { + setIntersecting(true); + observerRef.current?.disconnect(); + observerRef.current = null; + } else { + setIntersecting(entry.isIntersecting); + } + } + }); + observerRef.current.observe(element); + }; + + return [refCallback, isIntersecting]; +}; diff --git a/src/utils/text.ts b/src/utils/text.ts new file mode 100644 index 000000000..17826e80d --- /dev/null +++ b/src/utils/text.ts @@ -0,0 +1,36 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +// Utils for readable text transformations eg: `toTitle(fromKebab())` + +// Case style to words +export const wordsFromCamel = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase()); +export const wordsFromSnake = (text: string) => text.toLowerCase().split("_"); +export const wordsFromKebab = (text: string) => text.toLowerCase().split("-"); +export const wordsFromPascal = (text: string) => text.split(/(?=[A-Z])/).map(w => w.toLowerCase()); +export const wordsFromTitle = (text: string) => text.toLowerCase().split(" "); + +// Words to case style +export const wordsToCamel = (words: string[]) => + words.map((w, i) => (i ? w[0].toUpperCase() + w.slice(1) : w)).join(""); +export const wordsToSnake = (words: string[]) => words.join("_").toUpperCase(); +export const wordsToKebab = (words: string[]) => words.join("-").toLowerCase(); +export const wordsToPascal = (words: string[]) => + words.map(w => w[0].toUpperCase() + w.slice(1)).join(""); +export const wordsToTitle = (words: string[]) => + words.map(w => w[0].toUpperCase() + w.slice(1)).join(" "); diff --git a/src/webpack/common.tsx b/src/webpack/common.tsx index 8c4357730..56846c252 100644 --- a/src/webpack/common.tsx +++ b/src/webpack/common.tsx @@ -37,6 +37,8 @@ export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPorta export const RestAPI = findByPropsLazy("getAPIBaseURL", "get"); export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYear"); +export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight"); + export const MessageStore = findByPropsLazy("getRawMessages") as Omit & { getMessages(chanId: string): any; }; From 62b2acebe6806c7b0e2ca6a43c6b2419a627b8dc Mon Sep 17 00:00:00 2001 From: A user Date: Fri, 2 Dec 2022 12:55:53 -0300 Subject: [PATCH 07/22] Add support for Flatpak for Git updating (#274) Co-authored-by: Ven --- scripts/patcher/install.js | 31 +++++++++++++++++++++++++++---- src/ipcMain/updater/git.ts | 19 +++++++++++++------ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/scripts/patcher/install.js b/scripts/patcher/install.js index 036b0fa38..852f3a206 100755 --- a/scripts/patcher/install.js +++ b/scripts/patcher/install.js @@ -39,6 +39,7 @@ const { getDarwinDirs, getLinuxDirs, ENTRYPOINT, + question } = require("./common"); switch (process.platform) { @@ -62,15 +63,14 @@ async function install(installations) { // Attempt to give flatpak perms if (selected.isFlatpak) { try { - const { branch } = selected; const cwd = process.cwd(); - const globalCmd = `flatpak override ${branch} --filesystem=${cwd}`; - const userCmd = `flatpak override --user ${branch} --filesystem=${cwd}`; + const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`; + const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`; const cmd = selected.location.startsWith("/home") ? userCmd : globalCmd; execSync(cmd); - console.log("Successfully gave write perms to Discord Flatpak."); + console.log("Gave write perms to Discord Flatpak."); } catch (e) { console.log("Failed to give write perms to Discord Flatpak."); console.log( @@ -79,6 +79,29 @@ async function install(installations) { ); process.exit(1); } + + const answer = await question( + `Would you like to allow ${selected.branch} to talk to org.freedesktop.Flatpak?\n` + + "This is essentially full host access but necessary to spawn git. Without it, the updater will not work\n" + + "Consider using the http based updater (using the gui installer) instead if you want to maintain the sandbox.\n" + + "[y/N]: " + ); + + if (["y", "yes", "yeah"].includes(answer.toLowerCase())) { + try { + const globalCmd = `flatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`; + const userCmd = `flatpak override --user ${selected.branch} --talk-name=org.freedesktop.Flatpak`; + const cmd = selected.location.startsWith("/home") + ? userCmd + : globalCmd; + execSync(cmd); + console.log("Sucessfully gave talk permission"); + } catch (err) { + console.error("Failed to give talk permission\n", err); + } + } else { + console.log(`Not giving full host access. If you change your mind later, you can run:\nflatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`); + } } for (const version of selected.versions) { diff --git a/src/ipcMain/updater/git.ts b/src/ipcMain/updater/git.ts index 07c94cb56..20cc5b1f0 100644 --- a/src/ipcMain/updater/git.ts +++ b/src/ipcMain/updater/git.ts @@ -28,10 +28,13 @@ const VENCORD_SRC_DIR = join(__dirname, ".."); const execFile = promisify(cpExecFile); +const isFlatpak = Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord")); + function git(...args: string[]) { - return execFile("git", args, { - cwd: VENCORD_SRC_DIR - }); + const opts = { cwd: VENCORD_SRC_DIR }; + + if (isFlatpak) return execFile("flatpak-spawn", ["--host", "git", ...args], opts); + else return execFile("git", args, opts); } async function getRepo() { @@ -61,9 +64,13 @@ async function pull() { } async function build() { - const res = await execFile("node", ["scripts/build/build.mjs"], { - cwd: VENCORD_SRC_DIR - }); + const opts = { cwd: VENCORD_SRC_DIR }; + + let res; + + if (isFlatpak) res = await execFile("flatpak-spawn", ["--host", "node", "scripts/build/build.mjs"], opts); + else res = await execFile("node", ["scripts/build/build.mjs"], opts); + return !res.stderr.includes("Build failed"); } From 54010aab946e722b1aa1b92f8c1da38ae480c9e1 Mon Sep 17 00:00:00 2001 From: Justice Almanzar Date: Sat, 3 Dec 2022 05:32:14 -0500 Subject: [PATCH 08/22] fix: hljs fallback (#283) --- src/plugins/shikiCodeblocks/components/Code.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/shikiCodeblocks/components/Code.tsx b/src/plugins/shikiCodeblocks/components/Code.tsx index ae41113c9..ce6a70584 100644 --- a/src/plugins/shikiCodeblocks/components/Code.tsx +++ b/src/plugins/shikiCodeblocks/components/Code.tsx @@ -17,6 +17,7 @@ */ import type { IThemedToken } from "@vap/shiki"; +import { hljs } from "@webpack/common"; import { cl } from "../utils/misc"; import { ThemeBase } from "./Highlighter"; From 6b4b4772bba84991334c0f132a255f43821ce5cd Mon Sep 17 00:00:00 2001 From: Ven Date: Sat, 3 Dec 2022 13:41:31 +0100 Subject: [PATCH 09/22] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7be2d4722..8c8466e27 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,11 @@ If you're a power user who wants to contribute and make plugins or just want to ## Installing on Browser -Install the browser extension for [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip), [![Firefox](https://img.shields.io/badge/firefox-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Firefox.xpi) or [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js). Please note that they aren't automatically updated for now, so you will regularely have to reinstall it. +[![Get the Firefox extension](https://blog.mozilla.org/addons/files/2015/11/get-the-addon-small.png)](https://addons.mozilla.org/en-GB/firefox/addon/vencord-web/) + +Or install the browser extension for +- [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip) +- [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js) - Please note that QuickCSS, shiki and other plugins making use of external resources will not work with the UserScript. You may also build them from source, to do that do the same steps as in the manual regular install method, From d4f70218ba1c76846016f5e4f8eb36577a3314d4 Mon Sep 17 00:00:00 2001 From: Ven Date: Sat, 3 Dec 2022 13:42:46 +0100 Subject: [PATCH 10/22] ci: Do not release extension-v2.zip --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 301f7879a..8ef6503b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: run: | mv dist/*.xpi dist/Vencord-for-Firefox.xpi mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip - rm -rf dist/extension-v2-unpacked + rm -rf dist/extension-v2-unpacked dist/extension-v2.zip - name: Get some values needed for the release id: release_values From 4d8145f12ccbdcb3383387cc435147c62ec1066a Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 3 Dec 2022 14:57:51 +0100 Subject: [PATCH 11/22] Fix arrpc --- src/plugins/arRPC.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/arRPC.tsx b/src/plugins/arRPC.tsx index cba3504af..ca94a0ecd 100644 --- a/src/plugins/arRPC.tsx +++ b/src/plugins/arRPC.tsx @@ -30,7 +30,7 @@ const assetManager = mapMangledModuleLazy( } ); -const rpcManager = findByCodeLazy(".APPLICATION_RPC("); +const lookupRpcApp = findByCodeLazy(".APPLICATION_RPC("); async function lookupAsset(applicationId: string, key: string): Promise { return (await assetManager.getAsset(applicationId, [key, undefined]))[0]; @@ -39,7 +39,7 @@ async function lookupAsset(applicationId: string, key: string): Promise const apps: any = {}; async function lookupApp(applicationId: string): Promise { const socket: any = {}; - await rpcManager.lookupApp(socket, applicationId); + await lookupRpcApp(socket, applicationId); return socket.application; } From c74241fde6707704f66f10d752044540783d4736 Mon Sep 17 00:00:00 2001 From: Commandtechno <68407783+Commandtechno@users.noreply.github.com> Date: Sat, 3 Dec 2022 16:11:08 -0600 Subject: [PATCH 12/22] add commas in member count (#286) l --- src/plugins/memberCount.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plugins/memberCount.tsx b/src/plugins/memberCount.tsx index c016dff75..6377a9bee 100644 --- a/src/plugins/memberCount.tsx +++ b/src/plugins/memberCount.tsx @@ -35,11 +35,13 @@ function MemberCount() { if (!c) return null; - let total = String(c[0]); + let total = c[0].toLocaleString(); if (total === "0" && c[1] > 0) { total = "Loading..."; } + const online = c[1].toLocaleString(); + return ( - + {props => (
- {c[1]} + {online}
)}
- + {props => (
Date: Sat, 3 Dec 2022 16:42:18 -0600 Subject: [PATCH 13/22] add me to contributors (#287) --- src/plugins/memberCount.tsx | 2 +- src/utils/constants.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/memberCount.tsx b/src/plugins/memberCount.tsx index 6377a9bee..947d4d7b7 100644 --- a/src/plugins/memberCount.tsx +++ b/src/plugins/memberCount.tsx @@ -93,7 +93,7 @@ function MemberCount() { export default definePlugin({ name: "MemberCount", description: "Shows the amount of online & total members in the server member list", - authors: [Devs.Ven], + authors: [Devs.Ven, Devs.Commandtechno], patches: [{ find: ".isSidebarVisible,", diff --git a/src/utils/constants.ts b/src/utils/constants.ts index f45e8b02a..3fbfe5a71 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -165,4 +165,8 @@ export const Devs = Object.freeze({ name: "King Fish", id: 499400512559382538n }, + Commandtechno: { + name: "Commandtechno", + id: 296776625432035328n, + }, }); From 53d0a555618bd3f67aca281284b62a3d20080fe6 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 3 Dec 2022 22:16:47 -0300 Subject: [PATCH 14/22] refactor(IgnoreActivities): Use React Components and support Embedded Activities (#282) --- src/plugins/ignoreActivities.ts | 178 ------------------------ src/plugins/ignoreActivities.tsx | 220 ++++++++++++++++++++++++++++++ src/plugins/keepCurrentChannel.ts | 2 +- 3 files changed, 221 insertions(+), 179 deletions(-) delete mode 100644 src/plugins/ignoreActivities.ts create mode 100644 src/plugins/ignoreActivities.tsx diff --git a/src/plugins/ignoreActivities.ts b/src/plugins/ignoreActivities.ts deleted file mode 100644 index a39b02677..000000000 --- a/src/plugins/ignoreActivities.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import * as DataStore from "@api/DataStore"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { findByPropsLazy } from "@webpack"; - -interface MatchAndReplace { - match: RegExp; - replace: string; -} - -/** Used to re-render the Registered Games tab to update how our button looks like */ -const RunningGameStoreModule = findByPropsLazy("IgnoreActivities_reRenderGames"); - -let ignoredActivitiesCache: string[] = []; - -export default definePlugin({ - name: "IgnoreActivities", - authors: [Devs.Nuckyz], - description: "Ignore certain activities (like games) from showing up on your status. You can configure which ones are ignored from the Registered Games tab.", - patches: [{ - find: ".Messages.SETTINGS_GAMES_OVERLAY_ON", - replacement: [{ - match: /;(.\.renderOverlayToggle=function\(\).+?\)};)/, - replace: (_, mod) => { - /** Modify the renderOverlayToggle button to remove unneded stuff and render the component the way we want */ - const renderIgnoreActivitiesToggle = ([ - /** Remove overlay warn related stuff */ - { match: /,.{1,2}=.{1,2}\.overlayWarn/, replace: "" }, - { match: /,.{1,2}=.{1,2}\?\(0,.{1,2}\.jsx\)\(.{1,20}Messages\.SETTINGS_GAMES_OVERLAY_WARNING.{1,100}null/, replace: "" }, - /** Remove overlay status related stuff */ - { match: /,.{1,2}=.{1,2}\?.{1,50}Messages\.SETTINGS_GAMES_OVERLAY_OFF/, replace: "" }, - { match: /[^[]{1,2},\(0,.{1,2}\.jsx\)\("div".{1,20}\(\)\.overlayStatusText.+}\),/, replace: "" }, - /** Change the method name to renderIgnoreActivitiesToggle */ - { match: /renderOverlayToggle/, replace: "renderIgnoreActivitiesToggle" }, - /** Create an easily accessable variable to use the game props and then replace the boolean to determine if the button is activated or not with our custom function */ - { match: /((.)=this\.props\.game)(.{1,70})=.{1,2}overlay/, replace: "$1,IgnoreActivities_gameProps=$2$3=Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled(IgnoreActivities_gameProps)" }, - /** Change the handler for clicking the button */ - { match: /.\.handleOverlayToggle/, replace: "() => Vencord.Plugins.plugins.IgnoreActivities.handleActivityToggle(IgnoreActivities_gameProps)" }, - /** Change the button on component to our custom */ - { match: /(\(0,.{1,2}\.jsx\)\()(.{2})\..(.{1,50}\.overlayToggleIconOn)/, replace: "$1$2.IgnoreActivities_toggleOn$3" }, - /** Change the button off component to our custom */ - { match: /(\(0,.{1,2}\.jsx\)\()(.{2})\..{1}(.{1,50}\.overlayToggleIconOff)/, replace: "$1$2.IgnoreActivities_toggleOff$3" }, - /** Change the tooltip text */ - { match: /text:.{2}\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY/, replace: 'text:"Toggle activity"' }, - /** Change the aria-label text */ - { match: /"aria-label":.{2}\..\.Messages\.SETTINGS_GAMES_TOGGLE_OVERLAY/, replace: '"aria-label":"Toggle activity"' } - ] as MatchAndReplace[]) - .reduce((current, { match, replace }) => current.replace(match, replace), mod); - - /** Return the default renderOverlayToggle and our custom one */ - return `;${mod}${renderIgnoreActivitiesToggle}`; - - } - }, { - /** Render our ignore activity component */ - match: /(this.renderLastPlayed\(\)]}\),this.renderOverlayToggle\(\))/, - replace: "$1,this.renderIgnoreActivitiesToggle()" - }] - }, { - /** Patch the RunningGameStore to export the method to re-render the Registered Games tab */ - find: '.displayName="RunningGameStore"', - replacement: { - match: /(.:\(\)=>.{2})(.+function (.{2})\(\){.+\.dispatch\({type:"RUNNING_GAMES_CHANGE")/, - replace: "$1,IgnoreActivities_reRenderGames:()=>$3$2" - } - }, { - find: "M8.67872 19H11V21H7V23H17V21H13V19H20C21.103 19 22 18.104 22 17V6C22 5.89841 21.9924 5.79857 21.9777 5.70101L20 7.67872V15H12.6787L8.67872 19ZM13.1496 6H4V15H4.14961L2.00515 17.1445C2.00174 17.0967 2 17.0486 2 17V6C2 4.897 2.897 4 4 4H15.1496L13.1496 6Z", - replacement: { - match: /(.:\(\)=>.)(.+)(function (.)\(.{1,10}\.width.+\)\)})/s, - replace: (_, exports, restOfFunction, component) => { - /** Modify the overlayToggleOff component to how we want */ - const renderIgnoreActivitiesToggleOff = ([ - /** Change the method name to IgnoreActivities_toggleOffToExport */ - { match: /function ./, replace: "function IgnoreActivities_toggleOffToExport" }, - /** Change the svg path to our custom one */ - { match: /M8.67872 19H11V21H7V23H17V21H13V19H20C21.103 19 22 18.104 22 17V6C22 5.89841 21.9924 5.79857 21.9777 5.70101L20 7.67872V15H12.6787L8.67872 19ZM13.1496 6H4V15H4.14961L2.00515 17.1445C2.00174 17.0967 2 17.0486 2 17V6C2 4.897 2.897 4 4 4H15.1496L13.1496 6Z/, replace: "M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z" }, - /** Modify the view box to not cut our svg */ - { match: /viewBox:"0 0 24 24"/, replace: 'viewBox:"0 0 32 26"' }, - /** Change the rectangle coordinates to match the middle of our svg */ - { match: /x:"2"/, replace: 'x:"3"' }, - { match: /y:"20"/, replace: 'y:"26"' }, - ] as MatchAndReplace[]) - .reduce((current, { match, replace }) => current.replace(match, replace), component); - - /** Export our custom svg */ - return `${exports},IgnoreActivities_toggleOff:()=>IgnoreActivities_toggleOffToExport${restOfFunction}${component}${renderIgnoreActivitiesToggleOff}`; - } - } - }, { - find: "M4 2.5C2.897 2.5 2 3.397 2 4.5V15.5C2 16.604 2.897 17.5 4 17.5H11V19.5H7V21.5H17V19.5H13V17.5H20C21.103 17.5 22 16.604 22 15.5V4.5C22 3.397 21.103 2.5 20 2.5H4ZM20 4.5V13.5H4V4.5H20Z", - replacement: { - match: /(.:\(\)=>.)(.+)(function (.)\(.{1,10}\.width.+\)\)})/, - replace: (_, exports, restOfFunction, component) => { - /** Modify the overlayToggleOn svg to how we want */ - const renderIgnoreActivitiesToggleOn = ([ - /** Change the method name to IgnoreActivities_toggleOnToExport */ - { match: /function ./, replace: "function IgnoreActivities_toggleOnToExport" }, - /** Change the svg path to our custom one */ - { match: /M4 2.5C2.897 2.5 2 3.397 2 4.5V15.5C2 16.604 2.897 17.5 4 17.5H11V19.5H7V21.5H17V19.5H13V17.5H20C21.103 17.5 22 16.604 22 15.5V4.5C22 3.397 21.103 2.5 20 2.5H4ZM20 4.5V13.5H4V4.5H20Z/, replace: "M 16 8 C 7.664063 8 1.25 15.34375 1.25 15.34375 L 0.65625 16 L 1.25 16.65625 C 1.25 16.65625 7.097656 23.324219 14.875 23.9375 C 15.246094 23.984375 15.617188 24 16 24 C 16.382813 24 16.753906 23.984375 17.125 23.9375 C 24.902344 23.324219 30.75 16.65625 30.75 16.65625 L 31.34375 16 L 30.75 15.34375 C 30.75 15.34375 24.335938 8 16 8 Z M 16 10 C 18.203125 10 20.234375 10.601563 22 11.40625 C 22.636719 12.460938 23 13.675781 23 15 C 23 18.613281 20.289063 21.582031 16.78125 21.96875 C 16.761719 21.972656 16.738281 21.964844 16.71875 21.96875 C 16.480469 21.980469 16.242188 22 16 22 C 15.734375 22 15.476563 21.984375 15.21875 21.96875 C 11.710938 21.582031 9 18.613281 9 15 C 9 13.695313 9.351563 12.480469 9.96875 11.4375 L 9.9375 11.4375 C 11.71875 10.617188 13.773438 10 16 10 Z M 16 12 C 14.34375 12 13 13.34375 13 15 C 13 16.65625 14.34375 18 16 18 C 17.65625 18 19 16.65625 19 15 C 19 13.34375 17.65625 12 16 12 Z M 7.25 12.9375 C 7.09375 13.609375 7 14.285156 7 15 C 7 16.753906 7.5 18.394531 8.375 19.78125 C 5.855469 18.324219 4.105469 16.585938 3.53125 16 C 4.011719 15.507813 5.351563 14.203125 7.25 12.9375 Z M 24.75 12.9375 C 26.648438 14.203125 27.988281 15.507813 28.46875 16 C 27.894531 16.585938 26.144531 18.324219 23.625 19.78125 C 24.5 18.394531 25 16.753906 25 15 C 25 14.285156 24.90625 13.601563 24.75 12.9375 Z" }, - /** Modify the view box to not cut our svg */ - { match: /viewBox:"0 0 24 24"/, replace: 'viewBox:"0 0 32 26"' }, - ] as MatchAndReplace[]) - .reduce((current, { match, replace }) => current.replace(match, replace), component); - - /** Export our custom svg */ - return `${exports},IgnoreActivities_toggleOn:()=>IgnoreActivities_toggleOnToExport${restOfFunction}${component}${renderIgnoreActivitiesToggleOn}`; - } - } - }, { - /** Patch the LocalActivityStore to filter our ignored activities before they get pushed into the array */ - find: '.displayName="LocalActivityStore"', - replacement: { - match: /((.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?;)/, - replace: "$1$2=$2.filter(Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled);" - } - }], - - async start() { - ignoredActivitiesCache = (await DataStore.get("IgnoreActivities_ignoredActivities")) ?? []; - - if (ignoredActivitiesCache.length !== 0) { - const gamesSeen: Record[] = RunningGameStoreModule.Z.getGamesSeen(); - - for (const [index, ignoredActivity] of ignoredActivitiesCache.entries()) { - if (!gamesSeen.some(game => (game.id !== undefined && game.id === ignoredActivity) || game.exePath === ignoredActivity)) { - ignoredActivitiesCache.splice(index, 1); - } - } - - await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache); - } - }, - - isActivityEnabled(props: Record) { - /** LocalActivityStore games have a "type" prop */ - if ("type" in props) { - if (props.application_id !== undefined) return !ignoredActivitiesCache.includes(props.application_id); - else { - const exePath = RunningGameStoreModule.Z.getRunningGames().find(game => game.name === props.name)?.exePath; - if (exePath) return !ignoredActivitiesCache.includes(exePath); - } - } - /** Registered Games tab games have an "exePath" prop */ - else if ("exePath" in props) { - if (props.id !== undefined) return !ignoredActivitiesCache.includes(props.id); - else return !ignoredActivitiesCache.includes(props.exePath); - } - return true; - }, - - async handleActivityToggle(props: Record) { - const id = props.id ?? props.exePath; - if (id === undefined) return; - - if (ignoredActivitiesCache.includes(id)) ignoredActivitiesCache.splice(ignoredActivitiesCache.indexOf(id, 1)); - else ignoredActivitiesCache.push(id); - RunningGameStoreModule.IgnoreActivities_reRenderGames(); - await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache); - } -}); diff --git a/src/plugins/ignoreActivities.tsx b/src/plugins/ignoreActivities.tsx new file mode 100644 index 000000000..981145c68 --- /dev/null +++ b/src/plugins/ignoreActivities.tsx @@ -0,0 +1,220 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import * as DataStore from "@api/DataStore"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { useForceUpdater } from "@utils/misc"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { Tooltip } from "webpack/common"; + +enum ActivitiesTypes { + Game, + Embedded +} + +interface IgnoredActivity { + id: string; + type: ActivitiesTypes; +} + +const RegisteredGamesClasses = findByPropsLazy("overlayToggleIconOff", "overlayToggleIconOn"); +const PreviewBadgeClasses = findByPropsLazy("previewBadge", "previewBadgeIcon"); +const BaseShapeRoundClasses = findByPropsLazy("baseShapeRound", "baseShapeRoundLeft", "baseShapeRoundRight"); +const RunningGameStore = findByPropsLazy("getRunningGames", "getGamesSeen"); + +function ToggleIconOff() { + return ( + + + + + + + ); +} + +function ToggleIconOn() { + return ( + + + + ); +} + +function ToggleActivityComponent({ activity }: { activity: IgnoredActivity; }) { + const forceUpdate = useForceUpdater(); + + return ( + + {({ onMouseLeave, onMouseEnter }) => ( +
handleActivityToggle(e, activity, forceUpdate)} + > + { + ignoredActivitiesCache.has(activity.id) + ? + : + } +
+ )} +
+ ); +} + +function ToggleActivityComponentWithBackground({ activity }: { activity: IgnoredActivity; }) { + return ( +
+ +
+ ); +} + +function handleActivityToggle(e: React.MouseEvent, activity: IgnoredActivity, forceUpdateComponent: () => void) { + e.stopPropagation(); + if (ignoredActivitiesCache.has(activity.id)) ignoredActivitiesCache.delete(activity.id); + else ignoredActivitiesCache.set(activity.id, activity); + forceUpdateComponent(); + saveCacheToDatastore(); +} + +async function saveCacheToDatastore() { + await DataStore.set("IgnoreActivities_ignoredActivities", ignoredActivitiesCache); +} + +let ignoredActivitiesCache = new Map(); + +export default definePlugin({ + name: "IgnoreActivities", + authors: [Devs.Nuckyz], + description: "Ignore certain activities (like games and actual activities) from showing up on your status. You can configure which ones are ignored from the Registered Games and Activities tabs.", + patches: [{ + find: ".Messages.SETTINGS_GAMES_OVERLAY_ON", + replacement: { + match: /(this.renderLastPlayed\(\)]}\),this.renderOverlayToggle\(\))/, + replace: "$1,Vencord.Plugins.plugins.IgnoreActivities.renderToggleGameActivityButton(this.props)" + } + }, { + find: ".Messages.NEW,name", + replacement: { + match: /\(\)\.badgeContainer.+?.\?\(0,.\.jsx\)\(.{1,2},{name:(?.)\.name}\):null/, + replace: "$&,Vencord.Plugins.plugins.IgnoreActivities.renderToggleActivityButton($)" + } + }, { + find: '.displayName="LocalActivityStore"', + replacement: { + match: /((.)\.push\(.\({type:.\..{1,3}\.LISTENING.+?;)/, + replace: "$1$2=$2.filter(Vencord.Plugins.plugins.IgnoreActivities.isActivityEnabled);" + } + }], + + async start() { + const ignoredActivitiesData = await DataStore.get>("IgnoreActivities_ignoredActivities") ?? new Map(); + /** Migrate old data */ + if (Array.isArray(ignoredActivitiesData)) { + for (const id of ignoredActivitiesData) { + ignoredActivitiesCache.set(id, { id, type: ActivitiesTypes.Game }); + } + + await saveCacheToDatastore(); + } else ignoredActivitiesCache = ignoredActivitiesData; + + if (ignoredActivitiesCache.size !== 0) { + const gamesSeen: { id?: string; exePath: string; }[] = RunningGameStore.getGamesSeen(); + + for (const ignoredActivity of ignoredActivitiesCache.values()) { + if (ignoredActivity.type !== ActivitiesTypes.Game) continue; + + if (!gamesSeen.some(game => game.id === ignoredActivity.id || game.exePath === ignoredActivity.id)) { + /** Custom added game which no longer exists */ + ignoredActivitiesCache.delete(ignoredActivity.id); + } + } + + await saveCacheToDatastore(); + } + }, + + renderToggleGameActivityButton(props: { game: { id?: string; exePath: string; } | null; }) { + if (!props.game) return (null); + + return ( + + + + ); + }, + + renderToggleActivityButton(props: { id: string; }) { + return ( + + + + ); + }, + + isActivityEnabled(props: { type: number; application_id?: string; name?: string; }) { + if (props.type === 0) { + if (props.application_id !== undefined) return !ignoredActivitiesCache.has(props.application_id); + else { + const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath; + if (exePath) return !ignoredActivitiesCache.has(exePath); + } + } + return true; + }, +}); diff --git a/src/plugins/keepCurrentChannel.ts b/src/plugins/keepCurrentChannel.ts index 0d7147c76..e553b9369 100644 --- a/src/plugins/keepCurrentChannel.ts +++ b/src/plugins/keepCurrentChannel.ts @@ -40,7 +40,7 @@ interface PreviousChannel { export default definePlugin({ name: "KeepCurrentChannel", - description: "Attempt to navigate the channel you were in before switching accounts or loading Discord.", + description: "Attempt to navigate to the channel you were in before switching accounts or loading Discord.", authors: [Devs.Nuckyz], isSwitchingAccount: false, From 8a5a5c7d1e6ab733254be6a7e6f46a4f5fab45fb Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Sun, 4 Dec 2022 13:58:29 +0100 Subject: [PATCH 15/22] UserScript: add csp bypassing fetch (#284) --- browser/GMPolyfill.js | 107 +++++++++++++++++++++++++++++++++++++ browser/userscript.meta.js | 2 +- scripts/build/buildWeb.mjs | 7 ++- 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 browser/GMPolyfill.js diff --git a/browser/GMPolyfill.js b/browser/GMPolyfill.js new file mode 100644 index 000000000..3e0606d78 --- /dev/null +++ b/browser/GMPolyfill.js @@ -0,0 +1,107 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +function fetchOptions(url) { + return new Promise((resolve, reject) => { + const opt = { + method: "OPTIONS", + url: url, + }; + opt.onload = resp => resolve(resp.responseHeaders); + opt.ontimeout = () => reject("fetch timeout"); + opt.onerror = () => reject("fetch error"); + opt.onabort = () => reject("fetch abort"); + GM_xmlhttpRequest(opt); + }); +} + +function parseHeaders(headers) { + if (!headers) + return {}; + const result = {}; + const headersArr = headers.trim().split("\n"); + for (var i = 0; i < headersArr.length; i++) { + var row = headersArr[i]; + var index = row.indexOf(":") + , key = row.slice(0, index).trim().toLowerCase() + , value = row.slice(index + 1).trim(); + + if (result[key] === undefined) { + result[key] = value; + } else if (Array.isArray(result[key])) { + result[key].push(value); + } else { + result[key] = [result[key], value]; + } + } + return result; +} + +// returns true if CORS permits request +async function checkCors(url, method) { + const headers = parseHeaders(await fetchOptions(url)); + + const origin = headers["access-control-allow-origin"]; + if (origin !== "*" && origin !== window.location.origin) return false; + + const methods = headers["access-control-allow-methods"]?.split(/,\s/g); + if (methods && !methods.includes(method)) return false; + + return true; +} + +function blobTo(to, blob) { + if (to === "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer(); + return new Promise((resolve, reject) => { + var fileReader = new FileReader(); + fileReader.onload = event => resolve(event.target.result); + if (to === "arrayBuffer") fileReader.readAsArrayBuffer(blob); + else if (to === "text") fileReader.readAsText(blob, "utf-8"); + else reject("unknown to"); + }); +} + +function GM_fetch(url, opt) { + return new Promise((resolve, reject) => { + checkCors(url, opt?.method || "GET") + .then(can => { + if (can) { + // https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest + const options = opt || {}; + options.url = url; + options.data = options.body; + options.responseType = "blob"; + options.onload = resp => { + var blob = resp.response; + resp.blob = () => Promise.resolve(blob); + resp.arrayBuffer = () => blobTo("arrayBuffer", blob); + resp.text = () => blobTo("text", blob); + resp.json = async () => JSON.parse(await blobTo("text", blob)); + resolve(resp); + }; + options.ontimeout = () => reject("fetch timeout"); + options.onerror = () => reject("fetch error"); + options.onabort = () => reject("fetch abort"); + GM_xmlhttpRequest(options); + } else { + reject("CORS issue"); + } + }); + }); +} +export const fetch = GM_fetch; diff --git a/browser/userscript.meta.js b/browser/userscript.meta.js index 81cf3e7b0..5b2a39be6 100644 --- a/browser/userscript.meta.js +++ b/browser/userscript.meta.js @@ -7,7 +7,7 @@ // @supportURL https://github.com/Vendicated/Vencord // @license GPL-3.0 // @match *://*.discord.com/* -// @grant none +// @grant GM_xmlhttpRequest // @run-at document-start // @compatible chrome Chrome + Tampermonkey or Violentmonkey // @compatible firefox Firefox Tampermonkey diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 7508937ae..c85d8aad9 100755 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -60,13 +60,18 @@ await Promise.all( }), esbuild.build({ ...commonOptions, + inject: ["browser/GMPolyfill.js", ...(commonOptions?.inject || [])], + define: { + "window": "unsafeWindow", + ...(commonOptions?.define) + }, outfile: "dist/Vencord.user.js", banner: { js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`) }, footer: { // UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local - js: "Object.defineProperty(window,'Vencord',{get:()=>Vencord});" + js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});" }, }) ] From 49b45d82624c3c53aa2a0a4f8bd9957fed75abdd Mon Sep 17 00:00:00 2001 From: Commandtechno <68407783+Commandtechno@users.noreply.github.com> Date: Mon, 5 Dec 2022 17:14:48 -0600 Subject: [PATCH 16/22] google changed their shit (#294) --- src/plugins/reverseImageSearch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/reverseImageSearch.tsx b/src/plugins/reverseImageSearch.tsx index 26e10038d..a4068ccdc 100644 --- a/src/plugins/reverseImageSearch.tsx +++ b/src/plugins/reverseImageSearch.tsx @@ -21,7 +21,7 @@ import definePlugin from "@utils/types"; import { Menu } from "@webpack/common"; const Engines = { - Google: "https://www.google.com/searchbyimage?image_url=", + Google: "https://lens.google.com/uploadbyurl?url=", Yandex: "https://yandex.com/images/search?rpt=imageview&url=", SauceNAO: "https://saucenao.com/search.php?url=", IQDB: "https://iqdb.org/?url=", From 2d08dd8a9c289bcdb6395e8bdf28b6b729946ab1 Mon Sep 17 00:00:00 2001 From: Justice Almanzar Date: Wed, 7 Dec 2022 09:33:40 -0500 Subject: [PATCH 17/22] Shiki settings preview (#297) --- src/components/PluginSettings/PluginModal.tsx | 2 +- .../shikiCodeblocks/components/Highlighter.tsx | 8 +++++++- .../shikiCodeblocks/hooks/useShikiSettings.ts | 15 +++++++++++++-- src/plugins/shikiCodeblocks/index.ts | 12 +++++++++++- src/plugins/shikiCodeblocks/previewExample.tsx | 13 +++++++++++++ src/utils/types.ts | 4 +++- 6 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/plugins/shikiCodeblocks/previewExample.tsx diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index d1916673c..7cff58f77 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -196,7 +196,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
- +
diff --git a/src/plugins/shikiCodeblocks/components/Highlighter.tsx b/src/plugins/shikiCodeblocks/components/Highlighter.tsx index 6067fd836..d86e7727c 100644 --- a/src/plugins/shikiCodeblocks/components/Highlighter.tsx +++ b/src/plugins/shikiCodeblocks/components/Highlighter.tsx @@ -42,6 +42,7 @@ export interface HighlighterProps { lang?: string; content: string; isPreview: boolean; + tempSettings?: Record; } export const createHighlighter = (props: HighlighterProps) => ( @@ -53,8 +54,13 @@ export const Highlighter = ({ lang, content, isPreview, + tempSettings, }: HighlighterProps) => { - const { tryHljs, useDevIcon, bgOpacity } = useShikiSettings(["tryHljs", "useDevIcon", "bgOpacity"]); + const { + tryHljs, + useDevIcon, + bgOpacity, + } = useShikiSettings(["tryHljs", "useDevIcon", "bgOpacity"], tempSettings); const { id: currentThemeId, theme: currentTheme } = useTheme(); const shikiLang = lang ? resolveLang(lang) : null; diff --git a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts index 416f8e9ba..0d92f8095 100644 --- a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts +++ b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts @@ -18,8 +18,19 @@ import { useSettings } from "@api/settings"; +import { shiki } from "../api/shiki"; import { ShikiSettings } from "../types"; -export function useShikiSettings(settings: (keyof ShikiSettings)[]) { - return useSettings(settings.map(setting => `plugins.ShikiCodeblocks.${setting}`)).plugins.ShikiCodeblocks as ShikiSettings; +export function useShikiSettings(settingKeys: (keyof ShikiSettings)[], overrides?: Record) { + const settings = useSettings(settingKeys.map(key => `plugins.ShikiCodeblocks.${key}`)).plugins.ShikiCodeblocks as ShikiSettings; + + const withOverrides = { ...settings, ...overrides }; + + const themeUrl = withOverrides.customTheme || withOverrides.theme; + if (themeUrl !== shiki.currentThemeUrl) shiki.setTheme(themeUrl); + + return { + ...withOverrides, + isThemeLoading: themeUrl !== shiki.currentThemeUrl, + }; } diff --git a/src/plugins/shikiCodeblocks/index.ts b/src/plugins/shikiCodeblocks/index.ts index a8be92af4..fd6b04bf7 100644 --- a/src/plugins/shikiCodeblocks/index.ts +++ b/src/plugins/shikiCodeblocks/index.ts @@ -21,6 +21,7 @@ import { parseUrl } from "@utils/misc"; import { wordsFromPascal, wordsToTitle } from "@utils/text"; import definePlugin, { OptionType } from "@utils/types"; +import previewExampleText from "~fileContent/previewExample.tsx"; import cssText from "~fileContent/style.css"; import { Settings } from "../../Vencord"; @@ -59,6 +60,12 @@ export default definePlugin({ shiki.destroy(); clearStyles(); }, + settingsAboutComponent: ({ tempSettings }) => createHighlighter({ + lang: "tsx", + content: previewExampleText, + isPreview: true, + tempSettings, + }), options: { theme: { type: OptionType.SELECT, @@ -137,7 +144,10 @@ export default definePlugin({ description: "Background opacity", markers: [0, 20, 40, 60, 80, 100], default: 100, - stickToMarkers: false, + componentProps: { + stickToMarkers: false, + onValueRender: null, // Defaults to percentage + }, }, }, diff --git a/src/plugins/shikiCodeblocks/previewExample.tsx b/src/plugins/shikiCodeblocks/previewExample.tsx new file mode 100644 index 000000000..971d01670 --- /dev/null +++ b/src/plugins/shikiCodeblocks/previewExample.tsx @@ -0,0 +1,13 @@ +/* eslint-disable header/header */ +import React from "react"; + +const handleClick = async () => + console.log((await import("@webpack/common")).Clipboard.copy("\u200b")); + +export const Example: React.FC<{ + real: boolean, + shigged?: number, +}> = ({ real, shigged }) => <> +

{`Shigg${real ? `ies${shigged === 0x1B ? "t" : ""}` : "y"}`}

+ +; diff --git a/src/utils/types.ts b/src/utils/types.ts index 310fbea08..4e230fedc 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -89,7 +89,9 @@ export interface PluginDef { * Allows you to specify a custom Component that will be rendered in your * plugin's settings page */ - settingsAboutComponent?: React.ComponentType; + settingsAboutComponent?: React.ComponentType<{ + tempSettings?: Record; + }>; } export enum OptionType { From 2de461985dadadbaae1b2b0a83cf291dd8d3a972 Mon Sep 17 00:00:00 2001 From: Justice Almanzar Date: Thu, 8 Dec 2022 09:54:19 -0500 Subject: [PATCH 18/22] fix(ShikiCodeblocks): spoilers (#298) * fix(ShikiCodeblocks): spoilers * fix a settings bug i thikn --- .../shikiCodeblocks/components/Highlighter.tsx | 16 +++++++++------- .../shikiCodeblocks/hooks/useShikiSettings.ts | 15 +++++++++++++-- src/plugins/shikiCodeblocks/style.css | 3 +++ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/plugins/shikiCodeblocks/components/Highlighter.tsx b/src/plugins/shikiCodeblocks/components/Highlighter.tsx index d86e7727c..d26cd81a4 100644 --- a/src/plugins/shikiCodeblocks/components/Highlighter.tsx +++ b/src/plugins/shikiCodeblocks/components/Highlighter.tsx @@ -46,9 +46,11 @@ export interface HighlighterProps { } export const createHighlighter = (props: HighlighterProps) => ( - - - +
+        
+            
+        
+    
); export const Highlighter = ({ lang, @@ -66,7 +68,7 @@ export const Highlighter = ({ const shikiLang = lang ? resolveLang(lang) : null; const useHljs = shouldUseHljs({ lang, tryHljs }); - const [preRef, isIntersecting] = useIntersection(true); + const [rootRef, isIntersecting] = useIntersection(true); const [tokens] = useAwaiter(async () => { if (!shikiLang || useHljs || !isIntersecting) return null; @@ -93,8 +95,8 @@ export const Highlighter = ({ if (isPreview) preClasses.push(cl("preview")); return ( -
}
             
-        
+
); }; diff --git a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts index 0d92f8095..50b0fc978 100644 --- a/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts +++ b/src/plugins/shikiCodeblocks/hooks/useShikiSettings.ts @@ -17,17 +17,28 @@ */ import { useSettings } from "@api/settings"; +import { React } from "@webpack/common"; import { shiki } from "../api/shiki"; import { ShikiSettings } from "../types"; export function useShikiSettings(settingKeys: (keyof ShikiSettings)[], overrides?: Record) { const settings = useSettings(settingKeys.map(key => `plugins.ShikiCodeblocks.${key}`)).plugins.ShikiCodeblocks as ShikiSettings; + const [isLoading, setLoading] = React.useState(false); const withOverrides = { ...settings, ...overrides }; - const themeUrl = withOverrides.customTheme || withOverrides.theme; - if (themeUrl !== shiki.currentThemeUrl) shiki.setTheme(themeUrl); + + if (overrides) { + const willChangeTheme = shiki.currentThemeUrl && themeUrl !== shiki.currentThemeUrl; + const noOverrides = Object.keys(overrides).length === 0; + + if (isLoading && (!willChangeTheme || noOverrides)) setLoading(false); + if ((!isLoading && willChangeTheme)) { + setLoading(true); + shiki.setTheme(themeUrl); + } + } return { ...withOverrides, diff --git a/src/plugins/shikiCodeblocks/style.css b/src/plugins/shikiCodeblocks/style.css index 119ff80eb..b246db4c9 100644 --- a/src/plugins/shikiCodeblocks/style.css +++ b/src/plugins/shikiCodeblocks/style.css @@ -1,5 +1,8 @@ .shiki-root { border-radius: 4px; + + /* fallback background */ + background-color: var(--background-secondary); } .shiki-root code { From 022bf17140894144fde5b59b55b5b7210c18eb93 Mon Sep 17 00:00:00 2001 From: David Ralph Date: Thu, 8 Dec 2022 22:51:18 +0000 Subject: [PATCH 19/22] fix inconsistent margins & capitalisation (#281) --- src/components/VencordSettings/ThemesTab.tsx | 11 ++++++----- src/components/VencordSettings/VencordTab.tsx | 10 +++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index 60dd96d0d..b673c4b7f 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -57,7 +57,8 @@ function Validators({ themeLinks }: { themeLinks: string[]; }) { {themeLinks.map(link => ( Paste links to .css / .theme.css files here One link per line - Be careful to use the raw links or github.io links! + Make sure to use the raw links or github.io links! Find Themes: -
+
BetterDiscord Themes - Github + GitHub
If using the BD site, click on "Source" somewhere below the Download button In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button If the theme has configuration that requires you to edit the file:
    -
  • • Make a github account
  • +
  • • Make a GitHub account
  • • Click the fork button on the top right
  • • Edit the file
  • • Use the link to your own repository instead
  • diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index ad8fe142c..df25e2d85 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -89,8 +89,8 @@ function VencordSettings() { settings.useQuickCss = v} - note="Loads styles from your QuickCss file"> - Use QuickCss + note="Loads styles from your QuickCSS file"> + Use QuickCSS {!IS_WEB && ( @@ -103,8 +103,8 @@ function VencordSettings() { settings.notifyAboutUpdates = v} - note="Shows a Toast on StartUp"> - Get notified about new Updates + note="Shows a toast on startup"> + Get notified about new updates )} @@ -131,7 +131,7 @@ function DonateCard({ image }: DonateCardProps) {
    Support the Project - Please consider supporting the Development of Vencord by donating! + Please consider supporting the development of Vencord by donating!
    From ee24439795df84f2a226d1b915d8037b074436e9 Mon Sep 17 00:00:00 2001 From: megumin Date: Thu, 8 Dec 2022 22:53:12 +0000 Subject: [PATCH 20/22] feat(plugin): sort friend requests by date received (#280) --- src/plugins/sortFriendRequests.tsx | 74 ++++++++++++++++++++++++++++++ src/utils/types.ts | 1 + src/webpack/common.tsx | 5 +- src/webpack/patchWebpack.ts | 1 + 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/plugins/sortFriendRequests.tsx diff --git a/src/plugins/sortFriendRequests.tsx b/src/plugins/sortFriendRequests.tsx new file mode 100644 index 000000000..b9732afb8 --- /dev/null +++ b/src/plugins/sortFriendRequests.tsx @@ -0,0 +1,74 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Flex } from "@components/Flex"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { RelationshipStore } from "@webpack/common"; +import { User } from "discord-types/general"; +import { Settings } from "Vencord"; + +export default definePlugin({ + name: "SortFriendRequests", + authors: [Devs.Megu], + description: "Sorts friend requests by date of receipt", + + patches: [{ + find: ".PENDING_INCOMING||", + replacement: [{ + match: /\.sortBy\(\(function\((\w)\){return \w{1,3}\.comparator}\)\)/, + // If the row type is 3 or 4 (pendinng incoming or outgoing), sort by date of receipt + // Otherwise, use the default comparator + replace: (_, row) => `.sortBy((function(${row}) { + return ${row}.type === 3 || ${row}.type === 4 + ? -Vencord.Plugins.plugins.SortFriendRequests.getSince(${row}.user) + : ${row}.comparator + }))` + }, { + predicate: () => Settings.plugins.SortFriendRequests.showDates, + match: /(user:(\w{1,3}),.{10,30}),subText:(\w{1,3}),(.{10,30}userInfo}\))/, + // Show dates in the friend request list + replace: (_, pre, user, subText, post) => `${pre}, + subText: Vencord.Plugins.plugins.SortFriendRequests.makeSubtext(${subText}, ${user}), + ${post}` + }] + }], + + getSince(user: User) { + return new Date(RelationshipStore.getSince(user.id)); + }, + + makeSubtext(text: string, user: User) { + const since = this.getSince(user); + return ( + + {text} + {!isNaN(since.getTime()) && Received — {since.toDateString()}} + + ); + }, + + options: { + showDates: { + type: OptionType.BOOLEAN, + description: "Show dates on friend requests", + default: false, + restartNeeded: true + } + } +}); diff --git a/src/utils/types.ts b/src/utils/types.ts index 4e230fedc..fd8f02baa 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -27,6 +27,7 @@ export default function definePlugin

    (p: P & Record string); + predicate?(): boolean; } export interface Patch { diff --git a/src/webpack/common.tsx b/src/webpack/common.tsx index 56846c252..0a5fd7d56 100644 --- a/src/webpack/common.tsx +++ b/src/webpack/common.tsx @@ -52,7 +52,10 @@ export let UserStore: Stores.UserStore; export let SelectedChannelStore: Stores.SelectedChannelStore; export let SelectedGuildStore: any; export let ChannelStore: Stores.ChannelStore; -export let RelationshipStore: Stores.RelationshipStore; +export let RelationshipStore: Stores.RelationshipStore & { + /** Get the date (as a string) that the relationship was created */ + getSince(userId: string): string; +}; export const Forms = {} as { FormTitle: Components.FormTitle; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 273a0623f..8f11b6348 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -137,6 +137,7 @@ function patchPush() { // @ts-ignore we change all patch.replacement to array in plugins/index for (const replacement of patch.replacement) { + if (replacement.predicate && !replacement.predicate()) continue; const lastMod = mod; const lastCode = code; From f2e0542614bd47ecb5e8ad9c2f7f2d7476c50ee1 Mon Sep 17 00:00:00 2001 From: Commandtechno <68407783+Commandtechno@users.noreply.github.com> Date: Thu, 8 Dec 2022 17:35:09 -0600 Subject: [PATCH 21/22] New Plugin: NSFWGateBypass (#300) --- src/plugins/nsfwGateBypass.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/plugins/nsfwGateBypass.ts diff --git a/src/plugins/nsfwGateBypass.ts b/src/plugins/nsfwGateBypass.ts new file mode 100644 index 000000000..8f1b28682 --- /dev/null +++ b/src/plugins/nsfwGateBypass.ts @@ -0,0 +1,35 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 OpenAsar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NSFWGateBypass", + description: "Allows you to access NSFW channels without setting/verifying your age", + authors: [Devs.Commandtechno], + patches: [ + { + find: ".nsfwAllowed=null", + replacement: { + match: /(\w+)\.nsfwAllowed=/, + replace: "$1.nsfwAllowed=true;", + }, + }, + ], +}); From ebb8da0f2336df9267e734f2ba134f1a25afcfb6 Mon Sep 17 00:00:00 2001 From: Justice Almanzar Date: Thu, 8 Dec 2022 22:32:16 -0500 Subject: [PATCH 22/22] fix(FakeNitro): more reliable patches (#304) --- src/plugins/fakeNitro.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/fakeNitro.ts b/src/plugins/fakeNitro.ts index 0a1985a45..e5ac3b9ce 100644 --- a/src/plugins/fakeNitro.ts +++ b/src/plugins/fakeNitro.ts @@ -71,8 +71,8 @@ export default definePlugin({ "canUseEmojisEverywhere" ].map(func => { return { - match: new RegExp(`${func}:function\\(.+?}`), - replace: `${func}:function(e){return true;}` + match: new RegExp(`${func}:function\\(.+?\\{`), + replace: "$&return true;" }; }) }, @@ -80,8 +80,8 @@ export default definePlugin({ find: "canUseAnimatedEmojis:function", predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true, replacement: { - match: /canUseStickersEverywhere:function\(.+?}/, - replace: "canUseStickersEverywhere:function(e){return true;}" + match: /canUseStickersEverywhere:function\(.+?\{/, + replace: "$&return true;" }, }, { @@ -101,8 +101,8 @@ export default definePlugin({ "canStreamMidQuality" ].map(func => { return { - match: new RegExp(`${func}:function\\(.+?}`), - replace: `${func}:function(e){return true;}` + match: new RegExp(`${func}:function\\(.+?\\{`), + replace: "$&return true;" }; }) },