diff --git a/.github/workflows/codeberg-mirror.yml b/.github/workflows/codeberg-mirror.yml index 647a43135..1b2266ee7 100644 --- a/.github/workflows/codeberg-mirror.yml +++ b/.github/workflows/codeberg-mirror.yml @@ -18,5 +18,5 @@ jobs: fetch-depth: 0 - uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1 with: - target_repo_url: "git@codeberg.org:Ven/cord.git" + target_repo_url: "git@codeberg.org:Vee/cord.git" ssh_private_key: ${{ secrets.CODEBERG_SSH_PRIVATE_KEY }} diff --git a/README.md b/README.md index 8611babd7..a43c9f834 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Vencord -[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Ven/cord&color=2185D0&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAKbUlEQVR4nNVae3AV5RX/nW/3Pva+b24e5HHzIICQKGoiYiW8NFBFgohaa6ctglpbFSujSGurzUinohWsOij/gGX6R2fqOK0d1FYTEZXaTrWCBbEikJCEyCvkeXNvkrunf+zdkJDkPnex/c3cmd29+53v/M6e73znnF2Cydj4Tntldzi6qrN/qKqzf2jy6b7BnL4B1dI7oMp9AyoRAIdVsNMqhlxWMZjtspzyK/Jhr036OMsm//bh2vzPzNSPzBD6xFutd7R0Dq758ky4orkjYuc05RCAkixbeEq2/UCJ1/LczxcX/c5IPfU5DMHmxpbCpu7o1k/b+xc1n43YjJI7EqV+W2RmvuPt0oDjB2vn5bQbITNjAzzdeKK8qTO0bU9T77zucNQUjzofHrvENWWu3aUBZfW6+ZOOZiIrbYXrmUXo9daX3v6i667O/iGRiRLpwqtIvKDc+0efJ3hb/UIaSkdGWgZ4sqGt9r2m3lc/P9HvSWe80ZiRp3TPL/UsX1+bvyvVsSkb4NE3WjbuPNj5SM8Fcvdk4bAKrqvwv7DxhuCPUxmXNIn6XSy3nWr6R8OhrqrU1btwqJ3m/bgwu/SqZJdEUgbYsuuka09b9/4Pm3tLMlPvwuAbpe6m+RcplfdcURBKdG9CA2zZddLV2Nx1+JO2vlxj1LswqCpynlxc6SxLZIS40bueWfy9vXvv/xt5APhXa1/u7v+EPqvfxXK8++IaoO2Vpn9+cLS33FjVLhw+bOotOX7q6N/i3TOhAX7y+rHN/+sBLxm8fah71k93tjw/0f/jGuDJxtZrdh7setA8tS4sdn7eef+v3mmfP95/Ywxw6x9Yev9I35/6Iubv83WVfl5a6Uu3VkoavZEo7TnS/Vo98xi+Yy6UKC3bDp7sd5ut1OWFDjyzNMib6oq5Oug0ezp8dqLfG3r92Nbzr48ywNONJ8obDnV/z2xlAk4ZW1aUqhaJIAvCb5YVqwFn3GBtCBoO9dz5TOPxUbnMKAM0dYa2d5lc2AgCNi8r5klui3aBgWynjE11QZbI3FV3NjQkjnYNbB+lj36wubGlcE9T71xTNQDw0Px8nlvmHl73GmfCrKCL19Tkmh4P9jT1LHz2vVP5+vmwAZq71a1m1/PXTPXwD68eS5KIEVUZd1yZwwumeEw1Qld/lJrPhF7Sz4cNsO+rUK2ZExd6rfj10iCPZ2GJCCoAZuCJxQUc9FvNVAX72kPX6ccC0Hp4zR0Ru1kT2mTCSzeXqn5l/EAniMAqoDLDYZWwqa5EVSzmhaKmsxHbLxvbbgdiBmjpHFxj2mwANlxXxBdPUib8nwgQgqAyEFUZxT4L1i/MN3UpHDsTWQvEDHDoTLjCrIluuyzAt8zMSkhGFhp5hrYUFk3z8IqZftOMcKRj4GIAEM80tFccM8n9Z+Qq+MXigqRIWCQCMzQvYIbKwH1X53FFnjkr88iZsLKpoXWa6BiIrjbDzF67hK23lKp2Obm1LAstPEZVjTwDkAio/2ZQ9dolw/VjAB0DfKfoCg9WGy2cADy1NMhBX2rR3CIRGICq8rAhAg4Jj9UWsDBhg+4MR6vF2VC0zGjB99fk8eJp3pQdyyrRMHF9KURVxswCB6+alWO4o3b2RyeLU32D2UYKnVPm5gfm5qWlrF0Wo4hzbCmoDNw0089XlboNNcLpvsFc0RtRDXuNle+x4Lkbi9PO6WWJIBFGEY+qjGjswtq5eVzosRilLnoiUavoH1INiTCyIDy/vETNcmRW1dl0L4gRVxmx3YFhlwnrry1QrZIxASE0yJIIDaiGSHt8UQFXF2Ve1zusYgzxkXGhyGvFvePUE+mgfyAqhGqAqKWVPv5udbYhSjmtkpYWq6OJqzFjqCpjTpmbl1Rk3klSGRBWmTISNC3Hjo1LgoYFJ0GA1aIVR+cTVxlQoS2Pb18a4PLszMKXzSJYuCySmq4Al03CiytKVYfBhYvLKk1IXE+XLRLhwZp81WlNf26HTFHhd0jhdAYTgKduCPLkgPHfQjitYkLiAIEZBDBlu2R6aF7euCV2Mgg45bDw2qWOdAavnp3D109PPdlJBvpTnYg4kVY3MDMuylVw62WJi63x4LHLZ0TAIR9OdWBVodPUclUQwWmT4hLXfgCIUDfDi6oiR8rzBJzyl8LnkD9KZVCOU8aLN5eoshnJ+Qh4bFJC4gztmEjgrtk5anaKnWWfXfpIuBTLjmSpSILw/E0laq7LuGxsIngVCYmIa96hLRG3TaZ1C/KTfjAEQLFIO8TPFk7aH/RZI8kMWrdgEs8udqXLKSUoMkEW4ETEQTRsoHyPlVZfmVw+Uuy3hR9bVHBQAMD0XPu/Ew24dqqH777K/La1DiKCxyYlRRzQymgG4+oyDxZOTdxZnp5r3wvEWmJ5btuL8W4uzbJh87LitLebdOFVpKSJx4IlwIzbL81CcYLO8iSX/IImGQCYae6Wg/2tXQNjNnW7LPDKyilqZd7ETU2zEBlifNTSS4i9PNFIx44x4jh2nZlBsUr0dN8QP/6XVhEaHJvnlfhtkXd/NF0BUextKRFXFznfGk+JDdcX8tdBHtDa6YpFsB4I9ac88omf8wbEgqa2XAIOme6bM35foqrQ+QZIKwGG80ifVbrXZZNGDfhOVYBvviS9JMMoaP3AEcQpPnHdOxiMGXkKbrx4dGfZY5c4T8H9+vmwAeqXFLXOKXW9r59fWuDA44sKv1byAOBzyCkTH+kdS2f4MLPgXJI0p9T17vrFxcf181GVxEUB+0qfIqt+RcKWFSWGNR4ygd4RTpW4HiCJgFWzstmnSPA7ZLU827pypPwxDB/687GXl1X6Vs6bbGz/LRN80hZCT+yLFZ0cgHED4egACeiXm89GsP9EePuzy4rvGil7jAGYmQDsBjDHUBYZ4GhHBMfORigd4rpnyIS9u6d4rqgnGrUtjCmmSYuOqwB0GcwjbWh9xviurpNnxnDA1IspMPe6bOL755MHJvhKjIgOA7jbJD4pw22Thj+kSIW47h2KRaydVezeP57sCdspRPQqgGeNJJIuBAE+ReJUiOv32mXaXjPZs21C2QnmXgdghyEsMoRfkVMiDgCywF/by9z3xJMb1wCxeHAPgDczZpAh/Iq+HSYmDjCsstgThmf5t4ii8eQm7CgS0SCA5QBezoRApnBaBSyCEhIHCLJEb4ZUd+2SqZSwzE+qpUpEQ9CC4qb01M8cRIQsh8zxiKsMtsn08nvlnrpkyAPj5AGJwMw3AtgGwJ/q2ExxvHsQB74KxfKBMblAyGmTHq4pc4/5GjQeUm6qE9FrAK4E8H6ie41GlkN/jTk6F5Ak2ueUpNmpkgfSMAAAENERAAsB3AHgZDoy0oFdFnBYpXPEBfU4beLRD6Z4qmumug+kIzPjaoeZfQDWAHgAQFam8hLh4MkwWjsHemyS2OF08IYrCjynzZ4zKTCzi5nXMvOnzBw16bevIxR95JOj7DNKb1PqXWa+HMDtAGoBXII0lxq0N2OfAmgA8Hsi2muMhudgesHPzNkA5gKoADADwFRoS8UHQO+x9wLoBNAB4AsAnwM4AOADIjLVxf8L9kdXUOE0IskAAAAASUVORK5CYII=)](https://codeberg.org/Ven/cord) +[![Codeberg Mirror](https://img.shields.io/static/v1?style=for-the-badge&label=Codeberg%20Mirror&message=codeberg.org/Vee/cord&color=2185D0&logo=data:image/png;base64,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)](https://codeberg.org/Vee/cord) The cutest Discord client mod diff --git a/package.json b/package.json index b59fbafe7..076b2999d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.6.5", + "version": "1.6.7", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/scripts/runInstaller.mjs b/scripts/runInstaller.mjs index b35039f8a..145ea5a5e 100644 --- a/scripts/runInstaller.mjs +++ b/scripts/runInstaller.mjs @@ -35,11 +35,11 @@ const ETAG_FILE = join(FILE_DIR, "etag.txt"); function getFilename() { switch (process.platform) { case "win32": - return "VencordInstaller.exe"; + return "VencordInstallerCli.exe"; case "darwin": return "VencordInstaller.MacOS.zip"; case "linux": - return "VencordInstaller-" + (process.env.WAYLAND_DISPLAY ? "wayland" : "x11"); + return "VencordInstallerCli-linux"; default: throw new Error("Unsupported platform: " + process.platform); } @@ -118,11 +118,15 @@ const installerBin = await ensureBinary(); console.log("Now running Installer..."); -execFileSync(installerBin, { - stdio: "inherit", - env: { - ...process.env, - VENCORD_USER_DATA_DIR: BASE_DIR, - VENCORD_DEV_INSTALL: "1" - } -}); +try { + execFileSync(installerBin, { + stdio: "inherit", + env: { + ...process.env, + VENCORD_USER_DATA_DIR: BASE_DIR, + VENCORD_DEV_INSTALL: "1" + } + }); +} catch { + console.error("Something went wrong. Please check the logs above."); +} diff --git a/src/Vencord.ts b/src/Vencord.ts index a106a0b7d..29e965fa0 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -44,7 +44,7 @@ async function syncSettings() { // pre-check for local shared settings if ( Settings.cloud.authenticated && - await dsGet("Vencord_cloudSecret") === null // this has been enabled due to local settings share or some other bug + !await dsGet("Vencord_cloudSecret") // this has been enabled due to local settings share or some other bug ) { // show a notification letting them know and tell them how to fix it showNotification({ @@ -145,4 +145,3 @@ document.addEventListener("DOMContentLoaded", () => { })); } }, { once: true }); - diff --git a/src/api/ChatButton.css b/src/api/ChatButton.css new file mode 100644 index 000000000..30869a846 --- /dev/null +++ b/src/api/ChatButton.css @@ -0,0 +1,4 @@ +.vc-chatbar-button { + display: flex; + align-items: center; +} diff --git a/src/api/ChatButtons.tsx b/src/api/ChatButtons.tsx new file mode 100644 index 000000000..fcb76fffc --- /dev/null +++ b/src/api/ChatButtons.tsx @@ -0,0 +1,128 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./ChatButton.css"; + +import ErrorBoundary from "@components/ErrorBoundary"; +import { Logger } from "@utils/Logger"; +import { waitFor } from "@webpack"; +import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; +import { Channel } from "discord-types/general"; +import { HTMLProps, MouseEventHandler, ReactNode } from "react"; + +let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>; +waitFor(["buttonContainer", "channelTextArea"], m => ChannelTextAreaClasses = m); + +export interface ChatBarProps { + channel: Channel; + disabled: boolean; + isEmpty: boolean; + type: { + analyticsName: string; + attachments: boolean; + autocomplete: { + addReactionShortcut: boolean, + forceChatLayer: boolean, + reactions: boolean; + }, + commands: { + enabled: boolean; + }, + drafts: { + type: number, + commandType: number, + autoSave: boolean; + }, + emojis: { + button: boolean; + }, + gifs: { + button: boolean, + allowSending: boolean; + }, + gifts: { + button: boolean; + }, + permissions: { + requireSendMessages: boolean; + }, + showThreadPromptOnReply: boolean, + stickers: { + button: boolean, + allowSending: boolean, + autoSuggest: boolean; + }, + users: { + allowMentioning: boolean; + }, + submit: { + button: boolean, + ignorePreference: boolean, + disableEnterToSubmit: boolean, + clearOnSubmit: boolean, + useDisabledStylesOnSubmit: boolean; + }, + uploadLongMessages: boolean, + upsellLongMessages: { + iconOnly: boolean; + }, + showCharacterCount: boolean, + sedReplace: boolean; + }; +} + +export type ChatBarButton = (props: ChatBarProps & { isMainChat: boolean; }) => JSX.Element | null; + +const buttonFactories = new Map(); +const logger = new Logger("ChatButtons"); + +export function _injectButtons(buttons: ReactNode[], props: ChatBarProps) { + if (props.disabled) return; + + for (const [key, Button] of buttonFactories) { + buttons.push( + logger.error(`Failed to render ${key}`, e.error)}> + + + )} + + ); +}, { noop: true }); diff --git a/src/api/index.ts b/src/api/index.ts index 08f238104..5dca63105 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -17,6 +17,7 @@ */ import * as $Badges from "./Badges"; +import * as $ChatButtons from "./ChatButtons"; import * as $Commands from "./Commands"; import * as $ContextMenu from "./ContextMenu"; import * as $DataStore from "./DataStore"; @@ -104,3 +105,8 @@ export const Notifications = $Notifications; * An api allowing you to patch and add/remove items to/from context menus */ export const ContextMenu = $ContextMenu; + +/** + * An API allowing you to add buttons to the chat input + */ +export const ChatButtons = $ChatButtons; diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index 2808494a1..8abaaba4f 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -21,9 +21,11 @@ import { classNameFactory } from "@api/Styles"; import { Flex } from "@components/Flex"; import { DeleteIcon } from "@components/Icons"; import { Link } from "@components/Link"; +import PluginModal from "@components/PluginSettings/PluginModal"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; +import { openModal } from "@utils/modal"; import { showItemInFolder } from "@utils/native"; import { useAwaiter } from "@utils/react"; import { findByPropsLazy, findLazy } from "@webpack"; @@ -248,6 +250,21 @@ function ThemesTab() { > Edit QuickCSS + + {Vencord.Settings.plugins.ClientTheme.enabled && ( + + )} diff --git a/src/components/VencordSettings/UpdaterTab.tsx b/src/components/VencordSettings/UpdaterTab.tsx index 81433960f..0a5d1f149 100644 --- a/src/components/VencordSettings/UpdaterTab.tsx +++ b/src/components/VencordSettings/UpdaterTab.tsx @@ -81,9 +81,12 @@ function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) { return ( - + {updates.map(({ hash, author, message }) => ( -
+
) : ( - {isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"} + {isOutdated ? (updates.length === 1 ? "There is 1 Update" : `There are ${updates.length} Updates`) : "Up to Date!"} )} diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index 07d777eb3..ab910ea2a 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -83,10 +83,10 @@ function VencordSettings() { title: "Use Windows' native title bar instead of Discord's custom one", note: "Requires a full restart" }), - !IS_WEB && false /* This causes electron to freeze / white screen for some people */ && { + !IS_WEB && { key: "transparent", - title: "Enable window transparency", - note: "Requires a full restart" + title: "Enable window transparency.", + note: "You need a theme that supports transparency or this will do nothing. Will stop the window from being resizable. Requires a full restart" }, !IS_WEB && isWindows && { key: "winCtrlQ", diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts index 6254bc826..47d400eb6 100644 --- a/src/main/ipcMain.ts +++ b/src/main/ipcMain.ts @@ -139,8 +139,15 @@ export function initIpc(mainWindow: BrowserWindow) { } ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { + const title = "Vencord QuickCSS Editor"; + const existingWindow = BrowserWindow.getAllWindows().find(w => w.title === title); + if (existingWindow && !existingWindow.isDestroyed()) { + existingWindow.focus(); + return; + } + const win = new BrowserWindow({ - title: "Vencord QuickCSS Editor", + title, autoHideMenuBar: true, darkTheme: true, webPreferences: { diff --git a/src/main/patcher.ts b/src/main/patcher.ts index 0cc92550c..3ee44d92c 100644 --- a/src/main/patcher.ts +++ b/src/main/patcher.ts @@ -79,8 +79,7 @@ if (!IS_VANILLA) { delete options.frame; } - // This causes electron to freeze / white screen for some people - if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) { + if (settings.transparent) { options.transparent = true; options.backgroundColor = "#00000000"; } @@ -130,6 +129,15 @@ if (!IS_VANILLA) { }); process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord"); + + // Monkey patch commandLine to disable WidgetLayering: Fix DevTools context menus https://github.com/electron/electron/issues/38790 + const originalAppend = app.commandLine.appendSwitch; + app.commandLine.appendSwitch = function (...args) { + if (args[0] === "disable-features" && !args[1]?.includes("WidgetLayering")) { + args[1] += ",WidgetLayering"; + } + return originalAppend.apply(this, args); + }; } else { console.log("[Vencord] Running in vanilla mode. Not loading Vencord"); } diff --git a/src/main/updater/git.ts b/src/main/updater/git.ts index e96026443..2ff3ba512 100644 --- a/src/main/updater/git.ts +++ b/src/main/updater/git.ts @@ -73,6 +73,8 @@ async function build() { const command = isFlatpak ? "flatpak-spawn" : "node"; const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"]; + if (IS_DEV) args.push("--dev"); + const res = await execFile(command, args, opts); return !res.stderr.includes("Build failed"); diff --git a/src/plugins/_api/chatButtons.ts b/src/plugins/_api/chatButtons.ts new file mode 100644 index 000000000..ca85964c0 --- /dev/null +++ b/src/plugins/_api/chatButtons.ts @@ -0,0 +1,22 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "ChatInputButtonAPI", + description: "API to add buttons to the chat input", + authors: [Devs.Ven], + + patches: [{ + find: 'location:"ChannelTextAreaButtons"', + replacement: { + match: /if\(!\i\.isMobile\)\{(?=.+?&&(\i)\.push\(.{0,50}"gift")/, + replace: "$&Vencord.Api.ChatButtons._injectButtons($1,arguments[0]);" + } + }] +}); diff --git a/src/plugins/anonymiseFileNames/index.ts b/src/plugins/anonymiseFileNames/index.ts deleted file mode 100644 index 9e69d7a93..000000000 --- a/src/plugins/anonymiseFileNames/index.ts +++ /dev/null @@ -1,94 +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 { Settings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const enum Methods { - Random, - Consistent, - Timestamp, -} - -const tarExtMatcher = /\.tar\.\w+$/; - -export default definePlugin({ - name: "AnonymiseFileNames", - authors: [Devs.obscurity], - description: "Anonymise uploaded file names", - patches: [ - { - find: "instantBatchUpload:function", - replacement: { - match: /uploadFiles:(.{1,2}),/, - replace: - "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f.filename)),$1(...args)),", - }, - }, - ], - - options: { - method: { - description: "Anonymising method", - type: OptionType.SELECT, - options: [ - { label: "Random Characters", value: Methods.Random, default: true }, - { label: "Consistent", value: Methods.Consistent }, - { label: "Timestamp (4chan-like)", value: Methods.Timestamp }, - ], - }, - randomisedLength: { - description: "Random characters length", - type: OptionType.NUMBER, - default: 7, - disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Random, - }, - consistent: { - description: "Consistent filename", - type: OptionType.STRING, - default: "image", - disabled: () => Settings.plugins.AnonymiseFileNames.method !== Methods.Consistent, - }, - }, - - anonymise(file: string) { - let name = "image"; - const tarMatch = tarExtMatcher.exec(file); - const extIdx = tarMatch?.index ?? file.lastIndexOf("."); - const ext = extIdx !== -1 ? file.slice(extIdx) : ""; - - switch (Settings.plugins.AnonymiseFileNames.method) { - case Methods.Random: - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - name = Array.from( - { length: Settings.plugins.AnonymiseFileNames.randomisedLength }, - () => chars[Math.floor(Math.random() * chars.length)] - ).join(""); - break; - case Methods.Consistent: - name = Settings.plugins.AnonymiseFileNames.consistent; - break; - case Methods.Timestamp: - // UNIX timestamp in nanos, i could not find a better dependency-less way - name = `${Math.floor(Date.now() / 1000)}${Math.floor(window.performance.now())}`; - break; - } - return name + ext; - }, -}); diff --git a/src/plugins/anonymiseFileNames/index.tsx b/src/plugins/anonymiseFileNames/index.tsx new file mode 100644 index 000000000..0382b65c2 --- /dev/null +++ b/src/plugins/anonymiseFileNames/index.tsx @@ -0,0 +1,130 @@ +/* + * 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 { Upload } from "@api/MessageEvents"; +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByCodeLazy, findByPropsLazy } from "@webpack"; + +type AnonUpload = Upload & { anonymise?: boolean; }; + +const ActionBarIcon = findByCodeLazy(".actionBarIcon)"); +const UploadDraft = findByPropsLazy("popFirstFile", "update"); + +const enum Methods { + Random, + Consistent, + Timestamp, +} + +const tarExtMatcher = /\.tar\.\w+$/; + +const settings = definePluginSettings({ + anonymiseByDefault: { + description: "Whether to anonymise file names by default", + type: OptionType.BOOLEAN, + default: true, + }, + method: { + description: "Anonymising method", + type: OptionType.SELECT, + options: [ + { label: "Random Characters", value: Methods.Random, default: true }, + { label: "Consistent", value: Methods.Consistent }, + { label: "Timestamp", value: Methods.Timestamp }, + ], + }, + randomisedLength: { + description: "Random characters length", + type: OptionType.NUMBER, + default: 7, + disabled: () => settings.store.method !== Methods.Random, + }, + consistent: { + description: "Consistent filename", + type: OptionType.STRING, + default: "image", + disabled: () => settings.store.method !== Methods.Consistent, + }, +}); + +export default definePlugin({ + name: "AnonymiseFileNames", + authors: [Devs.obscurity], + description: "Anonymise uploaded file names", + patches: [ + { + find: "instantBatchUpload:function", + replacement: { + match: /uploadFiles:(.{1,2}),/, + replace: + "uploadFiles:(...args)=>(args[0].uploads.forEach(f=>f.filename=$self.anonymise(f)),$1(...args)),", + }, + }, + { + find: ".Messages.ATTACHMENT_UTILITIES_SPOILER", + replacement: { + match: /(?<=children:\[)(?=.{10,80}tooltip:.{0,100}\i\.\i\.Messages\.ATTACHMENT_UTILITIES_SPOILER)/, + replace: "arguments[0].canEdit!==false?$self.renderIcon(arguments[0]):null," + }, + }, + ], + settings, + + renderIcon: ErrorBoundary.wrap(({ upload, channelId, draftType }: { upload: AnonUpload; draftType: unknown; channelId: string; }) => { + const anonymise = upload.anonymise ?? settings.store.anonymiseByDefault; + return ( + { + upload.anonymise = !anonymise; + UploadDraft.update(channelId, upload.id, draftType, {}); // dummy update so component rerenders + }} + > + {anonymise + ? + : + } + + ); + }, { noop: true }), + + anonymise(upload: AnonUpload) { + if ((upload.anonymise ?? settings.store.anonymiseByDefault) === false) return upload.filename; + + const file = upload.filename; + const tarMatch = tarExtMatcher.exec(file); + const extIdx = tarMatch?.index ?? file.lastIndexOf("."); + const ext = extIdx !== -1 ? file.slice(extIdx) : ""; + + switch (settings.store.method) { + case Methods.Random: + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + return Array.from( + { length: settings.store.randomisedLength }, + () => chars[Math.floor(Math.random() * chars.length)] + ).join("") + ext; + case Methods.Consistent: + return settings.store.consistent + ext; + case Methods.Timestamp: + return Date.now() + ext; + } + }, +}); diff --git a/src/plugins/betterGifPicker/index.ts b/src/plugins/betterGifPicker/index.ts new file mode 100644 index 000000000..09bb570d7 --- /dev/null +++ b/src/plugins/betterGifPicker/index.ts @@ -0,0 +1,23 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "BetterGifPicker", + description: "Makes the gif picker open the favourite category by default", + authors: [Devs.Samwich], + patches: [ + { + find: ".GIFPickerResultTypes.SEARCH", + replacement: [{ + match: "this.state={resultType:null}", + replace: 'this.state={resultType:"Favorites"}' + }] + } + ] +}); diff --git a/src/plugins/biggerStreamPreview/index.tsx b/src/plugins/biggerStreamPreview/index.tsx index acad564da..40bbe30a8 100644 --- a/src/plugins/biggerStreamPreview/index.tsx +++ b/src/plugins/biggerStreamPreview/index.tsx @@ -82,7 +82,7 @@ export const streamContextPatch: NavContextMenuPatchCallback = (children, { stre }; export const userContextPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => { - return addViewStreamContext(children, { userId: user.id }); + if (user) return addViewStreamContext(children, { userId: user.id }); }; export default definePlugin({ diff --git a/src/plugins/clearURLs/defaultRules.ts b/src/plugins/clearURLs/defaultRules.ts index 0633b717d..b5aae5ff3 100644 --- a/src/plugins/clearURLs/defaultRules.ts +++ b/src/plugins/clearURLs/defaultRules.ts @@ -140,11 +140,11 @@ export const defaultRules = [ "tt_content", "lr@yandex.*", "redircnt@yandex.*", - "feature@youtube.com", - "kw@youtube.com", - "si@youtube.com", - "pp@youtube.com", - "si@youtu.be", + "feature@*.youtube.com", + "kw@*.youtube.com", + "si@*.youtube.com", + "pp@*.youtube.com", + "si@*.youtu.be", "wt_zmc", "utm_source", "utm_content", @@ -153,5 +153,6 @@ export const defaultRules = [ "utm_term", "si@open.spotify.com", "igshid", + "igsh", "share_id@reddit.com", ]; diff --git a/src/plugins/clientTheme/clientTheme.css b/src/plugins/clientTheme/clientTheme.css index 023f88bd2..64aefdcf5 100644 --- a/src/plugins/clientTheme/clientTheme.css +++ b/src/plugins/clientTheme/clientTheme.css @@ -19,6 +19,16 @@ border: thin solid var(--background-modifier-accent) !important; } -.client-theme-warning { +.client-theme-warning * { color: var(--text-danger); } + +.client-theme-contrast-warning { + background-color: var(--background-primary); + padding: 0.5rem; + border-radius: .5rem; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} diff --git a/src/plugins/clientTheme/index.tsx b/src/plugins/clientTheme/index.tsx index d75929961..5d8cd4dc0 100644 --- a/src/plugins/clientTheme/index.tsx +++ b/src/plugins/clientTheme/index.tsx @@ -8,19 +8,19 @@ import "./clientTheme.css"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import { getTheme, Theme } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import definePlugin, { OptionType, StartAt } from "@utils/types"; -import { findComponentByCodeLazy } from "@webpack"; -import { Button, Forms } from "@webpack/common"; +import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; +import { Button, Forms, lodash as _, useStateFromStores } from "@webpack/common"; const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); const colorPresets = [ "#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D", "#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42", - "#3C2E42", "#422938" + "#3C2E42", "#422938", "#b6908f", "#bfa088", "#d3c77d", + "#86ac86", "#88aab3", "#8693b5", "#8a89ba", "#ad94bb", ]; function onPickColor(color: number) { @@ -30,9 +30,35 @@ function onPickColor(color: number) { updateColorVars(hexColor); } +const { saveClientTheme } = findByPropsLazy("saveClientTheme"); + +function setTheme(theme: string) { + saveClientTheme({ theme }); +} + +const ThemeStore = findStoreLazy("ThemeStore"); +const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore"); + function ThemeSettings() { - const lightnessWarning = hexToLightness(settings.store.color) > 45; - const lightModeWarning = getTheme() === Theme.Light; + const theme = useStateFromStores([ThemeStore], () => ThemeStore.theme); + const isLightTheme = theme === "light"; + const oppositeTheme = isLightTheme ? "dark" : "light"; + + const nitroTheme = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset); + const nitroThemeEnabled = nitroTheme !== undefined; + + const selectedLuminance = relativeLuminance(settings.store.color); + + let contrastWarning = false, fixableContrast = true; + if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12) + contrastWarning = true; + if (selectedLuminance < 0.26 && selectedLuminance > 0.12) + fixableContrast = false; + // light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels + if (isLightTheme && selectedLuminance > 0.65) { + contrastWarning = true; + fixableContrast = false; + } return (
@@ -48,15 +74,18 @@ function ThemeSettings() { suggestedColors={colorPresets} />
- {lightnessWarning || lightModeWarning - ?
- - Your theme won't look good: - {lightnessWarning && Selected color is very light} - {lightModeWarning && Light mode isn't supported} + {(contrastWarning || nitroThemeEnabled) && (<> + +
+
+ Warning, your theme won't look good: + {contrastWarning && Selected color won't contrast well with text} + {nitroThemeEnabled && Nitro themes aren't supported} +
+ {(contrastWarning && fixableContrast) && } + {(nitroThemeEnabled) && }
- : null - } + )}
); } @@ -87,9 +116,12 @@ export default definePlugin({ settings, startAt: StartAt.DOMContentLoaded, - start() { + async start() { updateColorVars(settings.store.color); - generateColorOffsets(); + + const styles = await getStyles(); + generateColorOffsets(styles); + generateLightModeFixes(styles); }, stop() { @@ -98,56 +130,86 @@ export default definePlugin({ } }); -const variableRegex = /(--primary-[5-9]\d{2}-hsl):.*?(\S*)%;/g; +const variableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g; +const lightVariableRegex = /^--primary-[1-5]\d{2}-hsl/g; +const darkVariableRegex = /^--primary-[5-9]\d{2}-hsl/g; -async function generateColorOffsets() { - - const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]'); - const variableLightness = {} as Record; - - // Search all stylesheets for color variables - for (const styleLinkNode of styleLinkNodes) { - const cssLink = styleLinkNode.getAttribute("href"); - if (!cssLink) continue; - - const res = await fetch(cssLink); - const cssString = await res.text(); - - // Get lightness values of --primary variables >=500 - let variableMatch = variableRegex.exec(cssString); - while (variableMatch !== null) { - const [, variable, lightness] = variableMatch; - variableLightness[variable] = parseFloat(lightness); - variableMatch = variableRegex.exec(cssString); - } - } - - // Generate offsets - const lightnessOffsets = Object.entries(variableLightness) +// generates variables per theme by: +// - matching regex (so we can limit what variables are included in light/dark theme, otherwise text becomes unreadable) +// - offset from specified center (light/dark theme get different offsets because light uses 100 for background-primary, while dark uses 600) +function genThemeSpecificOffsets(variableLightness: Record, regex: RegExp, centerVariable: string): string { + return Object.entries(variableLightness).filter(([key]) => key.search(regex) > -1) .map(([key, lightness]) => { - const lightnessOffset = lightness - variableLightness["--primary-600-hsl"]; + const lightnessOffset = lightness - variableLightness[centerVariable]; const plusOrMinus = lightnessOffset >= 0 ? "+" : "-"; return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`; }) .join("\n"); +} - const style = document.createElement("style"); - style.setAttribute("id", "clientThemeOffsets"); - style.textContent = `:root:root { - ${lightnessOffsets} - }`; - document.head.appendChild(style); + +function generateColorOffsets(styles) { + const variableLightness = {} as Record; + + // Get lightness values of --primary variables + let variableMatch = variableRegex.exec(styles); + while (variableMatch !== null) { + const [, variable, lightness] = variableMatch; + variableLightness[variable] = parseFloat(lightness); + variableMatch = variableRegex.exec(styles); + } + + createStyleSheet("clientThemeOffsets", [ + `.theme-light {\n ${genThemeSpecificOffsets(variableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`, + `.theme-dark {\n ${genThemeSpecificOffsets(variableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`, + ].join("\n\n")); +} + +function generateLightModeFixes(styles) { + const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm; + // get light capturing groups that mention --white-500 + const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat(); + + const groupBackgroundRegex = /^([^{]*)\{background:var\(--white-500\)/m; + const groupBackgroundColorRegex = /^([^{]*)\{background-color:var\(--white-500\)/m; + // find all capturing groups that assign background or background-color directly to w500 + const backgroundGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundRegex)).join(",\n"); + const backgroundColorGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundColorRegex)).join(",\n"); + // create css to reassign them to --primary-100 + const reassignBackgrounds = `${backgroundGroups} {\n background: var(--primary-100) \n}`; + const reassignBackgroundColors = `${backgroundColorGroups} {\n background-color: var(--primary-100) \n}`; + + const groupBgVarRegex = /\.theme-light\{([^}]*--[^:}]*(?:background|bg)[^:}]*:var\(--white-500\)[^}]*)\}/m; + const bgVarRegex = /^(--[^:]*(?:background|bg)[^:]*):var\(--white-500\)/m; + // get all global variables used for backgrounds + const lightVars = mapReject(relevantStyles, style => captureOne(style, groupBgVarRegex)) // get the insides of capture groups that have at least one background var with w500 + .map(str => str.split(";")).flat(); // captureGroupInsides[] -> cssRule[] + const lightBgVars = mapReject(lightVars, variable => captureOne(variable, bgVarRegex)); // remove vars that aren't for backgrounds or w500 + // create css to reassign every var + const reassignVariables = `.theme-light {\n ${lightBgVars.map(variable => `${variable}: var(--primary-100);`).join("\n")} \n}`; + + createStyleSheet("clientThemeLightModeFixes", [ + reassignBackgrounds, + reassignBackgroundColors, + reassignVariables, + ].join("\n\n")); +} + +function captureOne(str, regex) { + const result = str.match(regex); + return (result === null) ? null : result[1]; +} + +function mapReject(arr, mapFunc, rejectFunc = _.isNull) { + return _.reject(arr.map(mapFunc), rejectFunc); } function updateColorVars(color: string) { const { hue, saturation, lightness } = hexToHSL(color); let style = document.getElementById("clientThemeVars"); - if (!style) { - style = document.createElement("style"); - style.setAttribute("id", "clientThemeVars"); - document.head.appendChild(style); - } + if (!style) + style = createStyleSheet("clientThemeVars"); style.textContent = `:root { --theme-h: ${hue}; @@ -156,6 +218,28 @@ function updateColorVars(color: string) { }`; } +function createStyleSheet(id, content = "") { + const style = document.createElement("style"); + style.setAttribute("id", id); + style.textContent = content.split("\n").map(line => line.trim()).join("\n"); + document.body.appendChild(style); + return style; +} + +// returns all of discord's native styles in a single string +async function getStyles(): Promise { + let out = ""; + const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]'); + for (const styleLinkNode of styleLinkNodes) { + const cssLink = styleLinkNode.getAttribute("href"); + if (!cssLink) continue; + + const res = await fetch(cssLink); + out += await res.text(); + } + return out; +} + // https://css-tricks.com/converting-color-spaces-in-javascript/ function hexToHSL(hexCode: string) { // Hex => RGB normalized to 0-1 @@ -198,17 +282,14 @@ function hexToHSL(hexCode: string) { return { hue, saturation, lightness }; } -// Minimized math just for lightness, lowers lag when changing colors -function hexToLightness(hexCode: string) { - // Hex => RGB normalized to 0-1 - const r = parseInt(hexCode.substring(0, 2), 16) / 255; - const g = parseInt(hexCode.substring(2, 4), 16) / 255; - const b = parseInt(hexCode.substring(4, 6), 16) / 255; +// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance +function relativeLuminance(hexCode: string) { + const normalize = (x: number) => + x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; - const cMax = Math.max(r, g, b); - const cMin = Math.min(r, g, b); + const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255); + const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255); + const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255); - const lightness = 100 * ((cMax + cMin) / 2); - - return lightness; + return r * 0.2126 + g * 0.7152 + b * 0.0722; } diff --git a/src/plugins/copyUserURLs/index.tsx b/src/plugins/copyUserURLs/index.tsx index e3c336fb2..9f69674cf 100644 --- a/src/plugins/copyUserURLs/index.tsx +++ b/src/plugins/copyUserURLs/index.tsx @@ -30,6 +30,8 @@ interface UserContextProps { } const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => { + if (!user) return; + children.push( - - - You can also access Decor decorations from the { - e.preventDefault(); - closeAllModals(); - FluxDispatcher.dispatch({ type: "USER_SETTINGS_MODAL_SET_SECTION", section: "Profile Customization" }); - }} - >Profiles page. - -
; - } - } -}); export default definePlugin({ name: "Decor", description: "Create and use your own custom avatar decorations, or pick your favorite from the presets.", diff --git a/src/plugins/decor/settings.tsx b/src/plugins/decor/settings.tsx new file mode 100644 index 000000000..0d3628cc6 --- /dev/null +++ b/src/plugins/decor/settings.tsx @@ -0,0 +1,47 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { Link } from "@components/Link"; +import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; +import { closeAllModals } from "@utils/modal"; +import { OptionType } from "@utils/types"; +import { FluxDispatcher, Forms } from "@webpack/common"; + +import DecorSection from "./ui/components/DecorSection"; + +export const settings = definePluginSettings({ + changeDecoration: { + type: OptionType.COMPONENT, + description: "Change your avatar decoration", + component() { + if (!Vencord.Plugins.plugins.Decor.started) return + Enable Decor and restart your client to change your avatar decoration. + ; + + return
+ + + You can also access Decor decorations from the { + e.preventDefault(); + closeAllModals(); + FluxDispatcher.dispatch({ type: "USER_SETTINGS_MODAL_SET_SECTION", section: "Profile Customization" }); + }} + >Profiles page. + +
; + } + }, + agreedToGuidelines: { + type: OptionType.BOOLEAN, + description: "Agreed to guidelines", + hidden: true, + default: false + } +}); diff --git a/src/plugins/decor/ui/components/index.ts b/src/plugins/decor/ui/components/index.ts index 8f39a10ee..8fe41fc92 100644 --- a/src/plugins/decor/ui/components/index.ts +++ b/src/plugins/decor/ui/components/index.ts @@ -19,7 +19,7 @@ export let DecorationGridItem: DecorationGridItemComponent; export const setDecorationGridItem = v => DecorationGridItem = v; export const AvatarDecorationModalPreview = LazyComponentWebpack(() => { - const component = findComponentByCode("AvatarDecorationModalPreview"); + const component = findComponentByCode(".shopPreviewBanner"); return React.memo(component); }); diff --git a/src/plugins/decor/ui/index.ts b/src/plugins/decor/ui/index.ts index 52b169d77..0ead602e2 100644 --- a/src/plugins/decor/ui/index.ts +++ b/src/plugins/decor/ui/index.ts @@ -5,9 +5,10 @@ */ import { classNameFactory } from "@api/Styles"; -import { extractAndLoadChunksLazy } from "@webpack"; +import { extractAndLoadChunksLazy, findByPropsLazy } from "@webpack"; export const cl = classNameFactory("vc-decor-"); +export const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); export const requireAvatarDecorationModal = extractAndLoadChunksLazy(["openAvatarDecorationModal:"]); export const requireCreateStickerModal = extractAndLoadChunksLazy(["stickerInspected]:"]); diff --git a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx index bed007174..5fbe165ce 100644 --- a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx @@ -4,12 +4,13 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; -import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; +import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { findComponentByCodeLazy } from "@webpack"; import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common"; import { User } from "discord-types/general"; @@ -18,16 +19,17 @@ import { GUILD_ID, INVITE_KEY } from "../../lib/constants"; import { useAuthorizationStore } from "../../lib/stores/AuthorizationStore"; import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; import { decorationToAvatarDecoration } from "../../lib/utils/decoration"; -import { cl, requireAvatarDecorationModal } from "../"; +import { settings } from "../../settings"; +import { cl, DecorationModalStyles, requireAvatarDecorationModal } from "../"; import { AvatarDecorationModalPreview } from "../components"; import DecorationGridCreate from "../components/DecorationGridCreate"; import DecorationGridNone from "../components/DecorationGridNone"; import DecorDecorationGridDecoration from "../components/DecorDecorationGridDecoration"; import SectionedGridList from "../components/SectionedGridList"; import { openCreateDecorationModal } from "./CreateDecorationModal"; +import { openGuidelinesModal } from "./GuidelinesModal"; const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); -const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); function usePresets() { const [presets, setPresets] = useState([]); @@ -83,7 +85,7 @@ function SectionHeader({ section }: { section: Section; }) {
; } -export default function ChangeDecorationModal(props: any) { +function ChangeDecorationModal(props: ModalProps) { // undefined = not trying, null = none, Decoration = selected const [tryingDecoration, setTryingDecoration] = useState(undefined); const isTryingDecoration = typeof tryingDecoration !== "undefined"; @@ -116,6 +118,7 @@ export default function ChangeDecorationModal(props: any) { const data = [ { title: "Your Decorations", + subtitle: "You can delete your own decorations by right clicking on them.", sectionKey: "ownDecorations", items: ["none", ...ownDecorations, "create"] }, @@ -148,60 +151,62 @@ export default function ChangeDecorationModal(props: any) { className={cl("change-decoration-modal-content")} scrollbarType="none" > - { - if (typeof item === "string") { - switch (item) { - case "none": - return setTryingDecoration(null)} - />; - case "create": - return - {tooltipProps => + { + if (typeof item === "string") { + switch (item) { + case "none": + return setTryingDecoration(null)} + />; + case "create": + return + {tooltipProps => { }} + />} + ; + } + } else { + return + {tooltipProps => ( + { }} - />} - ; + className={cl("change-decoration-modal-decoration")} + onSelect={item.reviewed !== false ? () => setTryingDecoration(item) : () => { }} + isSelected={activeSelectedDecoration?.hash === item.hash} + decoration={item} + /> + )} + ; } - } else { - return - {tooltipProps => ( - setTryingDecoration(item) : () => { }} - isSelected={activeSelectedDecoration?.hash === item.hash} - decoration={item} - /> - )} - ; - } - }} - getItemKey={item => typeof item === "string" ? item : item.hash} - getSectionKey={section => section.sectionKey} - renderSectionHeader={section => } - sections={data} - /> -
- typeof item === "string" ? item : item.hash} + getSectionKey={section => section.sectionKey} + renderSectionHeader={section => } + sections={data} /> - {isActiveDecorationPreset && Part of the {activeDecorationPreset.name} Preset} - {typeof activeSelectedDecoration === "object" && - - {activeSelectedDecoration?.alt} - - } - {activeDecorationHasAuthor && Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}} -
+
+ + {isActiveDecorationPreset && Part of the {activeDecorationPreset.name} Preset} + {typeof activeSelectedDecoration === "object" && + + {activeSelectedDecoration?.alt} + + } + {activeDecorationHasAuthor && Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}} +
+
diff --git a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx index a5937b0dd..0dcf855ef 100644 --- a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx @@ -4,23 +4,23 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import ErrorBoundary from "@components/ErrorBoundary"; import { Link } from "@components/Link"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; -import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { findByPropsLazy, findComponentByCodeLazy } from "@webpack"; import { Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Text, TextInput, useEffect, useMemo, UserStore, useState } from "@webpack/common"; import { GUILD_ID, INVITE_KEY, RAW_SKU_ID } from "../../lib/constants"; import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore"; -import { cl, requireAvatarDecorationModal, requireCreateStickerModal } from "../"; +import { cl, DecorationModalStyles, requireAvatarDecorationModal, requireCreateStickerModal } from "../"; import { AvatarDecorationModalPreview } from "../components"; - -const DecorationModalStyles = findByPropsLazy("modalFooterShopButton"); - const FileUpload = findComponentByCodeLazy("fileUploadInput,"); +const { default: HelpMessage, HelpMessageTypes } = findByPropsLazy("HelpMessageTypes"); + function useObjectURL(object: Blob | MediaSource | null) { const [url, setUrl] = useState(null); @@ -39,7 +39,7 @@ function useObjectURL(object: Blob | MediaSource | null) { return url; } -export default function CreateDecorationModal(props) { +function CreateDecorationModal(props: ModalProps) { const [name, setName] = useState(""); const [file, setFile] = useState(null); const [submitting, setSubmitting] = useState(false); @@ -75,65 +75,69 @@ export default function CreateDecorationModal(props) { className={cl("create-decoration-modal-content")} scrollbarType="none" > -
-
- {error !== null && {error.message}} - - + + Make sure your decoration does not violate + the guidelines + before submitting it. + +
+
+ {error !== null && {error.message}} + + + + File should be APNG or PNG. + + + + + + This name will be used when referring to this decoration. + + +
+
+ - - File should be APNG or PNG. - - - - - - This name will be used when referring to this decoration. - - +
-
- -
-
- - Make sure your decoration does not violate - the guidelines - before creating your decoration. -
You can receive updates on your decoration's review by joining { - e.preventDefault(); - if (!GuildStore.getGuild(GUILD_ID)) { - const inviteAccepted = await openInviteModal(INVITE_KEY); - if (inviteAccepted) { + +
You can receive updates on your decoration's review by joining { + e.preventDefault(); + if (!GuildStore.getGuild(GUILD_ID)) { + const inviteAccepted = await openInviteModal(INVITE_KEY); + if (inviteAccepted) { + closeAllModals(); + FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + } + } else { closeAllModals(); FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); + NavigationRouter.transitionToGuild(GUILD_ID); } - } else { - closeAllModals(); - FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" }); - NavigationRouter.transitionToGuild(GUILD_ID); - } - }} - > - Decor's Discord server - . -
+ }} + > + Decor's Discord server + . +
+ + + + ; +} + +export const openGuidelinesModal = () => + requireAvatarDecorationModal().then(() => openModal(props => )); diff --git a/src/plugins/decor/ui/styles.css b/src/plugins/decor/ui/styles.css index ff10c82fa..9730efb7e 100644 --- a/src/plugins/decor/ui/styles.css +++ b/src/plugins/decor/ui/styles.css @@ -8,7 +8,7 @@ display: flex; border-radius: 5px 5px 0 0; padding: 0 16px; - gap: 4px + gap: 4px; } .vc-decor-change-decoration-modal-preview { @@ -72,7 +72,7 @@ .vc-decor-sectioned-grid-list-grid { display: flex; flex-wrap: wrap; - gap: 8px + gap: 8px; } .vc-decor-section-remove-margin { diff --git a/src/plugins/fixCodeblockGap/index.ts b/src/plugins/fixCodeblockGap/index.ts new file mode 100644 index 000000000..133409959 --- /dev/null +++ b/src/plugins/fixCodeblockGap/index.ts @@ -0,0 +1,35 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "FixCodeblockGap", + description: "Removes the gap between codeblocks and text below it", + authors: [Devs.Grzesiek11], + patches: [ + { + find: ".default.Messages.DELETED_ROLE_PLACEHOLDER", + replacement: { + match: String.raw`/^${"```"}(?:([a-z0-9_+\-.#]+?)\n)?\n*([^\n][^]*?)\n*${"```"}`, + replace: "$&\\n?", + }, + }, + ], +}); diff --git a/src/plugins/fixYoutubeEmbeds.desktop/README.md b/src/plugins/fixYoutubeEmbeds.desktop/README.md new file mode 100644 index 000000000..875de9e28 --- /dev/null +++ b/src/plugins/fixYoutubeEmbeds.desktop/README.md @@ -0,0 +1,5 @@ +# FixYoutubeEmbeds + +Bypasses youtube videos being blocked from display on Discord (for example by UMG) + +![](https://github.com/Vendicated/Vencord/assets/45497981/7a5fdcaa-217c-4c63-acae-f0d6af2f79be) diff --git a/src/plugins/fixYoutubeEmbeds.desktop/index.ts b/src/plugins/fixYoutubeEmbeds.desktop/index.ts new file mode 100644 index 000000000..55486d763 --- /dev/null +++ b/src/plugins/fixYoutubeEmbeds.desktop/index.ts @@ -0,0 +1,14 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "FixYoutubeEmbeds", + description: "Bypasses youtube videos being blocked from display on Discord (for example by UMG)", + authors: [Devs.coolelectronics] +}); diff --git a/src/plugins/fixYoutubeEmbeds.desktop/native.ts b/src/plugins/fixYoutubeEmbeds.desktop/native.ts new file mode 100644 index 000000000..5419d4ad8 --- /dev/null +++ b/src/plugins/fixYoutubeEmbeds.desktop/native.ts @@ -0,0 +1,27 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { app } from "electron"; +import { getSettings } from "main/ipcMain"; + +app.on("browser-window-created", (_, win) => { + win.webContents.on("frame-created", (_, { frame }) => { + frame.once("dom-ready", () => { + if (frame.url.startsWith("https://www.youtube.com/")) { + const settings = getSettings().plugins?.FixYoutubeEmbeds; + if (!settings?.enabled) return; + + frame.executeJavaScript(` + new MutationObserver(() => { + if( + document.querySelector('div.ytp-error-content-wrap-subreason a[href^="https://www.youtube.com/watch?v="]') + ) location.reload() + }).observe(document.body, { childList: true, subtree:true }); + `); + } + }); + }); +}); diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx index c80c4ce54..fcb0af712 100644 --- a/src/plugins/invisibleChat.desktop/index.tsx +++ b/src/plugins/invisibleChat.desktop/index.tsx @@ -16,13 +16,14 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { addButton, removeButton } from "@api/MessagePopover"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { getStegCloak } from "@utils/dependencies"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; +import { ChannelStore, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common"; import { Message } from "discord-types/general"; import { buildDecModal } from "./components/DecryptionModal"; @@ -64,54 +65,31 @@ function Indicator() { } -function ChatBarIcon(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - if (chatBoxProps.type.analyticsName !== "normal") return null; +const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { + if (!isMainChat) return null; return ( - - {({ onMouseEnter, onMouseLeave }) => ( - // size="" = Button.Sizes.NONE - /* - many themes set "> button" to display: none, as the gift button is - the only directly descending button (all the other elements are divs.) - Thus, wrap in a div here to avoid getting hidden by that. - flex is for some reason necessary as otherwise the button goes flying off - */ -
- -
- ) - } -
+ buildEncModal()} + + buttonProps={{ + "aria-haspopup": "dialog", + }} + > + + + + ); -} +}; const settings = definePluginSettings({ savedPasswords: { @@ -125,7 +103,7 @@ export default definePlugin({ name: "InvisibleChat", description: "Encrypt your Messages in a non-suspicious way!", authors: [Devs.SammCheese], - dependencies: ["MessagePopoverAPI"], + dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI"], patches: [ { // Indicator @@ -135,13 +113,6 @@ export default definePlugin({ replace: "try {$1 && $self.INV_REGEX.test($1.message.content) ? $1.content.push($self.indicator()) : null } catch {};$&" } }, - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, ], EMBED_API_URL: "https://embed.sammcheese.net", @@ -151,10 +122,7 @@ export default definePlugin({ ), settings, async start() { - const { default: StegCloak } = await getStegCloak(); - steggo = new StegCloak(true, false); - - addButton("invDecrypt", message => { + addButton("InvisibleChat", message => { return this.INV_REGEX.test(message?.content) ? { label: "Decrypt Message", @@ -170,10 +138,16 @@ export default definePlugin({ } : null; }); + + addChatBarButton("InvisibleChat", ChatBarIcon); + + const { default: StegCloak } = await getStegCloak(); + steggo = new StegCloak(true, false); }, stop() { - removeButton("invDecrypt"); + removeButton("InvisibleChat"); + removeButton("InvisibleChat"); }, // Gets the Embed of a Link @@ -216,7 +190,6 @@ export default definePlugin({ }); }, - chatBarIcon: ErrorBoundary.wrap(ChatBarIcon, { noop: true }), popOverIcon: () => , indicator: ErrorBoundary.wrap(Indicator, { noop: true }) }); diff --git a/src/plugins/moreUserTags/index.tsx b/src/plugins/moreUserTags/index.tsx index 9921bca8e..d1ad941b0 100644 --- a/src/plugins/moreUserTags/index.tsx +++ b/src/plugins/moreUserTags/index.tsx @@ -198,7 +198,7 @@ export default definePlugin({ replacement: [ // make the tag show the right text { - match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=(\i\.\i\.Messages)\.BOT_TAG_BOT/, + match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.BOT_TAG_BOT/, replace: (_, origSwitch, variant, tags, displayedText, strings) => `${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}` }, diff --git a/src/plugins/permissionsViewer/index.tsx b/src/plugins/permissionsViewer/index.tsx index 985b23842..9e0131e64 100644 --- a/src/plugins/permissionsViewer/index.tsx +++ b/src/plugins/permissionsViewer/index.tsx @@ -126,7 +126,9 @@ function MenuItem(guildId: string, id?: string, type?: MenuItemParentType) { function makeContextMenuPatch(childId: string | string[], type?: MenuItemParentType): NavContextMenuPatchCallback { return (children, props) => () => { - if (!props || (type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild)) return children; + if (!props) return; + if ((type === MenuItemParentType.User && !props.user) || (type === MenuItemParentType.Guild && !props.guild) || (type === MenuItemParentType.Channel && (!props.channel || !props.guild))) + return children; const group = findGroupChildrenByChildId(childId, children); diff --git a/src/plugins/pictureInPicture/index.tsx b/src/plugins/pictureInPicture/index.tsx index ba4aa8387..ca766affc 100644 --- a/src/plugins/pictureInPicture/index.tsx +++ b/src/plugins/pictureInPicture/index.tsx @@ -24,7 +24,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "PictureInPicture", description: "Adds picture in picture to videos (next to the Download button)", - authors: [Devs.Lumap], + authors: [Devs.Nobody], settings, patches: [ { diff --git a/src/plugins/previewMessage/index.tsx b/src/plugins/previewMessage/index.tsx index f2634ae6b..fe6b227a5 100644 --- a/src/plugins/previewMessage/index.tsx +++ b/src/plugins/previewMessage/index.tsx @@ -16,22 +16,14 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { generateId, sendBotMessage } from "@api/Commands"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { StartAt } from "@utils/types"; import { findByPropsLazy } from "@webpack"; -import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; +import { DraftStore, DraftType, SelectedChannelStore, UserStore, useStateFromStores } from "@webpack/common"; import { MessageAttachment } from "discord-types/general"; -interface Props { - type: { - analyticsName: string; - isEmpty: boolean; - attachments: boolean; - }; -} - const UploadStore = findByPropsLazy("getUploads"); const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage); @@ -81,13 +73,11 @@ const getAttachments = async (channelId: string) => ); -export function PreviewButton(chatBoxProps: Props) { - const { isEmpty, attachments } = chatBoxProps.type; - +const PreviewButton: ChatBarButton = ({ isMainChat, isEmpty, type: { attachments } }) => { const channelId = SelectedChannelStore.getChannelId(); const draft = useStateFromStores([DraftStore], () => getDraft(channelId)); - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat) return null; const hasAttachments = attachments && UploadStore.getUploads(channelId, DraftType.ChannelMessage).length > 0; const hasContent = !isEmpty && draft?.length > 0; @@ -95,47 +85,47 @@ export function PreviewButton(chatBoxProps: Props) { if (!hasContent && !hasAttachments) return null; return ( - - {tooltipProps => ( - - )} - + + sendBotMessage( + channelId, + { + content: getDraft(channelId), + author: UserStore.getCurrentUser(), + attachments: hasAttachments ? await getAttachments(channelId) : undefined, + } + )} + buttonProps={{ + style: { + translate: "0 2px" + } + }} + > + + + + ); -} +}; export default definePlugin({ name: "PreviewMessage", description: "Lets you preview your message before sending it.", authors: [Devs.Aria], - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], + dependencies: ["ChatInputButtonAPI"], + // start early to ensure we're the first plugin to add our button + // This makes the popping in less awkward + startAt: StartAt.Init, - chatBarIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }), + start: () => addChatBarButton("previewMessage", PreviewButton), + stop: () => removeChatBarButton("previewMessage"), }); diff --git a/src/plugins/readAllNotificationsButton/index.tsx b/src/plugins/readAllNotificationsButton/index.tsx index b5b0b5f82..0e08edb6d 100644 --- a/src/plugins/readAllNotificationsButton/index.tsx +++ b/src/plugins/readAllNotificationsButton/index.tsx @@ -25,16 +25,17 @@ function onClick() { const channels: Array = []; Object.values(GuildStore.getGuilds()).forEach(guild => { - GuildChannelStore.getChannels(guild.id).SELECTABLE.forEach((c: { channel: { id: string; }; }) => { - if (!ReadStateStore.hasUnread(c.channel.id)) return; + GuildChannelStore.getChannels(guild.id).SELECTABLE + .concat(GuildChannelStore.getChannels(guild.id).VOCAL) + .forEach((c: { channel: { id: string; }; }) => { + if (!ReadStateStore.hasUnread(c.channel.id)) return; - channels.push({ - channelId: c.channel.id, - // messageId: c.channel?.lastMessageId, - messageId: ReadStateStore.lastMessageId(c.channel.id), - readStateType: 0 + channels.push({ + channelId: c.channel.id, + messageId: ReadStateStore.lastMessageId(c.channel.id), + readStateType: 0 + }); }); - }); }); FluxDispatcher.dispatch({ diff --git a/src/plugins/reverseImageSearch/index.tsx b/src/plugins/reverseImageSearch/index.tsx index 811e7ff08..6c5f3e729 100644 --- a/src/plugins/reverseImageSearch/index.tsx +++ b/src/plugins/reverseImageSearch/index.tsx @@ -36,62 +36,68 @@ function search(src: string, engine: string) { open(engine + encodeURIComponent(src), "_blank"); } -const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { - if (!props) return; - const { reverseImageSearchType, itemHref, itemSrc } = props; +function makeSearchItem(src: string) { + return ( + + {Object.keys(Engines).map((engine, i) => { + const key = "search-image-" + engine; + return ( + + = 3 // Do not round Google, Yandex & SauceNAO + ? "50%" + : void 0 + }} + aria-hidden="true" + height={16} + width={16} + src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")} + /> + {engine} + + } + action={() => search(src, Engines[engine])} + /> + ); + })} + + + All + + } + action={() => Object.values(Engines).forEach(e => search(src, e))} + /> + + ); +} - if (!reverseImageSearchType || reverseImageSearchType !== "img") return; +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { + if (props?.reverseImageSearchType !== "img") return; - const src = itemHref ?? itemSrc; + const src = props.itemHref ?? props.itemSrc; const group = findGroupChildrenByChildId("copy-link", children); - if (group) { - group.push(( - - {Object.keys(Engines).map((engine, i) => { - const key = "search-image-" + engine; - return ( - - = 3 // Do not round Google, Yandex & SauceNAO - ? "50%" - : void 0 - }} - aria-hidden="true" - height={16} - width={16} - src={new URL("/favicon.ico", Engines[engine]).toString().replace("lens.", "")} - /> - {engine} - - } - action={() => search(src, Engines[engine])} - /> - ); - })} - - - All - - } - action={() => Object.values(Engines).forEach(e => search(src, e))} - /> - - )); - } + group?.push(makeSearchItem(src)); +}; + +const imageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => { + if (!props?.src) return; + + const group = findGroupChildrenByChildId("copy-native-link", children) ?? children; + group.push(makeSearchItem(props.src)); }; export default definePlugin({ @@ -111,10 +117,12 @@ export default definePlugin({ ], start() { - addContextMenuPatch("message", imageContextMenuPatch); + addContextMenuPatch("message", messageContextMenuPatch); + addContextMenuPatch("image-context", imageContextMenuPatch); }, stop() { - removeContextMenuPatch("message", imageContextMenuPatch); + removeContextMenuPatch("message", messageContextMenuPatch); + removeContextMenuPatch("image-context", imageContextMenuPatch); } }); diff --git a/src/plugins/reviewDB/auth.tsx b/src/plugins/reviewDB/auth.tsx new file mode 100644 index 000000000..136001b2d --- /dev/null +++ b/src/plugins/reviewDB/auth.tsx @@ -0,0 +1,81 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2023 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { DataStore } from "@api/index"; +import { Logger } from "@utils/Logger"; +import { openModal } from "@utils/modal"; +import { findByPropsLazy } from "@webpack"; +import { showToast, Toasts, UserStore } from "@webpack/common"; + +import { ReviewDBAuth } from "./entities"; + +const DATA_STORE_KEY = "rdb-auth"; + +const { OAuth2AuthorizeModal } = findByPropsLazy("OAuth2AuthorizeModal"); + +export let Auth: ReviewDBAuth = {}; + +export async function initAuth() { + Auth = await getAuth() ?? {}; +} + +export async function getAuth(): Promise { + const auth = await DataStore.get(DATA_STORE_KEY); + return auth?.[UserStore.getCurrentUser()?.id]; +} + +export async function getToken() { + const auth = await getAuth(); + return auth?.token; +} + +export async function updateAuth(newAuth: ReviewDBAuth) { + return DataStore.update(DATA_STORE_KEY, auth => { + auth ??= {}; + Auth = auth[UserStore.getCurrentUser().id] ??= {}; + + if (newAuth.token) Auth.token = newAuth.token; + if (newAuth.user) Auth.user = newAuth.user; + + return auth; + }); +} + +export function authorize(callback?: any) { + openModal(props => + { + try { + const url = new URL(response.location); + url.searchParams.append("clientMod", "vencord"); + const res = await fetch(url, { + headers: new Headers({ Accept: "application/json" }) + }); + + if (!res.ok) { + const { message } = await res.json(); + showToast(message || "An error occured while authorizing", Toasts.Type.FAILURE); + return; + } + + const { token } = await res.json(); + updateAuth({ token }); + showToast("Successfully logged in!", Toasts.Type.SUCCESS); + callback?.(); + } catch (e) { + new Logger("ReviewDB").error("Failed to authorize", e); + } + }} + /> + ); +} diff --git a/src/plugins/reviewDB/components/BlockedUserModal.tsx b/src/plugins/reviewDB/components/BlockedUserModal.tsx new file mode 100644 index 000000000..43c81eb52 --- /dev/null +++ b/src/plugins/reviewDB/components/BlockedUserModal.tsx @@ -0,0 +1,99 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Logger } from "@utils/Logger"; +import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal"; +import { useAwaiter } from "@utils/react"; +import { Forms, Tooltip, useState } from "@webpack/common"; + +import { Auth } from "../auth"; +import { ReviewDBUser } from "../entities"; +import { fetchBlocks, unblockUser } from "../reviewDbApi"; +import { cl } from "../utils"; + +function UnblockButton(props: { onClick?(): void; }) { + return ( + + {tooltipProps => ( +
+ + + +
+ )} +
+ ); +} + +function BlockedUser({ user, isBusy, setIsBusy }: { user: ReviewDBUser; isBusy: boolean; setIsBusy(v: boolean): void; }) { + const [gone, setGone] = useState(false); + if (gone) return null; + + return ( +
+ + {user.username} + { + setIsBusy(true); + try { + await unblockUser(user.discordID); + setGone(true); + } finally { + setIsBusy(false); + } + }} + /> +
+ ); +} + +function Modal() { + const [isBusy, setIsBusy] = useState(false); + const [blocks, error, pending] = useAwaiter(fetchBlocks, { + onError: e => new Logger("ReviewDB").error("Failed to fetch blocks", e), + fallbackValue: [], + }); + + if (pending) + return null; + if (error) + return Failed to fetch blocks: ${String(error)}; + if (!blocks.length) + return No blocked users.; + + return ( + <> + {blocks.map(b => ( + + ))} + + ); +} + +export function openBlockModal() { + openModal(modalProps => ( + + + Blocked Users + + + + {Auth.token ? : You are not logged into ReviewDB!} + + + )); +} diff --git a/src/plugins/reviewDB/components/MessageButton.tsx b/src/plugins/reviewDB/components/MessageButton.tsx new file mode 100644 index 000000000..9b0b4be1a --- /dev/null +++ b/src/plugins/reviewDB/components/MessageButton.tsx @@ -0,0 +1,85 @@ +/* + * 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 { DeleteIcon } from "@components/Icons"; +import { classes } from "@utils/misc"; +import { findByPropsLazy } from "@webpack"; +import { Tooltip } from "@webpack/common"; + +const iconClasses = findByPropsLazy("button", "wrapper", "disabled", "separator"); + +export function DeleteButton({ onClick }: { onClick(): void; }) { + return ( + + {props => ( +
+ +
+ )} +
+ ); +} + +export function ReportButton({ onClick }: { onClick(): void; }) { + return ( + + {props => ( +
+ + + +
+ )} +
+ ); +} + +export function BlockButton({ onClick, isBlocked }: { onClick(): void; isBlocked: boolean; }) { + return ( + + {props => ( +
+ + {isBlocked + ? + : + } + +
+ )} +
+ ); +} diff --git a/src/plugins/reviewDB/components/ReviewBadge.tsx b/src/plugins/reviewDB/components/ReviewBadge.tsx new file mode 100644 index 000000000..665b9bb09 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewBadge.tsx @@ -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 { MaskedLink, React, Tooltip } from "@webpack/common"; +import { HTMLAttributes } from "react"; + +import { Badge } from "../entities"; +import { cl } from "../utils"; + +export default function ReviewBadge(badge: Badge & { onClick?(): void; }) { + const Wrapper = badge.redirectURL + ? MaskedLink + : (props: HTMLAttributes) => ( + {props.children} + ); + + return ( + + {({ onMouseEnter, onMouseLeave }) => ( + + {badge.description} + + )} + + ); +} diff --git a/src/plugins/reviewDB/components/ReviewComponent.tsx b/src/plugins/reviewDB/components/ReviewComponent.tsx new file mode 100644 index 000000000..977745a25 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewComponent.tsx @@ -0,0 +1,191 @@ +/* + * 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 { openUserProfile } from "@utils/discord"; +import { classes } from "@utils/misc"; +import { LazyComponent } from "@utils/react"; +import { filters, findBulk } from "@webpack"; +import { Alerts, moment, Parser, Timestamp, useState } from "@webpack/common"; + +import { Auth, getToken } from "../auth"; +import { Review, ReviewType } from "../entities"; +import { blockUser, deleteReview, reportReview, unblockUser } from "../reviewDbApi"; +import { settings } from "../settings"; +import { canBlockReviewAuthor, canDeleteReview, canReportReview, cl, showToast } from "../utils"; +import { openBlockModal } from "./BlockedUserModal"; +import { BlockButton, DeleteButton, ReportButton } from "./MessageButton"; +import ReviewBadge from "./ReviewBadge"; + +export default LazyComponent(() => { + // this is terrible, blame mantika + const p = filters.byProps; + const [ + { cozyMessage, buttons, message, buttonsInner, groupStart }, + { container, isHeader }, + { avatar, clickable, username, wrapper, cozy }, + buttonClasses, + botTag + ] = findBulk( + p("cozyMessage"), + p("container", "isHeader"), + p("avatar", "zalgo"), + p("button", "wrapper", "selected"), + p("botTag", "botTagRegular") + ); + + const dateFormat = new Intl.DateTimeFormat(); + + return function ReviewComponent({ review, refetch, profileId }: { review: Review; refetch(): void; profileId: string; }) { + const [showAll, setShowAll] = useState(false); + + function openModal() { + openUserProfile(review.sender.discordID); + } + + function delReview() { + Alerts.show({ + title: "Are you sure?", + body: "Do you really want to delete this review?", + confirmText: "Delete", + cancelText: "Nevermind", + onConfirm: async () => { + if (!(await getToken())) { + return showToast("You must be logged in to delete reviews."); + } else { + deleteReview(review.id).then(res => { + if (res) { + refetch(); + } + }); + } + } + }); + } + + function reportRev() { + Alerts.show({ + title: "Are you sure?", + body: "Do you really you want to report this review?", + confirmText: "Report", + cancelText: "Nevermind", + // confirmColor: "red", this just adds a class name and breaks the submit button guh + onConfirm: async () => { + if (!(await getToken())) { + return showToast("You must be logged in to report reviews."); + } else { + reportReview(review.id); + } + } + }); + } + + const isAuthorBlocked = Auth?.user?.blockedUsers?.includes(review.sender.discordID) ?? false; + + function blockReviewSender() { + if (isAuthorBlocked) + return unblockUser(review.sender.discordID); + + Alerts.show({ + title: "Are you sure?", + body: "Do you really you want to block this user? They will be unable to leave further reviews on your profile. You can unblock users in the plugin settings.", + confirmText: "Block", + cancelText: "Nevermind", + // confirmColor: "red", this just adds a class name and breaks the submit button guh + onConfirm: async () => { + if (!(await getToken())) { + return showToast("You must be logged in to block users."); + } else { + blockUser(review.sender.discordID); + } + } + }); + } + + return ( +
+ + +
+ openModal()} + > + {review.sender.username} + + + {review.type === ReviewType.System && ( + + + System + + + )} +
+ {isAuthorBlocked && ( + openBlockModal()} + /> + )} + {review.sender.badges.map(badge => )} + + { + !settings.store.hideTimestamps && review.type !== ReviewType.System && ( + + {dateFormat.format(review.timestamp * 1000)} + ) + } + +
+ {(review.comment.length > 200 && !showAll) + ? [Parser.parseGuildEventDescription(review.comment.substring(0, 200)), "...",
, ( setShowAll(true)}>Read more)] + : Parser.parseGuildEventDescription(review.comment)} +
+ + {review.id !== 0 && ( +
+
+ {canReportReview(review) && } + {canBlockReviewAuthor(profileId, review) && } + {canDeleteReview(profileId, review) && } +
+
+ )} +
+ ); + }; +}); diff --git a/src/plugins/reviewDB/components/ReviewModal.tsx b/src/plugins/reviewDB/components/ReviewModal.tsx new file mode 100644 index 000000000..9669a2b32 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewModal.tsx @@ -0,0 +1,105 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; +import { useForceUpdater } from "@utils/react"; +import { Paginator, Text, useRef, useState } from "@webpack/common"; + +import { Auth } from "../auth"; +import { Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; +import { cl } from "../utils"; +import ReviewComponent from "./ReviewComponent"; +import ReviewsView, { ReviewsInputComponent } from "./ReviewsView"; + +function Modal({ modalProps, discordId, name }: { modalProps: any; discordId: string; name: string; }) { + const [data, setData] = useState(); + const [signal, refetch] = useForceUpdater(true); + const [page, setPage] = useState(1); + + const ref = useRef(null); + + const reviewCount = data?.reviewCount; + const ownReview = data?.reviews.find(r => r.sender.discordID === Auth.user?.discordID); + + return ( + + + + + {name}'s Reviews + {!!reviewCount && ({reviewCount} Reviews)} + + + + + +
+ ref.current?.scrollTo({ top: 0, behavior: "smooth" })} + hideOwnReview + /> +
+
+ + +
+ {ownReview && ( + + )} + + + {!!reviewCount && ( + + )} +
+
+
+
+ ); +} + +export function openReviewsModal(discordId: string, name: string) { + openModal(props => ( + + )); +} diff --git a/src/plugins/reviewDB/components/ReviewsView.tsx b/src/plugins/reviewDB/components/ReviewsView.tsx new file mode 100644 index 000000000..64cea1815 --- /dev/null +++ b/src/plugins/reviewDB/components/ReviewsView.tsx @@ -0,0 +1,197 @@ +/* + * 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 { LazyComponent, useAwaiter, useForceUpdater } from "@utils/react"; +import { find, findByPropsLazy } from "@webpack"; +import { Forms, React, RelationshipStore, useRef, UserStore } from "@webpack/common"; + +import { Auth, authorize } from "../auth"; +import { Review } from "../entities"; +import { addReview, getReviews, Response, REVIEWS_PER_PAGE } from "../reviewDbApi"; +import { settings } from "../settings"; +import { cl, showToast } from "../utils"; +import ReviewComponent from "./ReviewComponent"; + + +const { Editor, Transforms } = findByPropsLazy("Editor", "Transforms"); +const { ChatInputTypes } = findByPropsLazy("ChatInputTypes"); + +const InputComponent = LazyComponent(() => find(m => m.default?.type?.render?.toString().includes("default.CHANNEL_TEXT_AREA")).default); + +interface UserProps { + discordId: string; + name: string; +} + +interface Props extends UserProps { + onFetchReviews(data: Response): void; + refetchSignal?: unknown; + showInput?: boolean; + page?: number; + scrollToTop?(): void; + hideOwnReview?: boolean; +} + +export default function ReviewsView({ + discordId, + name, + onFetchReviews, + refetchSignal, + scrollToTop, + page = 1, + showInput = false, + hideOwnReview = false, +}: Props) { + const [signal, refetch] = useForceUpdater(true); + + const [reviewData] = useAwaiter(() => getReviews(discordId, (page - 1) * REVIEWS_PER_PAGE), { + fallbackValue: null, + deps: [refetchSignal, signal, page], + onSuccess: data => { + if (settings.store.hideBlockedUsers) + data!.reviews = data!.reviews?.filter(r => !RelationshipStore.isBlocked(r.sender.discordID)); + + scrollToTop?.(); + onFetchReviews(data!); + } + }); + + if (!reviewData) return null; + + return ( + <> + + + {showInput && ( + r.sender.discordID === UserStore.getCurrentUser().id)} + /> + )} + + ); +} + +function ReviewList({ refetch, reviews, hideOwnReview, profileId }: { refetch(): void; reviews: Review[]; hideOwnReview: boolean; profileId: string; }) { + const myId = UserStore.getCurrentUser().id; + + return ( +
+ {reviews?.map(review => + (review.sender.discordID !== myId || !hideOwnReview) && + + )} + + {reviews?.length === 0 && ( + + Looks like nobody reviewed this user yet. You could be the first! + + )} +
+ ); +} + + +export function ReviewsInputComponent({ discordId, isAuthor, refetch, name }: { discordId: string, name: string; isAuthor: boolean; refetch(): void; }) { + const { token } = Auth; + const editorRef = useRef(null); + const inputType = ChatInputTypes.FORM; + inputType.disableAutoFocus = true; + + const channel = { + flags_: 256, + guild_id_: null, + id: "0", + getGuildId: () => null, + isPrivate: () => true, + isActiveThread: () => false, + isArchivedLockedThread: () => false, + isDM: () => true, + roles: { "0": { permissions: 0n } }, + getRecipientId: () => "0", + hasFlag: () => false, + }; + + return ( + <> +
{ + if (!token) { + showToast("Opening authorization window..."); + authorize(); + } + }}> + editorRef.current = ref} + textValue="" + onSubmit={ + async res => { + const response = await addReview({ + userid: discordId, + comment: res.value, + }); + + if (response) { + refetch(); + + const slateEditor = editorRef.current.ref.current.getSlateEditor(); + + // clear editor + Transforms.delete(slateEditor, { + at: { + anchor: Editor.start(slateEditor, []), + focus: Editor.end(slateEditor, []), + } + }); + } + + // even tho we need to return this, it doesnt do anything + return { + shouldClear: false, + shouldRefocus: true, + }; + } + } + /> +
+ + + ); +} diff --git a/src/plugins/reviewDB/entities.ts b/src/plugins/reviewDB/entities.ts new file mode 100644 index 000000000..a871d90f2 --- /dev/null +++ b/src/plugins/reviewDB/entities.ts @@ -0,0 +1,100 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +export const enum UserType { + Banned = -1, + Normal = 0, + Admin = 1 +} + +export const enum ReviewType { + User = 0, + Server = 1, + Support = 2, + System = 3 +} + +export const enum NotificationType { + Info = 0, + Ban = 1, + Unban = 2, + Warning = 3 +} + +export interface ReviewDBAuth { + token?: string; + user?: ReviewDBCurrentUser; +} + +export interface Badge { + name: string; + description: string; + icon: string; + redirectURL?: string; + type: number; +} + +export interface BanInfo { + id: string; + discordID: string; + reviewID: number; + reviewContent: string; + banEndDate: number; +} + +export interface Notification { + id: number; + title: string; + content: string; + type: NotificationType; +} + +export interface ReviewDBUser { + ID: number; + discordID: string; + username: string; + type: UserType; + profilePhoto: string; + badges: any[]; +} + +export interface ReviewDBCurrentUser extends ReviewDBUser { + warningCount: number; + clientMod: string; + banInfo: BanInfo | null; + notification: Notification | null; + lastReviewID: number; + blockedUsers?: string[]; +} + +export interface ReviewAuthor { + id: number, + discordID: string, + username: string, + profilePhoto: string, + badges: Badge[]; +} + +export interface Review { + comment: string, + id: number, + star: number, + sender: ReviewAuthor, + timestamp: number; + type?: ReviewType; +} diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx new file mode 100644 index 000000000..50bb62184 --- /dev/null +++ b/src/plugins/reviewDB/index.tsx @@ -0,0 +1,158 @@ +/* + * 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 "./style.css"; + +import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import ErrorBoundary from "@components/ErrorBoundary"; +import ExpandableHeader from "@components/ExpandableHeader"; +import { OpenExternalIcon } from "@components/Icons"; +import { Devs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import definePlugin from "@utils/types"; +import { Alerts, Menu, Parser, useState } from "@webpack/common"; +import { Guild, User } from "discord-types/general"; + +import { Auth, initAuth, updateAuth } from "./auth"; +import { openReviewsModal } from "./components/ReviewModal"; +import ReviewsView from "./components/ReviewsView"; +import { NotificationType } from "./entities"; +import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; +import { settings } from "./settings"; +import { showToast } from "./utils"; + +const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => () => { + children.push( + openReviewsModal(props.guild.id, props.guild.name)} + /> + ); +}; + +export default definePlugin({ + name: "ReviewDB", + description: "Review other users (Adds a new settings to profiles)", + authors: [Devs.mantikafasi, Devs.Ven], + + settings, + + patches: [ + { + find: "showBorder:null", + replacement: { + match: /user:(\i),setNote:\i,canDM.+?\}\)/, + replace: "$&,$self.getReviewsComponent($1)" + } + } + ], + + flux: { + CONNECTION_OPEN: initAuth, + }, + + async start() { + addContextMenuPatch("guild-header-popout", guildPopoutPatch); + + const s = settings.store; + const { lastReviewId, notifyReviews } = s; + + const legacy = s as any as { token?: string; }; + if (legacy.token) { + await updateAuth({ token: legacy.token }); + legacy.token = undefined; + new Logger("ReviewDB").info("Migrated legacy settings"); + } + + await initAuth(); + + setTimeout(async () => { + if (!Auth.token) return; + + const user = await getCurrentUserInfo(Auth.token); + updateAuth({ user }); + + if (notifyReviews) { + if (lastReviewId && lastReviewId < user.lastReviewID) { + s.lastReviewId = user.lastReviewID; + if (user.lastReviewID !== 0) + showToast("You have new reviews on your profile!"); + } + } + + if (user.notification) { + const props = user.notification.type === NotificationType.Ban ? { + cancelText: "Appeal", + confirmText: "Ok", + onCancel: async () => + VencordNative.native.openExternal( + "https://reviewdb.mantikafasi.dev/api/redirect?" + + new URLSearchParams({ + token: Auth.token!, + page: "dashboard/appeal" + }) + ) + } : {}; + + Alerts.show({ + title: user.notification.title, + body: ( + Parser.parse( + user.notification.content, + false + ) + ), + ...props + }); + + readNotification(user.notification.id); + } + }, 4000); + }, + + stop() { + removeContextMenuPatch("guild-header-popout", guildPopoutPatch); + }, + + getReviewsComponent: ErrorBoundary.wrap((user: User) => { + const [reviewCount, setReviewCount] = useState(); + + return ( + openReviewsModal(user.id, user.username)} + moreTooltipText={ + reviewCount && reviewCount > 50 + ? `View all ${reviewCount} reviews` + : "Open Review Modal" + } + onDropDownClick={state => settings.store.reviewsDropdownState = !state} + defaultState={settings.store.reviewsDropdownState} + > + setReviewCount(r.reviewCount)} + showInput + /> + + ); + }, { message: "Failed to render Reviews" }) +}); diff --git a/src/plugins/reviewDB/reviewDbApi.ts b/src/plugins/reviewDB/reviewDbApi.ts new file mode 100644 index 000000000..ec6d9ff3b --- /dev/null +++ b/src/plugins/reviewDB/reviewDbApi.ts @@ -0,0 +1,202 @@ +/* + * 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 { Toasts } from "@webpack/common"; + +import { Auth, authorize, getToken, updateAuth } from "./auth"; +import { Review, ReviewDBCurrentUser, ReviewDBUser, ReviewType } from "./entities"; +import { settings } from "./settings"; +import { showToast } from "./utils"; + +const API_URL = "https://manti.vendicated.dev/api/reviewdb"; + +export const REVIEWS_PER_PAGE = 50; + +export interface Response { + message: string; + reviews: Review[]; + updated: boolean; + hasNextPage: boolean; + reviewCount: number; +} + +const WarningFlag = 0b00000010; + +async function rdbRequest(path: string, options: RequestInit = {}) { + return fetch(API_URL + path, { + ...options, + headers: { + ...options.headers, + Authorization: await getToken() || "", + } + }); +} + +export async function getReviews(id: string, offset = 0): Promise { + let flags = 0; + if (!settings.store.showWarning) flags |= WarningFlag; + + const params = new URLSearchParams({ + flags: String(flags), + offset: String(offset) + }); + const req = await fetch(`${API_URL}/users/${id}/reviews?${params}`); + + const res = (req.ok) + ? await req.json() as Response + : { + message: req.status === 429 ? "You are sending requests too fast. Wait a few seconds and try again." : "An Error occured while fetching reviews. Please try again later.", + reviews: [], + updated: false, + hasNextPage: false, + reviewCount: 0 + }; + + if (!req.ok) { + showToast(res.message, Toasts.Type.FAILURE); + return { + ...res, + reviews: [ + { + id: 0, + comment: res.message, + star: 0, + timestamp: 0, + type: ReviewType.System, + sender: { + id: 0, + username: "ReviewDB", + profilePhoto: "https://cdn.discordapp.com/avatars/1134864775000629298/3f87ad315b32ee464d84f1270c8d1b37.png?size=256&format=webp&quality=lossless", + discordID: "1134864775000629298", + badges: [] + } + } + ] + }; + } + + return res; +} + +export async function addReview(review: any): Promise { + + const token = await getToken(); + if (!token) { + showToast("Please authorize to add a review."); + authorize(); + return null; + } + + return await rdbRequest(`/users/${review.userid}/reviews`, { + method: "PUT", + body: JSON.stringify(review), + headers: { + "Content-Type": "application/json", + } + }).then(async r => { + const data = await r.json() as Response; + showToast(data.message); + return r.ok ? data : null; + }); +} + +export async function deleteReview(id: number): Promise { + return await rdbRequest(`/users/${id}/reviews`, { + method: "DELETE", + headers: new Headers({ + "Content-Type": "application/json", + Accept: "application/json", + }), + body: JSON.stringify({ + reviewid: id + }) + }).then(async r => { + const data = await r.json() as Response; + showToast(data.message); + return r.ok ? data : null; + }); +} + +export async function reportReview(id: number) { + const res = await rdbRequest("/reports", { + method: "PUT", + headers: new Headers({ + "Content-Type": "application/json", + Accept: "application/json", + }), + body: JSON.stringify({ + reviewid: id, + }) + }).then(r => r.json()) as Response; + + showToast(res.message); +} + +async function patchBlock(action: "block" | "unblock", userId: string) { + const res = await rdbRequest("/blocks", { + method: "PATCH", + headers: new Headers({ + "Content-Type": "application/json", + Accept: "application/json", + }), + body: JSON.stringify({ + action: action, + discordId: userId + }) + }); + + if (!res.ok) { + showToast(`Failed to ${action} user`, Toasts.Type.FAILURE); + } else { + showToast(`Successfully ${action}ed user`, Toasts.Type.SUCCESS); + + if (Auth?.user?.blockedUsers) { + const newBlockedUsers = action === "block" + ? [...Auth.user.blockedUsers, userId] + : Auth.user.blockedUsers.filter(id => id !== userId); + updateAuth({ user: { ...Auth.user, blockedUsers: newBlockedUsers } }); + } + } +} + +export const blockUser = (userId: string) => patchBlock("block", userId); +export const unblockUser = (userId: string) => patchBlock("unblock", userId); + +export async function fetchBlocks(): Promise { + const res = await rdbRequest("/blocks", { + method: "GET", + headers: new Headers({ + Accept: "application/json", + }) + }); + + if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`); + return res.json(); +} + +export function getCurrentUserInfo(token: string): Promise { + return rdbRequest("/users", { + method: "POST", + }).then(r => r.json()); +} + +export async function readNotification(id: number) { + return rdbRequest(`/notifications?id=${id}`, { + method: "PATCH" + }); +} diff --git a/src/plugins/reviewDB/settings.tsx b/src/plugins/reviewDB/settings.tsx new file mode 100644 index 000000000..79cf4174e --- /dev/null +++ b/src/plugins/reviewDB/settings.tsx @@ -0,0 +1,96 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { definePluginSettings } from "@api/Settings"; +import { OptionType } from "@utils/types"; +import { Button } from "@webpack/common"; + +import { authorize, getToken } from "./auth"; +import { openBlockModal } from "./components/BlockedUserModal"; +import { cl } from "./utils"; + +export const settings = definePluginSettings({ + authorize: { + type: OptionType.COMPONENT, + description: "Authorize with ReviewDB", + component: () => ( + + ) + }, + notifyReviews: { + type: OptionType.BOOLEAN, + description: "Notify about new reviews on startup", + default: true, + }, + showWarning: { + type: OptionType.BOOLEAN, + description: "Display warning to be respectful at the top of the reviews list", + default: true, + }, + hideTimestamps: { + type: OptionType.BOOLEAN, + description: "Hide timestamps on reviews", + default: false, + }, + hideBlockedUsers: { + type: OptionType.BOOLEAN, + description: "Hide reviews from blocked users", + default: true, + }, + buttons: { + type: OptionType.COMPONENT, + description: "ReviewDB buttons", + component: () => ( +
+ + + + + + + + +
+ ) + } +}).withPrivateSettings<{ + lastReviewId?: number; + reviewsDropdownState?: boolean; +}>(); diff --git a/src/plugins/reviewDB/style.css b/src/plugins/reviewDB/style.css new file mode 100644 index 000000000..190b8f620 --- /dev/null +++ b/src/plugins/reviewDB/style.css @@ -0,0 +1,140 @@ +[class|="section"]:not([class|="lastSection"]) + .vc-rdb-view { + margin-top: 12px; +} + +.vc-rdb-badge { + vertical-align: middle; + margin-left: 4px; +} + +.vc-rdb-input { + margin-top: 6px; + margin-bottom: 12px; + resize: none; + overflow: hidden; + background: transparent; + border: 1px solid var(--profile-message-input-border-color); +} + +.vc-rdb-modal-footer > div { + width: 100%; + margin: 6px 16px; +} + +/* When input becomes disabled(while sending review), input adds unneccesary padding to left, this prevents it */ +.vc-rdb-input > div > div { + padding-left: 0 !important; +} + +.vc-rdb-placeholder { + margin-bottom: 4px; + font-weight: bold; + font-style: italic; + color: var(--text-muted); +} + +.vc-rdb-input * { + font-size: 14px; +} + +.vc-rdb-modal-footer { + padding: 0; +} + +.vc-rdb-modal-footer .vc-rdb-input { + margin-bottom: 0; + background: var(--input-background); +} + +.vc-rdb-modal-footer [class|="pageControlContainer"] { + margin-top: 0; +} + +.vc-rdb-modal-header { + flex-grow: 1; +} + +.vc-rdb-modal-reviews { + margin-top: 16px; +} + +.vc-rdb-review { + padding-top: 8px !important; + padding-bottom: 8px !important; + padding-right: 32px !important; +} + +.vc-rdb-review:hover { + background: var(--background-message-hover) !important; + border-radius: 8px; +} + +.vc-rdb-review-comment img { + vertical-align: text-top; +} + +.vc-rdb-review-comment { + overflow-y: hidden; + margin-top: 1px; + margin-bottom: 8px; + color: var(--text-normal); + font-size: 15px; +} + +.vc-rdb-blocked-badge { + cursor: pointer; +} + +.vc-rdb-block-modal-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.vc-rdb-block-modal { + padding: 1em; + display: grid; + gap: 0.75em; +} + +.vc-rdb-button-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +/* stylelint-disable-next-line media-feature-range-notation */ +@media (max-width: 600px) { + .vc-rdb-button-grid { + grid-template-columns: 1fr; + } +} + +.vc-rdb-block-modal-row { + display: flex; + height: 2em; + gap: 0.5em; + align-items: center; +} + +.vc-rdb-block-modal-row img { + border-radius: 50%; + height: 2em; + width: 2em; +} + +.vc-rdb-block-modal img::before { + content: ""; + display: block; + width: 100%; + height: 100%; + background-color: var(--background-modifier-accent); +} + +.vc-rdb-block-modal-username { + flex-grow: 1; +} + +.vc-rdb-block-modal-unblock { + cursor: pointer; +} diff --git a/src/plugins/reviewDB/utils.tsx b/src/plugins/reviewDB/utils.tsx new file mode 100644 index 000000000..916adc45e --- /dev/null +++ b/src/plugins/reviewDB/utils.tsx @@ -0,0 +1,54 @@ +/* + * 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 { classNameFactory } from "@api/Styles"; +import { Toasts, UserStore } from "@webpack/common"; + +import { Auth } from "./auth"; +import { Review, UserType } from "./entities"; + +export const cl = classNameFactory("vc-rdb-"); + +export function canDeleteReview(profileId: string, review: Review) { + const myId = UserStore.getCurrentUser().id; + return ( + myId === profileId + || review.sender.discordID === myId + || Auth.user?.type === UserType.Admin + ); +} + +export function canBlockReviewAuthor(profileId: string, review: Review) { + const myId = UserStore.getCurrentUser().id; + return profileId === myId && review.sender.discordID !== myId; +} + +export function canReportReview(review: Review) { + return review.sender.discordID !== UserStore.getCurrentUser().id; +} + +export function showToast(message: string, type = Toasts.Type.MESSAGE) { + Toasts.show({ + id: Toasts.genId(), + message, + type, + options: { + position: Toasts.Position.BOTTOM, // NOBODY LIKES TOASTS AT THE TOP + }, + }); +} diff --git a/src/plugins/roleColorEverywhere/index.tsx b/src/plugins/roleColorEverywhere/index.tsx index d718f4027..968027163 100644 --- a/src/plugins/roleColorEverywhere/index.tsx +++ b/src/plugins/roleColorEverywhere/index.tsx @@ -72,10 +72,6 @@ export default definePlugin({ { find: 'tutorialId:"whos-online', replacement: [ - { - match: /\i.roleIcon,\.\.\.\i/, - replace: "$&,color:$self.roleGroupColor(arguments[0])" - }, { match: /null,\i," — ",\i\]/, replace: "null,$self.roleGroupColor(arguments[0])]" @@ -83,6 +79,16 @@ export default definePlugin({ ], predicate: () => settings.store.memberList, }, + { + find: ".Messages.THREAD_BROWSER_PRIVATE", + replacement: [ + { + match: /children:\[\i," — ",\i\]/, + replace: "children:[$self.roleGroupColor(arguments[0])]" + }, + ], + predicate: () => settings.store.memberList, + }, { find: "renderPrioritySpeaker", replacement: [ diff --git a/src/plugins/sendTimestamps/index.tsx b/src/plugins/sendTimestamps/index.tsx index 7904545c8..ac7c5edb5 100644 --- a/src/plugins/sendTimestamps/index.tsx +++ b/src/plugins/sendTimestamps/index.tsx @@ -18,14 +18,24 @@ import "./styles.css"; +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; +import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { Devs } from "@utils/constants"; import { getTheme, insertTextIntoChatInputBox, Theme } from "@utils/discord"; import { Margins } from "@utils/margins"; import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; -import definePlugin from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, Forms, Parser, Select, Tooltip, useMemo, useState } from "@webpack/common"; +import definePlugin, { OptionType } from "@utils/types"; +import { Button, Forms, Parser, Select, useMemo, useState } from "@webpack/common"; + +const settings = definePluginSettings({ + replaceMessageContents: { + description: "Replace timestamps in message contents", + type: OptionType.BOOLEAN, + default: true, + }, +}); function parseTime(time: string) { const cleanTime = time.slice(1, -1).replace(/(\d)(AM|PM)$/i, "$1 $2"); @@ -113,79 +123,61 @@ function PickerModal({ rootProps, close }: { rootProps: ModalProps, close(): voi ); } +const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { + if (!isMainChat) return null; + + return ( + { + const key = openModal(props => ( + closeModal(key)} + /> + )); + }} + buttonProps={{ "aria-haspopup": "dialog" }} + > + + + ); +}; + export default definePlugin({ name: "SendTimestamps", description: "Send timestamps easily via chat box button & text shortcuts. Read the extended description!", - authors: [Devs.Ven, Devs.Tyler], - dependencies: ["MessageEventsAPI"], + authors: [Devs.Ven, Devs.Tyler, Devs.Grzesiek11], + dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], + settings, start() { + addChatBarButton("SendTimestamps", ChatBarIcon); this.listener = addPreSendListener((_, msg) => { - msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); + if (settings.store.replaceMessageContents) { + msg.content = msg.content.replace(/`\d{1,2}:\d{2} ?(?:AM|PM)?`/gi, parseTime); + } }); }, stop() { + removeChatBarButton("SendTimestamps"); removePreSendListener(this.listener); }, - chatBarIcon(chatBoxProps: { type: { analyticsName: string; }; }) { - if (chatBoxProps.type.analyticsName !== "normal") return null; - - return ( - - {({ onMouseEnter, onMouseLeave }) => ( -
- -
- ) - } -
- ); - }, - settingsAboutComponent() { const samples = [ "12:00", diff --git a/src/plugins/sendTimestamps/styles.css b/src/plugins/sendTimestamps/styles.css index 75c3f16bd..34a856a60 100644 --- a/src/plugins/sendTimestamps/styles.css +++ b/src/plugins/sendTimestamps/styles.css @@ -42,10 +42,6 @@ margin-bottom: 1em; } -.vc-st-button { - padding: 0 6px; -} - .vc-st-button svg { transform: scale(1.1) translateY(1px); } diff --git a/src/plugins/silentMessageToggle/index.tsx b/src/plugins/silentMessageToggle/index.tsx index b7b33826d..d4207a68d 100644 --- a/src/plugins/silentMessageToggle/index.tsx +++ b/src/plugins/silentMessageToggle/index.tsx @@ -16,12 +16,12 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, React, Tooltip } from "@webpack/common"; +import { React, useEffect, useState } from "@webpack/common"; let lastState = false; @@ -41,19 +41,15 @@ const settings = definePluginSettings({ } }); -function SilentMessageToggle(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - const [enabled, setEnabled] = React.useState(lastState); +const SilentMessageToggle: ChatBarButton = ({ isMainChat }) => { + const [enabled, setEnabled] = useState(lastState); function setEnabledValue(value: boolean) { if (settings.store.persistState) lastState = value; setEnabled(value); } - React.useEffect(() => { + useEffect(() => { const listener: SendListener = (_, message) => { if (enabled) { if (settings.store.autoDisable) setEnabledValue(false); @@ -65,55 +61,39 @@ function SilentMessageToggle(chatBoxProps: { return () => void removePreSendListener(listener); }, [enabled]); - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat) return null; return ( - - {tooltipProps => ( -
- -
- )} -
+ setEnabledValue(!enabled)} + > + + + {!enabled && <> + + + + + + } + + ); -} +}; export default definePlugin({ name: "SilentMessageToggle", authors: [Devs.Nuckyz, Devs.CatNoir], description: "Adds a button to the chat bar to toggle sending a silent message.", - dependencies: ["MessageEventsAPI"], - + dependencies: ["MessageEventsAPI", "ChatInputButtonAPI"], settings, - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], - chatBarIcon: ErrorBoundary.wrap(SilentMessageToggle, { noop: true }), + start: () => addChatBarButton("SilentMessageToggle", SilentMessageToggle), + stop: () => removeChatBarButton("SilentMessageToggle") }); diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index dae7ad4c9..8b59c6ace 100644 --- a/src/plugins/silentTyping/index.tsx +++ b/src/plugins/silentTyping/index.tsx @@ -16,12 +16,12 @@ * along with this program. If not, see . */ +import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, FluxDispatcher, React, Tooltip } from "@webpack/common"; +import { FluxDispatcher, React } from "@webpack/common"; const settings = definePluginSettings({ showIcon: { @@ -37,45 +37,32 @@ const settings = definePluginSettings({ } }); -function SilentTypingToggle(chatBoxProps: { - type: { - analyticsName: string; - }; -}) { - const { isEnabled } = settings.use(["isEnabled"]); +const SilentTypingToggle: ChatBarButton = ({ isMainChat }) => { + const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]); const toggle = () => settings.store.isEnabled = !settings.store.isEnabled; - if (chatBoxProps.type.analyticsName !== "normal") return null; + if (!isMainChat || !showIcon) return null; return ( - - {(tooltipProps: any) => ( -
- -
- )} -
+ + + + {isEnabled && } + + ); -} +}; export default definePlugin({ name: "SilentTyping", authors: [Devs.Ven, Devs.Rini], description: "Hide that you are typing", + dependencies: ["CommandsAPI", "ChatInputButtonAPI"], + settings, + patches: [ { find: '.dispatch({type:"TYPING_START_LOCAL"', @@ -84,17 +71,8 @@ export default definePlugin({ replace: "startTyping:$self.startTyping,stop" } }, - { - find: "ChannelTextAreaButtons", - predicate: () => settings.store.showIcon, - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, ], - dependencies: ["CommandsAPI"], - settings, + commands: [{ name: "silenttype", description: "Toggle whether you're hiding that you're typing or not.", @@ -120,5 +98,6 @@ export default definePlugin({ FluxDispatcher.dispatch({ type: "TYPING_START_LOCAL", channelId }); }, - chatBarIcon: ErrorBoundary.wrap(SilentTypingToggle, { noop: true }), + start: () => addChatBarButton("SilentTyping", SilentTypingToggle), + stop: () => removeChatBarButton("SilentTyping"), }); diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index 5d66d2265..416ce83fb 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -213,7 +213,7 @@ function applyRules(content: string): string { if (stringRules) { for (const rule of stringRules) { - if (!rule.find || !rule.replace) continue; + if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; content = ` ${content} `.replaceAll(rule.find, rule.replace.replaceAll("\\n", "\n")).replace(/^\s|\s$/g, ""); @@ -222,7 +222,7 @@ function applyRules(content: string): string { if (regexRules) { for (const rule of regexRules) { - if (!rule.find || !rule.replace) continue; + if (!rule.find) continue; if (rule.onlyIfIncludes && !content.includes(rule.onlyIfIncludes)) continue; try { diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx index 649589435..cc0ed5e93 100644 --- a/src/plugins/translate/TranslateIcon.tsx +++ b/src/plugins/translate/TranslateIcon.tsx @@ -16,9 +16,11 @@ * along with this program. If not, see . */ +import { ChatBarButton } from "@api/ChatButtons"; +import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; import { openModal } from "@utils/modal"; -import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common"; +import { Alerts, Forms } from "@webpack/common"; import { settings } from "./settings"; import { TranslateModal } from "./TranslateModal"; @@ -37,42 +39,49 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?: ); } -export function TranslateChatBarIcon({ slateProps }: { slateProps: { type: { analyticsName: string; }; }; }) { +export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => { const { autoTranslate } = settings.use(["autoTranslate"]); - if (slateProps.type.analyticsName !== "normal") - return null; + if (!isMainChat) return null; - const toggle = () => settings.store.autoTranslate = !autoTranslate; + const toggle = () => { + const newState = !autoTranslate; + settings.store.autoTranslate = newState; + if (newState && settings.store.showAutoTranslateAlert !== false) + Alerts.show({ + title: "Vencord Auto-Translate Enabled", + body: <> + + You just enabled auto translate (by right clicking the Translate icon). Any message you send will automatically be translated before being sent. + + + If this was an accident, disable it again, or it will change your message content before sending. + + , + cancelText: "Disable Auto-Translate", + confirmText: "Got it", + secondaryConfirmText: "Don't show again", + onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false, + onCancel: () => settings.store.autoTranslate = false + }); + }; return ( - - {({ onMouseEnter, onMouseLeave }) => ( -
- -
- )} -
+ openModal(props => ( + + )); + }} + onContextMenu={() => toggle()} + buttonProps={{ + "aria-haspopup": "dialog" + }} + > + + ); -} +}; diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx index 3b067c634..702e60cf7 100644 --- a/src/plugins/translate/index.tsx +++ b/src/plugins/translate/index.tsx @@ -18,11 +18,11 @@ import "./styles.css"; +import { addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addPreSendListener, removePreSendListener } from "@api/MessageEvents"; import { addButton, removeButton } from "@api/MessagePopover"; -import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { ChannelStore, Menu } from "@webpack/common"; @@ -55,25 +55,16 @@ export default definePlugin({ name: "Translate", description: "Translate messages with Google Translate", authors: [Devs.Ven], - dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI"], + dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"], settings, // not used, just here in case some other plugin wants it or w/e translate, - patches: [ - { - find: "ChannelTextAreaButtons", - replacement: { - match: /(\i)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&,(()=>{try{$2||$1.push($self.chatBarIcon(arguments[0]))}catch{}})()", - } - }, - ], - start() { addAccessory("vc-translation", props => ); addContextMenuPatch("message", messageCtxPatch); + addChatBarButton("vc-translate", TranslateChatBarIcon); addButton("vc-translate", message => { if (!message.content) return null; @@ -101,13 +92,8 @@ export default definePlugin({ stop() { removePreSendListener(this.preSend); removeContextMenuPatch("message", messageCtxPatch); + removeChatBarButton("vc-translate"); removeButton("vc-translate"); removeAccessory("vc-translation"); }, - - chatBarIcon: (slateProps: any) => ( - - - - ) }); diff --git a/src/plugins/translate/settings.ts b/src/plugins/translate/settings.ts index aa4f633a7..cef003a83 100644 --- a/src/plugins/translate/settings.ts +++ b/src/plugins/translate/settings.ts @@ -49,4 +49,6 @@ export const settings = definePluginSettings({ description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this", default: false } -}); +}).withPrivateSettings<{ + showAutoTranslateAlert: boolean; +}>(); diff --git a/src/plugins/translate/styles.css b/src/plugins/translate/styles.css index b6d2223a5..e9085b4ee 100644 --- a/src/plugins/translate/styles.css +++ b/src/plugins/translate/styles.css @@ -35,3 +35,7 @@ .vc-trans-auto-translate { color: var(--green-360); } + +.vc-trans-chat-button { + scale: 1.085; +} diff --git a/src/plugins/urbanDictionary/README.md b/src/plugins/urbanDictionary/README.md new file mode 100644 index 000000000..e065456a3 --- /dev/null +++ b/src/plugins/urbanDictionary/README.md @@ -0,0 +1,13 @@ +# Urban Dictionary + +Use /urban slash command to search for a definition for a word on [Urban Dictionary](https://www.urbandictionary.com/). + +## Preview + +![preview](https://i.imgur.com/1zwzj38.png) + +## Usage + +- Enable this plugin +- Set plugin settings as desired +- Type /urban and start getting definitions right into your Discord client. diff --git a/src/plugins/urbanDictionary/index.ts b/src/plugins/urbanDictionary/index.ts index 840fe5cb6..89dcdcba4 100644 --- a/src/plugins/urbanDictionary/index.ts +++ b/src/plugins/urbanDictionary/index.ts @@ -18,14 +18,24 @@ import { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands"; import { ApplicationCommandInputType } from "@api/Commands/types"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; + +const settings = definePluginSettings({ + resultsAmount: { + type: OptionType.NUMBER, + description: "The amount of results you want to get (more gives better results, but is slower)", + default: 10 + } +}); export default definePlugin({ name: "UrbanDictionary", description: "Search for a word on Urban Dictionary via /urban slash command", authors: [Devs.jewdev], dependencies: ["CommandsAPI"], + settings, commands: [ { name: "urban", @@ -41,12 +51,16 @@ export default definePlugin({ ], execute: async (args, ctx) => { try { - const query = encodeURIComponent(args[0].value); - const { list: [definition] } = await (await fetch(`https://api.urbandictionary.com/v0/define?term=${query}`)).json(); + const query: string = encodeURIComponent(args[0].value); + const { list } = await fetch(`https://api.urbandictionary.com/v0/define?term=${query}&per_page=${settings.store.resultsAmount}`).then(response => response.json()); - if (!definition) + if (!list.length) return void sendBotMessage(ctx.channel.id, { content: "No results found." }); + const definition = list.reduce((prev, curr) => { + return prev.thumbs_up > curr.thumbs_up ? prev : curr; + }); + const linkify = (text: string) => text .replaceAll("\r\n", "\n") .replace(/([*>_`~\\])/gsi, "\\$1") diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index 2b0ce14e6..0a805a0d2 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -33,7 +33,7 @@ function VencordPopout(onClose: () => void) { const pluginEntries = [] as ReactNode[]; for (const plugin of Object.values(Vencord.Plugins.plugins)) { - if (plugin.toolboxActions) { + if (plugin.toolboxActions && Vencord.Plugins.isPluginEnabled(plugin.name)) { pluginEntries.push( () => { - if ((name === "Guild" && !props.guild) || (name === "User" && !props.user)) return; + const value = props[name.toLowerCase()]; + if (!value) return; + if (props.label === i18n.Messages.CHANNEL_ACTIONS_MENU_LABEL) return; // random shit like notification settings + const lastChild = children.at(-1); if (lastChild?.key === "developer-actions") { const p = lastChild.props; if (!Array.isArray(p.children)) p.children = [p.children]; - ({ children } = p); + + children = p.children; } children.splice(-1, 0, openViewRawModal(JSON.stringify(props[name.toLowerCase()], null, 4), name)} + action={() => openViewRawModal(JSON.stringify(value, null, 4), name)} icon={CopyIcon} /> ); diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index eb076dfd0..5f6beca2c 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -46,6 +46,23 @@ const settings = definePluginSettings({ } }); +const MEDIA_PROXY_URL = "https://media.discordapp.net"; +const CDN_URL = "https://cdn.discordapp.com"; + +function fixImageUrl(urlString: string, explodeWebp: boolean) { + const url = new URL(urlString); + if (url.origin === CDN_URL) return urlString; + if (url.origin === MEDIA_PROXY_URL) return CDN_URL + url.pathname; + + url.searchParams.delete("width"); + url.searchParams.delete("height"); + url.searchParams.set("quality", "lossless"); + if (explodeWebp && url.searchParams.get("format") === "webp") + url.searchParams.set("format", "png"); + + return url.toString(); +} + export default definePlugin({ name: "WebContextMenus", description: "Re-adds context menus missing in the web version of Discord: Links & Images (Copy/Open Link/Image), Text Area (Copy, Cut, Paste, SpellCheck)", @@ -169,32 +186,53 @@ export default definePlugin({ match: /let\{text:\i=""/, replace: "return [null,null];$&" } + }, + + // Add back "Show My Camera" context menu + { + find: '.default("MediaEngineWebRTC");', + replacement: { + match: /supports\(\i\)\{switch\(\i\)\{case (\i).Features/, + replace: "$&.DISABLE_VIDEO:return true;case $1.Features" + } } ], async copyImage(url: string) { - // Clipboard only supports image/png, jpeg and similar won't work. Thus, we need to convert it to png - // via canvas first - const img = new Image(); - img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = img.naturalWidth; - canvas.height = img.naturalHeight; - canvas.getContext("2d")!.drawImage(img, 0, 0); + url = fixImageUrl(url, true); - canvas.toBlob(data => { - navigator.clipboard.write([ - new ClipboardItem({ - "image/png": data! - }) - ]); - }, "image/png"); - }; - img.crossOrigin = "anonymous"; - img.src = url; + let imageData = await fetch(url).then(r => r.blob()); + if (imageData.type !== "image/png") { + const bitmap = await createImageBitmap(imageData); + + const canvas = document.createElement("canvas"); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + canvas.getContext("2d")!.drawImage(bitmap, 0, 0); + + await new Promise(done => { + canvas.toBlob(data => { + imageData = data!; + done(); + }, "image/png"); + }); + } + + if (IS_VESKTOP && VesktopNative.clipboard) { + VesktopNative.clipboard.copyImage(await imageData.arrayBuffer(), url); + return; + } else { + navigator.clipboard.write([ + new ClipboardItem({ + "image/png": imageData + }) + ]); + } }, async saveImage(url: string) { + url = fixImageUrl(url, false); + const data = await fetchImage(url); if (!data) return; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index a94ba0fc3..55af93605 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -42,6 +42,10 @@ export interface Dev { * If you are fine with attribution but don't want the badge, add badge: false */ export const Devs = /* #__PURE__*/ Object.freeze({ + Nobody: { + name: "Nobody", + id: 0n, + }, Ven: { name: "Vendicated", id: 343383572805058560n @@ -359,10 +363,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "bb010g", id: 72791153467990016n, }, - Lumap: { - name: "lumap", - id: 635383782576357407n - }, Dolfies: { name: "Dolfies", id: 852892297661906993n, @@ -403,6 +403,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Grzesiek11", id: 368475654662127616n, }, + Samwich: { + name: "Samwich", + id: 976176454511509554n, + }, + coolelectronics: { + name: "coolelectronics", + id: 696392247205298207n, + } } satisfies Record); // iife so #__PURE__ works correctly