diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f27870705..46645ad8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: - name: Clean up obsolete files run: | - rm -rf dist/extension* Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map + rm -rf dist/*-unpacked Vencord.user.css vencordDesktopRenderer.css vencordDesktopRenderer.css.map - name: Get some values needed for the release id: release_values diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 89cc2cb8a..f9ddf608d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,15 +35,15 @@ jobs: - name: Publish extension run: | - cd dist/extension-unpacked - # Do not fail so that even if chrome fails, firefox gets a shot. But also store exit code to fail workflow later EXIT_CODE=0 # Chrome + cd dist/chromium-unpacked pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish || EXIT_CODE=$? # Firefox + cd ../firefox-unpacked npm i -g web-ext@7.4.0 web-ext-submit@7.4.0 web-ext-submit || EXIT_CODE=$? @@ -58,4 +58,3 @@ jobs: # Firefox WEB_EXT_API_KEY: ${{ secrets.WEBEXT_USER }} WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_SECRET }} - diff --git a/browser/background.js b/browser/background.js new file mode 100644 index 000000000..7fc4a8224 --- /dev/null +++ b/browser/background.js @@ -0,0 +1,32 @@ +/** + * @template T + * @param {T[]} arr + * @param {(v: T) => boolean} predicate + */ +function removeFirst(arr, predicate) { + const idx = arr.findIndex(predicate); + if (idx !== -1) arr.splice(idx, 1); +} + +chrome.webRequest.onHeadersReceived.addListener( + ({ responseHeaders, type, url }) => { + if (!responseHeaders) return; + + if (type === "main_frame") { + // In main frame requests, the CSP needs to be removed to enable fetching of custom css + // as desired by the user + removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-security-policy"); + } else if (type === "stylesheet" && url.startsWith("https://raw.githubusercontent.com")) { + // Most users will load css from GitHub, but GitHub doesn't set the correct content type, + // so we fix it here + removeFirst(responseHeaders, h => h.name.toLowerCase() === "content-type"); + responseHeaders.push({ + name: "Content-Type", + value: "text/css" + }); + } + return { responseHeaders }; + }, + { urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"], types: ["main_frame", "stylesheet"] }, + ["blocking", "responseHeaders"] +); diff --git a/browser/manifest.json b/browser/manifest.json index 293888051..cb7ecc71e 100644 --- a/browser/manifest.json +++ b/browser/manifest.json @@ -21,7 +21,8 @@ { "run_at": "document_start", "matches": ["*://*.discord.com/*"], - "js": ["content.js"] + "js": ["content.js"], + "all_frames": true } ], diff --git a/browser/manifestv2.json b/browser/manifestv2.json new file mode 100644 index 000000000..3cac9450a --- /dev/null +++ b/browser/manifestv2.json @@ -0,0 +1,41 @@ +{ + "manifest_version": 2, + "minimum_chrome_version": "91", + + "name": "Vencord Web", + "description": "The cutest Discord mod now in your browser", + "author": "Vendicated", + "homepage_url": "https://github.com/Vendicated/Vencord", + "icons": { + "128": "icon.png" + }, + + "permissions": [ + "webRequest", + "webRequestBlocking", + "*://*.discord.com/*", + "https://raw.githubusercontent.com/*" + ], + + "content_scripts": [ + { + "run_at": "document_start", + "matches": ["*://*.discord.com/*"], + "js": ["content.js"], + "all_frames": true + } + ], + + "background": { + "scripts": ["background.js"] + }, + + "web_accessible_resources": ["dist/Vencord.js", "dist/Vencord.css"], + + "browser_specific_settings": { + "gecko": { + "id": "vencord-firefox@vendicated.dev", + "strict_min_version": "91.0" + } + } +} diff --git a/browser/modifyResponseHeaders.json b/browser/modifyResponseHeaders.json index ed3907596..5b0b3e376 100644 --- a/browser/modifyResponseHeaders.json +++ b/browser/modifyResponseHeaders.json @@ -15,7 +15,7 @@ ] }, "condition": { - "resourceTypes": ["main_frame"] + "resourceTypes": ["main_frame", "sub_frame"] } }, { diff --git a/package.json b/package.json index 18de64381..59ae57294 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.1.6", + "version": "1.1.9", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 98d56b0d4..cc27ea8cd 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -142,6 +142,7 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content await Promise.all([ appendCssRuntime, buildPluginZip("extension.zip", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], true), - buildPluginZip("extension-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false), + buildPluginZip("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"], false), + buildPluginZip("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"], false), ]); diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index d55cc8abe..b5e705bdc 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -186,7 +186,16 @@ page.on("console", async e => { } else if (isDebug) { console.error(e.text()); } else if (level === "error") { - const text = e.text(); + const text = await Promise.all( + e.args().map(async a => { + try { + return await maybeGetError(a) || await a.jsonValue(); + } catch (e) { + return a.toString(); + } + }) + ).then(a => a.join(" ")); + if (!text.startsWith("Failed to load resource: the server responded with a status of")) { console.error("Got unexpected error", text); report.otherErrors.push(text); @@ -258,7 +267,7 @@ function runTime(token: string) { if (!isWasm) await wreq.e(id as any); - await new Promise(r => setTimeout(r, 100)); + await new Promise(r => setTimeout(r, 500)); } console.error("[PUP_DEBUG]", "Finished loading chunks!"); diff --git a/src/Vencord.ts b/src/Vencord.ts index f11ca167c..ad793456e 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -27,14 +27,13 @@ export { PlainSettings, Settings }; import "./utils/quickCss"; import "./webpack/patchWebpack"; -import { relaunch } from "@utils/native"; - import { showNotification } from "./api/Notifications"; import { PlainSettings, Settings } from "./api/settings"; import { patches, PMLogger, startAllPlugins } from "./plugins"; import { localStorage } from "./utils/localStorage"; +import { relaunch } from "./utils/native"; import { getCloudSettings, putCloudSettings } from "./utils/settingsSync"; -import { checkForUpdates, rebuild, update,UpdateLogger } from "./utils/updater"; +import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater"; import { onceReady } from "./webpack"; import { SettingsRouter } from "./webpack/common"; @@ -57,7 +56,7 @@ async function syncSettings() { title: "Cloud Settings", body: "Your settings have been updated! Click here to restart to fully apply changes!", color: "var(--green-360)", - onClick: () => window.DiscordNative.app.relaunch() + onClick: relaunch }); } } diff --git a/src/api/Badges.ts b/src/api/Badges.ts index d4aabaf21..9abaefe2b 100644 --- a/src/api/Badges.ts +++ b/src/api/Badges.ts @@ -29,11 +29,12 @@ export enum BadgePosition { export interface ProfileBadge { /** The tooltip to show on hover. Required for image badges */ - tooltip?: string; + description?: string; /** Custom component for the badge (tooltip not included) */ component?: ComponentType; /** The custom image to use */ image?: string; + link?: string; /** Action to perform when you click the badge */ onClick?(): void; /** Should the user display this badge? */ @@ -69,17 +70,19 @@ export function removeBadge(badge: ProfileBadge) { * Inject badges into the profile badges array. * You probably don't need to use this. */ -export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) { +export function _getBadges(args: BadgeUserArgs) { + const badges = [] as ProfileBadge[]; for (const badge of Badges) { if (!badge.shouldShow || badge.shouldShow(args)) { badge.position === BadgePosition.START - ? badgeArray.unshift({ ...badge, ...args }) - : badgeArray.push({ ...badge, ...args }); + ? badges.unshift({ ...badge, ...args }) + : badges.push({ ...badge, ...args }); } } - (Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id); + const donorBadge = (Plugins.BadgeAPI as any).getDonorBadge(args.user.id); + if (donorBadge) badges.unshift(donorBadge); - return badgeArray; + return badges; } export interface BadgeUserArgs { diff --git a/src/api/Commands/index.ts b/src/api/Commands/index.ts index 88139d4d3..3f639a18a 100644 --- a/src/api/Commands/index.ts +++ b/src/api/Commands/index.ts @@ -111,6 +111,7 @@ function registerSubCommands(cmd: Command, plugin: string) { ...o, type: ApplicationCommandType.CHAT_INPUT, name: `${cmd.name} ${o.name}`, + id: `${o.name}-${cmd.id}`, displayName: `${cmd.name} ${o.name}`, subCommandPath: [{ name: o.name, diff --git a/src/api/ContextMenu.ts b/src/api/ContextMenu.ts index 16f9f32b5..4d1d577b4 100644 --- a/src/api/ContextMenu.ts +++ b/src/api/ContextMenu.ts @@ -19,17 +19,20 @@ import Logger from "@utils/Logger"; import type { ReactElement } from "react"; +type ContextMenuPatchCallbackReturn = (() => void) | void; /** * @param children The rendered context menu elements * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example + * @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates) */ -export type NavContextMenuPatchCallback = (children: Array, ...args: Array) => void; +export type NavContextMenuPatchCallback = (children: Array, ...args: Array) => ContextMenuPatchCallbackReturn; /** - * @param The navId of the context menu being patched + * @param navId The navId of the context menu being patched * @param children The rendered context menu elements * @param args Any arguments passed into making the context menu, like the guild, channel, user or message for example + * @returns A callback which is only ran once used to modify the context menu elements (Use to avoid duplicates) */ -export type GlobalContextMenuPatchCallback = (navId: string, children: Array, ...args: Array) => void; +export type GlobalContextMenuPatchCallback = (navId: string, children: Array, ...args: Array) => ContextMenuPatchCallbackReturn; const ContextMenuLogger = new Logger("ContextMenu"); @@ -78,6 +81,7 @@ export function removeContextMenuPatch>(navId: /** * Remove a global context menu patch + * @param patch The patch to be removed * @returns Wheter the patch was sucessfully removed */ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallback): boolean { @@ -87,12 +91,13 @@ export function removeGlobalContextMenuPatch(patch: GlobalContextMenuPatchCallba /** * A helper function for finding the children array of a group nested inside a context menu based on the id of one of its childs * @param id The id of the child + * @param children The context menu children */ -export function findGroupChildrenByChildId(id: string, children: Array, itemsArray?: Array): Array | null { +export function findGroupChildrenByChildId(id: string, children: Array, _itemsArray?: Array): Array | null { for (const child of children) { if (child == null) continue; - if (child.props?.id === id) return itemsArray ?? null; + if (child.props?.id === id) return _itemsArray ?? null; let nextChildren = child.props?.children; if (nextChildren) { @@ -118,6 +123,8 @@ interface ContextMenuProps { onClose: (callback: (...args: Array) => any) => void; } +const patchedMenus = new WeakSet(); + export function _patchContextMenu(props: ContextMenuProps) { props.contextMenuApiArguments ??= []; const contextMenuPatches = navPatches.get(props.navId); @@ -127,7 +134,8 @@ export function _patchContextMenu(props: ContextMenuProps) { if (contextMenuPatches) { for (const patch of contextMenuPatches) { try { - patch(props.children, ...props.contextMenuApiArguments); + const callback = patch(props.children, ...props.contextMenuApiArguments); + if (!patchedMenus.has(props)) callback?.(); } catch (err) { ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); } @@ -136,9 +144,12 @@ export function _patchContextMenu(props: ContextMenuProps) { for (const patch of globalPatches) { try { - patch(props.navId, props.children, ...props.contextMenuApiArguments); + const callback = patch(props.navId, props.children, ...props.contextMenuApiArguments); + if (!patchedMenus.has(props)) callback?.(); } catch (err) { ContextMenuLogger.error("Global patch errored,", err); } } + + patchedMenus.add(props); } diff --git a/src/api/Notifications/Notifications.tsx b/src/api/Notifications/Notifications.tsx index c842ec887..600ea63d1 100644 --- a/src/api/Notifications/Notifications.tsx +++ b/src/api/Notifications/Notifications.tsx @@ -77,6 +77,8 @@ function _showNotification(notification: NotificationData, id: number) { } function shouldBeNative() { + if (typeof Notification === "undefined") return false; + const { useNative } = Settings.notifications; if (useNative === "always") return true; if (useNative === "not-focused") return !document.hasFocus(); diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index 83a087004..a3df69d2e 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -91,7 +91,7 @@ export default ErrorBoundary.wrap(function () { return ( <> - Paste links to .css / .theme.css files here + Paste links to .theme.css files here One link per line Make sure to use the raw links or github.io links! @@ -103,7 +103,7 @@ export default ErrorBoundary.wrap(function () { GitHub If using the BD site, click on "Source" somewhere below the Download button - In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button + In the GitHub repository of your theme, find X.theme.css, click on it, then click the "Raw" button If the theme has configuration that requires you to edit the file:
    diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index 7bb5353b1..751220856 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -18,7 +18,7 @@ import { openNotificationLogModal } from "@api/Notifications/notificationLog"; -import { useSettings } from "@api/settings"; +import { Settings, useSettings } from "@api/settings"; import { classNameFactory } from "@api/Styles"; import DonateButton from "@components/DonateButton"; import ErrorBoundary from "@components/ErrorBoundary"; @@ -43,7 +43,6 @@ function VencordSettings() { fallbackValue: "Loading..." }); const settings = useSettings(); - const notifSettings = settings.notifications; const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []); @@ -158,8 +157,16 @@ function VencordSettings() { + {typeof Notification !== "undefined" && } + + ); +} + +function NotificationSection({ settings }: { settings: typeof Settings["notifications"]; }) { + return ( + <> Notification Style - {notifSettings.useNative !== "never" && Notification.permission === "denied" && ( + {settings.useNative !== "never" && Notification?.permission === "denied" && ( Desktop Notification Permission denied You have denied Notification Permissions. Thus, Desktop notifications will not work! @@ -178,35 +185,35 @@ function VencordSettings() { { label: "Only use Desktop notifications when Discord is not focused", value: "not-focused", default: true }, { label: "Always use Desktop notifications", value: "always" }, { label: "Always use Vencord notifications", value: "never" }, - ] satisfies Array<{ value: typeof settings["notifications"]["useNative"]; } & Record>} + ] satisfies Array<{ value: typeof settings["useNative"]; } & Record>} closeOnSelect={true} - select={v => notifSettings.useNative = v} - isSelected={v => v === notifSettings.useNative} + select={v => settings.useNative = v} + isSelected={v => v === settings.useNative} serialize={identity} /> Notification Position