diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6e71f929..9cb7407d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,25 +15,9 @@ env: GITHUB_TOKEN: ${{ secrets.ETOKEN }} jobs: - DetermineRunner: - name: Determine Runner - runs-on: ubuntu-latest - outputs: - runner: ${{ steps.set-runner.outputs.runner }} - steps: - - name: Determine which runner to use - id: set-runner - uses: benjaminmichaelis/get-soonest-available-runner@v1.1.0 - with: - primary-runner: "self-hosted" - fallback-runner: "ubuntu-latest" - min-available-runners: 1 - github-token: ${{ env.GITHUB_TOKEN }} - Build: name: Build Equicord - needs: DetermineRunner - runs-on: ${{ needs.DetermineRunner.outputs.runner}} + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -89,12 +73,10 @@ jobs: rm release/*.map - name: Upload Equicord - if: github.repository == 'Equicord/Equicord' run: | gh release upload latest --clobber dist/release/* - - name: Upload Plugins JSON to Ignore repo - if: github.repository == 'Equicord/Equicord' + - name: Upload Plugins JSON to Equibored repo run: | git config --global user.name "$USERNAME" git config --global user.email "78185467+thororen1234@users.noreply.github.com" diff --git a/.github/workflows/codeberg-mirror.yml b/.github/workflows/codeberg-mirror.yml index 0f7ae1d1..9bbb434d 100644 --- a/.github/workflows/codeberg-mirror.yml +++ b/.github/workflows/codeberg-mirror.yml @@ -9,28 +9,9 @@ on: - cron: "0 */6 * * *" jobs: - DetermineRunner: - name: Determine Runner - runs-on: ubuntu-latest - outputs: - runner: ${{ steps.set-runner.outputs.runner }} - steps: - - name: Determine which runner to use - id: set-runner - uses: benjaminmichaelis/get-soonest-available-runner@v1.1.0 - with: - primary-runner: "self-hosted" - fallback-runner: "ubuntu-latest" - min-available-runners: 1 - github-token: ${{ env.GITHUB_TOKEN }} - env: - GITHUB_TOKEN: ${{ secrets.ETOKEN }} - codeberg: name: Sync Codeberg and Github - if: github.repository == 'Equicord/Equicord' - needs: DetermineRunner - runs-on: ${{ needs.DetermineRunner.outputs.runner }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index e410f5bf..c396c3e9 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -8,7 +8,6 @@ on: jobs: TestPlugins: name: Test Patches - if: github.repository == 'Equicord/Equicord' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/syncDev.yml b/.github/workflows/syncDev.yml index 9890bbbd..1c106318 100644 --- a/.github/workflows/syncDev.yml +++ b/.github/workflows/syncDev.yml @@ -1,25 +1,25 @@ name: Sync Vencord Dev env: - WORKFLOW_TOKEN: ${{ secrets.ETOKEN }} - UPSTREAM_URL: "https://github.com/Vendicated/Vencord.git" - UPSTREAM_BRANCH: "dev" - DOWNSTREAM_BRANCH: "dev" + WORKFLOW_TOKEN: ${{ secrets.ETOKEN }} + UPSTREAM_URL: "https://github.com/Vendicated/Vencord.git" + UPSTREAM_BRANCH: "dev" + DOWNSTREAM_BRANCH: "dev" on: - schedule: - - cron: "0 * * * *" - workflow_dispatch: + schedule: + - cron: "0 * * * *" + workflow_dispatch: jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Sync Vencord Dev - id: sync - uses: verticalsync/sync-upstream-repo@master - with: - upstream_repo: ${{ env.UPSTREAM_URL }} - upstream_branch: ${{ env.UPSTREAM_BRANCH }} - downstream_branch: ${{ env.DOWNSTREAM_BRANCH }} - token: ${{ env.WORKFLOW_TOKEN }} + build: + runs-on: ubuntu-latest + steps: + - name: Sync Vencord Dev + id: sync + uses: verticalsync/sync-upstream-repo@master + with: + upstream_repo: ${{ env.UPSTREAM_URL }} + upstream_branch: ${{ env.UPSTREAM_BRANCH }} + downstream_branch: ${{ env.DOWNSTREAM_BRANCH }} + token: ${{ env.WORKFLOW_TOKEN }} diff --git a/.github/workflows/syncMain.yml b/.github/workflows/syncMain.yml index b549a0d7..147c949f 100644 --- a/.github/workflows/syncMain.yml +++ b/.github/workflows/syncMain.yml @@ -1,25 +1,25 @@ name: Sync Vencord Main env: - WORKFLOW_TOKEN: ${{ secrets.ETOKEN }} - UPSTREAM_URL: "https://github.com/Vendicated/Vencord.git" - UPSTREAM_BRANCH: "main" - DOWNSTREAM_BRANCH: "main" + WORKFLOW_TOKEN: ${{ secrets.ETOKEN }} + UPSTREAM_URL: "https://github.com/Vendicated/Vencord.git" + UPSTREAM_BRANCH: "main" + DOWNSTREAM_BRANCH: "main" on: - schedule: - - cron: "0 * * * *" - workflow_dispatch: + schedule: + - cron: "0 * * * *" + workflow_dispatch: jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Sync Vencord Main - id: sync - uses: verticalsync/sync-upstream-repo@master - with: - upstream_repo: ${{ env.UPSTREAM_URL }} - upstream_branch: ${{ env.UPSTREAM_BRANCH }} - downstream_branch: ${{ env.DOWNSTREAM_BRANCH }} - token: ${{ env.WORKFLOW_TOKEN }} + build: + runs-on: ubuntu-latest + steps: + - name: Sync Vencord Main + id: sync + uses: verticalsync/sync-upstream-repo@master + with: + upstream_repo: ${{ env.UPSTREAM_URL }} + upstream_branch: ${{ env.UPSTREAM_BRANCH }} + downstream_branch: ${{ env.DOWNSTREAM_BRANCH }} + token: ${{ env.WORKFLOW_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d14307aa..f550f151 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,29 +10,10 @@ on: - dev jobs: - DetermineRunner: - name: Determine Runner - if: ${{ github.event_name == 'push' }} - runs-on: ubuntu-latest - outputs: - runner: ${{ steps.set-runner.outputs.runner }} - steps: - - name: Determine which runner to use - id: set-runner - uses: benjaminmichaelis/get-soonest-available-runner@v1.1.0 - with: - primary-runner: "self-hosted" - fallback-runner: "ubuntu-latest" - min-available-runners: 1 - github-token: ${{ env.GITHUB_TOKEN }} - env: - GITHUB_TOKEN: ${{ secrets.ETOKEN }} - Test: name: Test For Pushes - needs: DetermineRunner if: ${{ github.event_name == 'push' }} - runs-on: ${{ needs.DetermineRunner.outputs.runner}} + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 785287df..0339fb0c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - Request for plugins from Discord.
-Extra included plugins (119 additional plugins) +Extra included plugins (122 additional plugins) - AllCallTimers by MaxHerbold and D3SOX - AltKrispSwitch by newwares @@ -82,27 +82,29 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - InRole by nin0dev - IrcColors by Grzesiek11 - IRememberYou by zoodogood +- Jumpscare by Surgedevs - JumpToStart by Samwich - KeyboardSounds by HypedDomi - KeywordNotify by camila314 (maintained by thororen) - LoginWithQR by nexpid - MediaDownloader by Colorman +- MediaPlaybackSpeed by D3SOX - Meow by Samwich +- MessageColors by Hen - MessageLinkTooltip by Kyuuhachi - MessageLoggerEnhanced by Aria - MessageTranslate by Samwich - ModalFade by Kyuuhachi -- MusicTitleRPC by Blackilykay - NewPluginsManager by Sqaaakoi - noAppsAllowed by kvba - NoBulletPoints by Samwich - NoDefaultEmojis by Samwich - NoDeleteSafety by Samwich +- NoMirroredCamera by Nyx - NoModalAnimation by AutumnVN - NoNitroUpsell by thororen - NoRoleHeaders by Samwich - NotificationTitle by Kyuuhachi -- NotifyUserChanges by D3SOX - OnePingPerDM by ProffDea - PlatformSpoofer by Drag - PurgeMessages by bhop and nyx @@ -117,7 +119,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - SearchFix by Jaxx - SekaiStickers by MaiKokain - ServerSearch by camila314 -- Shakespearean by vmohammad +- Shakespearean by vmohammad (Dev build only) - ShowBadgesInChat by Inbestigator and KrystalSkull - Slap by Korbo - SoundBoardLogger by Moxxie, fres, echo (maintained by thororen) @@ -132,13 +134,14 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend - Translate+ by Prince527 (Using Translate by Ven) - UnitConverter by sadan - UnlimitedAccounts by thororen +- UnreadCountBadge by Joona - UserPFP by nexpid (maintained by thororen) - UwUifier by echo - VCSupport by thororen - VencordRPC by AutumnVN - VideoSpeed by Samwich - ViewRaw2 by Kyuuhachi -- VoiceChatUtilities by Dams and D3SOX +- VoiceChatUtilities by D3SOX - WebpackTarball by Kyuuhachi - WhosWatching by fres - WigglyText by nexpid diff --git a/scripts/build/buildWeb.mts b/scripts/build/buildWeb.mts index 3cc42ab2..8f1b33ef 100644 --- a/scripts/build/buildWeb.mts +++ b/scripts/build/buildWeb.mts @@ -36,7 +36,7 @@ const commonOptions: esbuild.BuildOptions = { external: ["~plugins", "~git-hash", "/assets/*"], plugins: [ globPlugins("web"), - ...commonOpts.plugins, + ...commonOpts.plugins ], target: ["esnext"], define: { diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 9a0e2c5b..1d46d578 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -35,21 +35,6 @@ for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) { const CANARY = process.env.USE_CANARY === "true"; -const browser = await pup.launch({ - headless: true, - executablePath: process.env.CHROMIUM_BIN -}); - -const page = await browser.newPage(); -await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"); -await page.setBypassCSP(true); - -async function maybeGetError(handle: JSHandle): Promise { - return await (handle as JSHandle)?.getProperty("message") - .then(m => m?.jsonValue()) - .catch(() => undefined); -} - const report = { badPatches: [] as { plugin: string; @@ -181,123 +166,11 @@ async function printReport() { } } -page.on("console", async e => { - const level = e.type(); - const rawArgs = e.args(); - - async function getText() { - try { - return await Promise.all( - e.args().map(async a => { - return await maybeGetError(a) || await a.jsonValue(); - }) - ).then(a => a.join(" ").trim()); - } catch { - return e.text(); - } - } - - const firstArg = await rawArgs[0]?.jsonValue(); - - const isEquicord = firstArg === "[Equicord]"; - const isDebug = firstArg === "[PUP_DEBUG]"; - - outer: - if (isEquicord) { - try { - var args = await Promise.all(e.args().map(a => a.jsonValue())); - } catch { - break outer; - } - - const [, tag, message, otherMessage] = args as Array; - - switch (tag) { - case "WebpackInterceptor:": - const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!; - if (!patchFailMatch) break; - - console.error(await getText()); - process.exitCode = 1; - - const [, plugin, type, id, regex] = patchFailMatch; - report.badPatches.push({ - plugin, - type, - id, - match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"), - error: await maybeGetError(e.args()[3]) - }); - - break; - case "PluginManager:": - const failedToStartMatch = message.match(/Failed to start (.+)/); - if (!failedToStartMatch) break; - - console.error(await getText()); - process.exitCode = 1; - - const [, name] = failedToStartMatch; - report.badStarts.push({ - plugin: name, - error: await maybeGetError(e.args()[3]) ?? "Unknown error" - }); - - break; - case "LazyChunkLoader:": - console.error(await getText()); - - switch (message) { - case "A fatal error occurred:": - process.exit(1); - } - - break; - case "Reporter:": - console.error(await getText()); - - switch (message) { - case "A fatal error occurred:": - process.exit(1); - case "Webpack Find Fail:": - process.exitCode = 1; - report.badWebpackFinds.push(otherMessage); - break; - case "Finished test": - await browser.close(); - await printReport(); - process.exit(); - } - } - } - - if (isDebug) { - console.error(await getText()); - } else if (level === "error") { - const text = await getText(); - - if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) { - if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) { - report.ignoredErrors.push(text); - } else { - console.error("[Unexpected Error]", text); - report.otherErrors.push(text); - } - } - } -}); - -page.on("error", e => console.error("[Error]", e.message)); -page.on("pageerror", e => { - if (e.message.includes("Sentry successfully disabled")) return; - - if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) { - console.error("[Page Error]", e.message); - report.otherErrors.push(e.message); - } else { - report.ignoredErrors.push(e.message); - } -}); +async function maybeGetError(handle: JSHandle): Promise { + return await (handle as JSHandle)?.getProperty("message") + .then(m => m?.jsonValue()) + .catch(() => undefined); +} async function reporterRuntime(token: string) { Vencord.Webpack.waitFor( @@ -309,11 +182,144 @@ async function reporterRuntime(token: string) { ); } -await page.evaluateOnNewDocument(` - if (location.host.endsWith("discord.com")) { - ${readFileSync("./dist/browser/browser.js", "utf-8")}; - (${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); - } -`); +try { + const browser = await pup.launch({ + headless: true, + executablePath: process.env.CHROMIUM_BIN + }); + const page = await browser.newPage(); + await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"); + await page.setBypassCSP(true); -await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login"); + page.on("console", async e => { + const level = e.type(); + const rawArgs = e.args(); + + async function getText() { + try { + return await Promise.all( + e.args().map(async a => { + return await maybeGetError(a) || await a.jsonValue(); + }) + ).then(a => a.join(" ").trim()); + } catch { + return e.text(); + } + } + + const firstArg = await rawArgs[0]?.jsonValue(); + + const isEquicord = firstArg === "[Equicord]"; + const isDebug = firstArg === "[PUP_DEBUG]"; + + outer: + if (isEquicord) { + try { + var args = await Promise.all(e.args().map(a => a.jsonValue())); + } catch { + break outer; + } + + const [, tag, message, otherMessage] = args as Array; + + switch (tag) { + case "WebpackInterceptor:": + const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!; + if (!patchFailMatch) break; + + console.error(await getText()); + process.exitCode = 1; + + const [, plugin, type, id, regex] = patchFailMatch; + report.badPatches.push({ + plugin, + type, + id, + match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"), + error: await maybeGetError(e.args()[3]) + }); + + break; + case "PluginManager:": + const failedToStartMatch = message.match(/Failed to start (.+)/); + if (!failedToStartMatch) break; + + console.error(await getText()); + process.exitCode = 1; + + const [, name] = failedToStartMatch; + report.badStarts.push({ + plugin: name, + error: await maybeGetError(e.args()[3]) ?? "Unknown error" + }); + + break; + case "LazyChunkLoader:": + console.error(await getText()); + + switch (message) { + case "A fatal error occurred:": + process.exit(1); + } + + break; + case "Reporter:": + console.error(await getText()); + + switch (message) { + case "A fatal error occurred:": + process.exit(1); + case "Webpack Find Fail:": + process.exitCode = 1; + report.badWebpackFinds.push(otherMessage); + break; + case "Finished test": + await browser.close(); + await printReport(); + process.exit(); + } + } + } + + if (isDebug) { + console.error(await getText()); + } else if (level === "error") { + const text = await getText(); + + if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) { + if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) { + report.ignoredErrors.push(text); + } else { + console.error("[Unexpected Error]", text); + report.otherErrors.push(text); + } + } + } + }); + + page.on("error", e => console.error("[Error]", e.message)); + page.on("pageerror", e => { + if (e.message.includes("Sentry successfully disabled")) return; + if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) { + console.error("[Page Error]", e.message); + report.otherErrors.push(e.message); + } else { + report.ignoredErrors.push(e.message); + } + }); + + await page.evaluateOnNewDocument(` + if (location.host.endsWith("discord.com")) { + ${readFileSync("./dist/browser/browser.js", "utf-8")}; + (${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); + } + `); + + await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login"); + + await printReport(); + await browser.close(); +} catch (error) { + console.error("An error occurred:", error); + process.exit(1); +} diff --git a/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx b/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx deleted file mode 100644 index cc5a03ea..00000000 --- a/src/equicordplugins/betterActivities/components/ActivityTooltip.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import ErrorBoundary from "@components/ErrorBoundary"; -import { findComponentByCodeLazy } from "@webpack"; -import { moment, React, useMemo } from "@webpack/common"; -import { User } from "discord-types/general"; - -import { Activity, Application } from "../types"; -import { - formatElapsedTime, - getActivityImage, - getApplicationIcons, - getValidStartTimeStamp, - getValidTimestamps -} from "../utils"; - -const TimeBar = findComponentByCodeLazy<{ - start: number; - end: number; - themed: boolean; - className: string; -}>("isSingleLine"); - -interface ActivityTooltipProps { - activity: Activity; - application?: Application; - user: User; - cl: ReturnType; -} - -export default function ActivityTooltip({ activity, application, user, cl }: Readonly) { - const image = useMemo(() => { - const activityImage = getActivityImage(activity, application); - if (activityImage) { - return activityImage; - } - const icon = getApplicationIcons([activity], true)[0]; - return icon?.image.src; - }, [activity]); - const timestamps = useMemo(() => getValidTimestamps(activity), [activity]); - const startTime = useMemo(() => getValidStartTimeStamp(activity), [activity]); - - const hasDetails = activity.details ?? activity.state; - return ( - -
- {image && Activity logo} -
{activity.name}
- {hasDetails &&
} -
-
{activity.details}
-
{activity.state}
- {!timestamps && startTime && -
- {formatElapsedTime(moment(startTime / 1000), moment())} -
- } -
- {timestamps && ( - - )} -
- - ); -} diff --git a/src/equicordplugins/betterActivities/index.tsx b/src/equicordplugins/betterActivities/index.tsx index 4aaa5a65..429fb356 100644 --- a/src/equicordplugins/betterActivities/index.tsx +++ b/src/equicordplugins/betterActivities/index.tsx @@ -1,51 +1,234 @@ /* - * 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 . -*/ + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ import "./styles.css"; -import { migratePluginSettings } from "@api/Settings"; +import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { findComponentByCodeLazy } from "@webpack"; -import { PresenceStore, React, Tooltip, useStateFromStores } from "@webpack/common"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; +import { PresenceStore, React, Tooltip, useEffect, useMemo, useState, useStateFromStores } from "@webpack/common"; import { User } from "discord-types/general"; -import ActivityTooltip from "./components/ActivityTooltip"; import { Caret } from "./components/Caret"; import { SpotifyIcon } from "./components/SpotifyIcon"; import { TwitchIcon } from "./components/TwitchIcon"; -import settings from "./settings"; -import { Activity, ActivityListIcon, ActivityViewProps, ApplicationIcon, IconCSSProperties } from "./types"; -import { - getApplicationIcons -} from "./utils"; +import { Activity, ActivityListIcon, Application, ApplicationIcon, IconCSSProperties } from "./types"; + +const settings = definePluginSettings({ + memberList: { + type: OptionType.BOOLEAN, + description: "Show activity icons in the member list", + default: true, + restartNeeded: true, + }, + iconSize: { + type: OptionType.SLIDER, + description: "Size of the activity icons", + markers: [10, 15, 20], + default: 15, + stickToMarkers: false, + }, + specialFirst: { + type: OptionType.BOOLEAN, + description: "Show special activities first (Currently Spotify and Twitch)", + default: true, + restartNeeded: false, + }, + renderGifs: { + type: OptionType.BOOLEAN, + description: "Allow rendering GIFs", + default: true, + restartNeeded: false, + }, + showAppDescriptions: { + type: OptionType.BOOLEAN, + description: "Show application descriptions in the activity tooltip", + default: true, + restartNeeded: false, + }, + divider: { + type: OptionType.COMPONENT, + description: "", + component: () => ( +
+ ), + }, + userPopout: { + type: OptionType.BOOLEAN, + description: "Show all activities in the profile popout/sidebar", + default: true, + restartNeeded: true, + }, + allActivitiesStyle: { + type: OptionType.SELECT, + description: "Style for showing all activities", + options: [ + { + default: true, + label: "Carousel", + value: "carousel", + }, + { + label: "List", + value: "list", + }, + ] + } +}); const cl = classNameFactory("vc-bactivities-"); -const ActivityView = findComponentByCodeLazy(",onOpenGameProfileModal:"); +const ApplicationStore: { + getApplication: (id: string) => Application | null; +} = findStoreLazy("ApplicationStore"); + +const { fetchApplication }: { + fetchApplication: (id: string) => Promise; +} = findByPropsLazy("fetchApplication"); + +const ActivityView = findComponentByCodeLazy<{ + activity: Activity | null; + user: User; + application?: Application; + type?: string; +}>(",onOpenGameProfileModal:"); // if discord one day decides to change their icon this needs to be updated const DefaultActivityIcon = findComponentByCodeLazy("M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4 L8,5 Z M8,3 L2,3 L2,2 L8,2 L8,3 Z M8.88888889,0 L1.11111111,0 C0.494444444,0 0,0.494444444 0,1.11111111 L0,8.88888889 C0,9.50253861 0.497461389,10 1.11111111,10 L8.88888889,10 C9.50253861,10 10,9.50253861 10,8.88888889 L10,1.11111111 C10,0.494444444 9.5,0 8.88888889,0 Z"); +const fetchedApplications = new Map(); + +const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png"; // TODO: replace with "renderXboxImage"? + +const ActivityTooltip = ({ activity, application, user }: Readonly<{ activity: Activity, application?: Application, user: User; }>) => { + return ( + +
+ +
+
+ ); +}; + +function getActivityApplication({ application_id }: Activity) { + if (!application_id) return undefined; + let application = ApplicationStore.getApplication(application_id); + if (!application && fetchedApplications.has(application_id)) { + application = fetchedApplications.get(application_id) ?? null; + } + return application ?? undefined; +} + +function getApplicationIcons(activities: Activity[], preferSmall = false) { + const applicationIcons: ApplicationIcon[] = []; + const applications = activities.filter(activity => activity.application_id || activity.platform); + + for (const activity of applications) { + const { assets, application_id, platform } = activity; + if (!application_id && !platform) { + continue; + } + + if (assets) { + const addImage = (image: string, alt: string) => { + if (image.startsWith("mp:")) { + const discordMediaLink = `https://media.discordapp.net/${image.replace(/mp:/, "")}`; + if (settings.store.renderGifs || !discordMediaLink.endsWith(".gif")) { + applicationIcons.push({ + image: { src: discordMediaLink, alt }, + activity + }); + } + } else { + const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`; + applicationIcons.push({ + image: { src, alt }, + activity + }); + } + }; + + const smallImage = assets.small_image; + const smallText = assets.small_text ?? "Small Text"; + const largeImage = assets.large_image; + const largeText = assets.large_text ?? "Large Text"; + if (preferSmall) { + if (smallImage) { + addImage(smallImage, smallText); + } else if (largeImage) { + addImage(largeImage, largeText); + } + } else { + if (largeImage) { + addImage(largeImage, largeText); + } else if (smallImage) { + addImage(smallImage, smallText); + } + } + } else if (application_id) { + let application = ApplicationStore.getApplication(application_id); + if (!application) { + if (fetchedApplications.has(application_id)) { + application = fetchedApplications.get(application_id) as Application | null; + } else { + fetchedApplications.set(application_id, null); + fetchApplication(application_id).then(app => { + fetchedApplications.set(application_id, app); + }).catch(console.error); + } + } + + if (application) { + if (application.icon) { + const src = `https://cdn.discordapp.com/app-icons/${application.id}/${application.icon}.png`; + applicationIcons.push({ + image: { src, alt: application.name }, + activity, + application + }); + } else if (platform === "xbox") { + applicationIcons.push({ + image: { src: xboxUrl, alt: "Xbox" }, + activity, + application + }); + } + } else if (platform === "xbox") { + applicationIcons.push({ + image: { src: xboxUrl, alt: "Xbox" }, + activity + }); + } + } else if (platform === "xbox") { + applicationIcons.push({ + image: { src: xboxUrl, alt: "Xbox" }, + activity + }); + } + } + + return applicationIcons; +} migratePluginSettings("BetterActivities", "MemberListActivities"); + export default definePlugin({ name: "BetterActivities", description: "Shows activity icons in the member list and allows showing all activities", @@ -68,12 +251,7 @@ export default definePlugin({ for (const appIcon of uniqueIcons) { icons.push({ iconElement: , - tooltip: + tooltip: }); } } @@ -84,7 +262,7 @@ export default definePlugin({ const activity = activities[activityIndex]; const iconObject: ActivityListIcon = { iconElement: , - tooltip: + tooltip: }; if (settings.store.specialFirst) { @@ -131,8 +309,8 @@ export default definePlugin({ return null; }, - showAllActivitiesComponent({ activity, user, guild, channelId, onClose }: ActivityViewProps) { - const [currentActivity, setCurrentActivity] = React.useState( + showAllActivitiesComponent({ activity, user, ...props }: Readonly<{ activity: Activity; user: User; application: Application; type: string; }>) { + const [currentActivity, setCurrentActivity] = useState( activity?.type !== 4 ? activity! : null ); @@ -140,7 +318,7 @@ export default definePlugin({ [PresenceStore], () => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity.type !== 4) ) ?? []; - React.useEffect(() => { + useEffect(() => { if (!activities.length) { setCurrentActivity(null); return; @@ -148,75 +326,92 @@ export default definePlugin({ if (!currentActivity || !activities.includes(currentActivity)) setCurrentActivity(activities[0]); - }, [activities]); + // we use these for other activities, it would be better to somehow get the corresponding activity props + const generalProps = useMemo(() => Object.keys(props).reduce((acc, key) => { + // exclude activity specific props to prevent copying them to all activities (e.g. buttons) + if (key !== "renderActions" && key !== "application") acc[key] = props[key]; + return acc; + }, {}), [props]); + if (!activities.length) return null; if (settings.store.allActivitiesStyle === "carousel") { return (
- -
- {({ - onMouseEnter, - onMouseLeave - }) => { - return { - const index = activities.indexOf(currentActivity!); - if (index - 1 >= 0) - setCurrentActivity(activities[index - 1]); - }} - > - - ; - }} + {currentActivity?.id === activity?.id ? ( + + ) : ( + + )} + {activities.length > 1 && +
+ {({ + onMouseEnter, + onMouseLeave + }) => { + return { + const index = activities.indexOf(currentActivity!); + if (index - 1 >= 0) + setCurrentActivity(activities[index - 1]); + }} + > + + ; + }} -
- {activities.map((activity, index) => ( -
setCurrentActivity(activity)} - className={`dot ${currentActivity === activity ? "selected" : ""}`} /> - ))} +
+ {activities.map((activity, index) => ( +
setCurrentActivity(activity)} + className={`dot ${currentActivity === activity ? "selected" : ""}`} /> + ))} +
+ + {({ + onMouseEnter, + onMouseLeave + }) => { + return { + const index = activities.indexOf(currentActivity!); + if (index + 1 < activities.length) + setCurrentActivity(activities[index + 1]); + }} + > + = activities.length - 1} + direction="right" /> + ; + }}
- - {({ - onMouseEnter, - onMouseLeave - }) => { - return { - const index = activities.indexOf(currentActivity!); - if (index + 1 < activities.length) - setCurrentActivity(activities[index + 1]); - }} - > - = activities.length - 1} - direction="right" /> - ; - }} -
+ }
); } else { @@ -228,16 +423,22 @@ export default definePlugin({ gap: "5px", }} > - {activities.map((activity, index) => ( - - ))} + {activities.map((activity, index) => + index === 0 ? ( + ) : ( + + ))}
); } @@ -254,22 +455,13 @@ export default definePlugin({ predicate: () => settings.store.memberList, }, { - // Show all activities in the profile panel - find: "{layout:\"DM_PANEL\",", + // Show all activities in the user popout/sidebar + find: '"UserActivityContainer"', replacement: { - match: /(?<=\(0,\i\.jsx\)\()\i\.\i(?=,{activity:.+?,user:\i,channelId:\i.id,)/, - replace: "$self.showAllActivitiesComponent" - }, - predicate: () => settings.store.profileSidebar, - }, - { - // Show all activities in the user popout - find: "customStatusSection,", - replacement: { - match: /(?<=\(0,\i\.jsx\)\()\i\.\i(?=,{activity:\i,user:\i,guild:\i,channelId:\i,onClose:\i,)/, - replace: "$self.showAllActivitiesComponent" + match: /(?<=\(0,\i\.jsx\)\()(\i\.\i)(?=,{...(\i),activity:\i,user:\i,application:\i)/, + replace: "$2.type==='BiteSizePopout'?$self.showAllActivitiesComponent:$1" }, predicate: () => settings.store.userPopout - } + }, ], }); diff --git a/src/equicordplugins/betterActivities/settings.tsx b/src/equicordplugins/betterActivities/settings.tsx deleted file mode 100644 index 98f1b06e..00000000 --- a/src/equicordplugins/betterActivities/settings.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { OptionType } from "@utils/types"; -import { React } from "@webpack/common"; - -const settings = definePluginSettings({ - memberList: { - type: OptionType.BOOLEAN, - description: "Show activity icons in the member list", - default: true, - restartNeeded: true, - }, - iconSize: { - type: OptionType.SLIDER, - description: "Size of the activity icons", - markers: [10, 15, 20], - default: 15, - stickToMarkers: false, - }, - specialFirst: { - type: OptionType.BOOLEAN, - description: "Show special activities first (Currently Spotify and Twitch)", - default: true, - }, - renderGifs: { - type: OptionType.BOOLEAN, - description: "Allow rendering GIFs", - default: true, - }, - divider: { - type: OptionType.COMPONENT, - description: "", - component: () => ( -
- ), - }, - profileSidebar: { - type: OptionType.BOOLEAN, - description: "Show all activities in the profile sidebar", - default: true, - restartNeeded: true, - }, - userPopout: { - type: OptionType.BOOLEAN, - description: "Show all activities in the user popout", - default: true, - restartNeeded: true, - }, - allActivitiesStyle: { - type: OptionType.SELECT, - description: "Style for showing all activities", - options: [ - { - default: true, - label: "Carousel", - value: "carousel", - }, - { - label: "List", - value: "list", - }, - ] - } -}); - -export default settings; diff --git a/src/equicordplugins/betterActivities/styles.css b/src/equicordplugins/betterActivities/styles.css index 1990c173..1ab6d3ad 100644 --- a/src/equicordplugins/betterActivities/styles.css +++ b/src/equicordplugins/betterActivities/styles.css @@ -19,44 +19,8 @@ border-radius: 50%; } -.vc-bactivities-activity { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 5px; -} - -.vc-bactivities-activity-title { - font-weight: bold; - text-align: center; -} - -.vc-bactivities-activity-image { - height: 20px; - width: 20px; - border-radius: 50%; - object-fit: cover; -} - -.vc-bactivities-activity-divider { - width: 100%; - border-top: 1px dotted rgb(255 255 255 / 20%); - margin-top: 3px; - margin-bottom: 3px; -} - -.vc-bactivities-activity-details { - display: flex; - flex-direction: column; - color: var(--text-muted); - word-break: break-word; -} - -.vc-bactivities-activity-time-bar { - width: 100%; - margin-top: 3px; - margin-bottom: 3px; +.vc-bactivities-activity-tooltip { + padding: 1px; } .vc-bactivities-caret-left, @@ -101,12 +65,12 @@ background: var(--background-modifier-accent); } -.vc-bactivities-controls .carousell { +.vc-bactivities-controls .carousel { display: flex; align-items: center; } -.vc-bactivities-controls .carousell .dot { +.vc-bactivities-controls .carousel .dot { margin: 0 4px; width: 10px; cursor: pointer; @@ -117,11 +81,11 @@ opacity: 0.6; } -.vc-bactivities-controls .carousell .dot:hover:not(.selected) { +.vc-bactivities-controls .carousel .dot:hover:not(.selected) { opacity: 1; } -.vc-bactivities-controls .carousell .dot.selected { +.vc-bactivities-controls .carousel .dot.selected { opacity: 1; background: var(--dot-color, var(--brand-500)); } diff --git a/src/equicordplugins/betterActivities/types.ts b/src/equicordplugins/betterActivities/types.ts index 6ca4b533..e11896c8 100644 --- a/src/equicordplugins/betterActivities/types.ts +++ b/src/equicordplugins/betterActivities/types.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Guild, User } from "discord-types/general"; import { CSSProperties, ImgHTMLAttributes } from "react"; export interface Timestamp { @@ -81,11 +80,3 @@ export interface ActivityListIcon { export interface IconCSSProperties extends CSSProperties { "--icon-size": string; } - -export interface ActivityViewProps { - activity: Activity | null; - user: User; - guild: Guild; - channelId: string; - onClose: () => void; -} diff --git a/src/equicordplugins/betterActivities/utils.ts b/src/equicordplugins/betterActivities/utils.ts deleted file mode 100644 index d1511acc..00000000 --- a/src/equicordplugins/betterActivities/utils.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { findByPropsLazy, findStoreLazy } from "@webpack"; -import { moment } from "@webpack/common"; - -import settings from "./settings"; -import { Activity, Application, ApplicationIcon, Timestamp } from "./types"; - -const ApplicationStore: { - getApplication: (id: string) => Application | null; -} = findStoreLazy("ApplicationStore"); - -const { fetchApplication }: { - fetchApplication: (id: string) => Promise; -} = findByPropsLazy("fetchApplication"); - -export function getActivityImage(activity: Activity, application?: Application): string | undefined { - if (activity.type === 2 && activity.name === "Spotify") { - // get either from large or small image - const image = activity.assets?.large_image ?? activity.assets?.small_image; - // image needs to replace 'spotify:' - if (image?.startsWith("spotify:")) { - // spotify cover art is always https://i.scdn.co/image/ID - return image.replace("spotify:", "https://i.scdn.co/image/"); - } - } - if (activity.type === 1 && activity.name === "Twitch") { - const image = activity.assets?.large_image; - // image needs to replace 'twitch:' - if (image?.startsWith("twitch:")) { - // twitch images are always https://static-cdn.jtvnw.net/previews-ttv/live_user_USERNAME-RESOLTUON.jpg - return `${image.replace("twitch:", "https://static-cdn.jtvnw.net/previews-ttv/live_user_")}-108x60.jpg`; - } - } - // TODO: we could support other assets here -} - -const fetchedApplications = new Map(); - -// TODO: replace with "renderXboxImage"? -const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png"; - -export function getApplicationIcons(activities: Activity[], preferSmall = false) { - const applicationIcons: ApplicationIcon[] = []; - const applications = activities.filter(activity => activity.application_id || activity.platform); - - for (const activity of applications) { - const { assets, application_id, platform } = activity; - if (!application_id && !platform) { - continue; - } - if (assets) { - - const addImage = (image: string, alt: string) => { - if (image.startsWith("mp:")) { - const discordMediaLink = `https://media.discordapp.net/${image.replace(/mp:/, "")}`; - if (settings.store.renderGifs || !discordMediaLink.endsWith(".gif")) { - applicationIcons.push({ - image: { src: discordMediaLink, alt }, - activity - }); - } - } else { - const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`; - applicationIcons.push({ - image: { src, alt }, - activity - }); - } - }; - - const smallImage = assets.small_image; - const smallText = assets.small_text ?? "Small Text"; - const largeImage = assets.large_image; - const largeText = assets.large_text ?? "Large Text"; - if (preferSmall) { - if (smallImage) { - addImage(smallImage, smallText); - } else if (largeImage) { - addImage(largeImage, largeText); - } - } else { - if (largeImage) { - addImage(largeImage, largeText); - } else if (smallImage) { - addImage(smallImage, smallText); - } - } - } else if (application_id) { - let application = ApplicationStore.getApplication(application_id); - if (!application) { - if (fetchedApplications.has(application_id)) { - application = fetchedApplications.get(application_id) as Application | null; - } else { - fetchedApplications.set(application_id, null); - fetchApplication(application_id).then(app => { - fetchedApplications.set(application_id, app); - }); - } - } - - if (application) { - if (application.icon) { - const src = `https://cdn.discordapp.com/app-icons/${application.id}/${application.icon}.png`; - applicationIcons.push({ - image: { src, alt: application.name }, - activity, - application - }); - } else if (platform === "xbox") { - applicationIcons.push({ - image: { src: xboxUrl, alt: "Xbox" }, - activity, - application - }); - } - } - } else { - if (platform === "xbox") { - applicationIcons.push({ - image: { src: xboxUrl, alt: "Xbox" }, - activity - }); - } - } - } - - return applicationIcons; -} - -export function getValidTimestamps(activity: Activity): Required | null { - if (activity.timestamps?.start !== undefined && activity.timestamps?.end !== undefined) { - return activity.timestamps as Required; - } - return null; -} - -export function getValidStartTimeStamp(activity: Activity): number | null { - if (activity.timestamps?.start !== undefined) { - return activity.timestamps.start; - } - return null; -} - -const customFormat = (momentObj: moment.Moment): string => { - const hours = momentObj.hours(); - const formattedTime = momentObj.format("mm:ss"); - return hours > 0 ? `${momentObj.format("HH:")}${formattedTime}` : formattedTime; -}; - -export function formatElapsedTime(startTime: moment.Moment, endTime: moment.Moment): string { - const duration = moment.duration(endTime.diff(startTime)); - return `${customFormat(moment.utc(duration.asMilliseconds()))} elapsed`; -} diff --git a/src/equicordplugins/glide/index.tsx b/src/equicordplugins/glide/index.tsx index bb4a4fb4..2e0e4e6a 100644 --- a/src/equicordplugins/glide/index.tsx +++ b/src/equicordplugins/glide/index.tsx @@ -291,7 +291,7 @@ function getCSS(fontName) { ` : ""} /*Privacy blur*/ ${Settings.plugins.Glide.privacyBlur ? ` - .header_ec86aa, + .header_f9f2ca, .container_ee69e0, .title_a7d72e, .layout_f9647d, @@ -300,7 +300,7 @@ function getCSS(fontName) { transition: filter 0.2s ease-in-out; } - body:not(:hover) .header_ec86aa, + body:not(:hover) .header_f9f2ca, body:not(:hover) .container_ee69e0, body:not(:hover) .title_a7d72e, body:not(:hover) [aria-label="Members"], @@ -512,8 +512,16 @@ function getCSS(fontName) { { color: var(--mutedtext) !important } - - ${settings.store.pastelStatuses ? ` + .menu_d90b3d + { + background: var(--accent) !important; + } + .messageGroupWrapper_ac90a2, .header_ac90a2 + { + background-color: var(--primary); + } + ${settings.store.pastelStatuses ? + ` /*Pastel statuses*/ rect[fill='#23a55a'], svg[fill='#23a55a'] { fill: #80c968 !important; @@ -612,7 +620,7 @@ function getCSS(fontName) { } /*No more useless spotify activity header*/ - .headerContainer_d5089b + .headerContainer_c1d9fd { display: none; } @@ -634,7 +642,7 @@ function getCSS(fontName) { } /*Hide icon on file uploading status*/ - .icon_a4623d + .icon_b52bef { display: none; } @@ -655,12 +663,12 @@ function getCSS(fontName) { padding: 6px 8px !important; } /*Hide the icon that displays what platform the user is listening with on spotify status*/ - .platformIcon_d5089b + .platformIcon_c1d9fd { display: none !important; } /*hide the album name on spotify statuses (who cares)*/ - [class="state_d5089b ellipsis_d5089b textRow_d5089b"] + [class="state_c1d9fd ellipsis_c1d9fd textRow_c1d9fd"] { display: none; } @@ -782,5 +790,3 @@ export default definePlugin({ // preview thing, kinda low effort but eh settingsAboutComponent: () => }); - - diff --git a/src/equicordplugins/jumpscare/index.tsx b/src/equicordplugins/jumpscare/index.tsx new file mode 100644 index 00000000..bfddd14d --- /dev/null +++ b/src/equicordplugins/jumpscare/index.tsx @@ -0,0 +1,95 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { definePluginSettings } from "@api/Settings"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { FluxDispatcher, ReactDOM, useEffect, useState } from "@webpack/common"; +import { Root } from "react-dom/client"; + +let jumpscareRoot: Root | undefined; + +const settings = definePluginSettings({ + imageSource: { + type: OptionType.STRING, + description: "Sets the image url of the jumpscare", + default: "https://github.com/Equicord/Equibored/blob/main/misc/troll.gif?raw=true" + }, + audioSource: { + type: OptionType.STRING, + description: "Sets the audio url of the jumpscare", + default: "https://github.com/Equicord/Equibored/raw/main/misc/trollolol.mp3?raw=true" + }, + chance: { + type: OptionType.NUMBER, + description: "The chance of a jumpscare happening (1 in X so: 100 = 1/100 or 1%, 50 = 1/50 or 2%, etc.)", + default: 1000 + } +}); + +function getJumpscareRoot(): Root { + if (!jumpscareRoot) { + const element = document.createElement("div"); + element.id = "jumpscare-root"; + element.classList.add("jumpscare-root"); + document.body.append(element); + jumpscareRoot = ReactDOM.createRoot(element); + } + + return jumpscareRoot; +} + +export default definePlugin({ + name: "Jumpscare", + description: "Adds a configurable chance of jumpscaring you whenever you open a channel. Inspired by Geometry Dash Mega Hack", + authors: [Devs.surgedevs], + settings, + + start() { + getJumpscareRoot().render( + + ); + }, + + stop() { + jumpscareRoot?.unmount(); + jumpscareRoot = undefined; + }, + + JumpscareComponent() { + const [isPlaying, setIsPlaying] = useState(false); + + const audio = new Audio(settings.store.audioSource); + + const jumpscare = event => { + if (isPlaying) return; + + const chance = 1 / settings.store.chance; + if (Math.random() > chance) return; + + setIsPlaying(true); + audio.play(); + + console.log(isPlaying); + + setTimeout(() => { + setIsPlaying(false); + }, 1000); + }; + + useEffect(() => { + FluxDispatcher.subscribe("CHANNEL_SELECT", jumpscare); + + return () => { + FluxDispatcher.unsubscribe("CHANNEL_SELECT", jumpscare); + }; + }); + + return ; + } +}); diff --git a/src/equicordplugins/jumpscare/styles.css b/src/equicordplugins/jumpscare/styles.css new file mode 100644 index 00000000..3bfd50c3 --- /dev/null +++ b/src/equicordplugins/jumpscare/styles.css @@ -0,0 +1,33 @@ +.jumpscare-root { + pointer-events: none; +} + +.jumpscare-img { + position: absolute; + width: 100%; + height: 100%; + background-color: #000; + z-index: 99999; + object-fit: contain; + opacity: 0; +} + +.jumpscare-animate { + animation: jumpscare-animation 0.7s; +} + +@keyframes jumpscare-animation { + 0% { + transform: scale(0); + } + + 80% { + transform: scale(1); + opacity: 1; + } + + 100% { + transform: scale(0); + opacity: 0; + } +} diff --git a/src/equicordplugins/keywordNotify/index.tsx b/src/equicordplugins/keywordNotify/index.tsx index cadce33a..4f2ec84b 100644 --- a/src/equicordplugins/keywordNotify/index.tsx +++ b/src/equicordplugins/keywordNotify/index.tsx @@ -7,23 +7,37 @@ import "./style.css"; import { DataStore } from "@api/index"; -import { showNotification } from "@api/Notifications"; import { definePluginSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { Flex } from "@components/Flex"; import { DeleteIcon } from "@components/Icons"; import { EquicordDevs } from "@utils/constants"; import { Margins } from "@utils/margins"; +import { classes } from "@utils/misc"; import { useForceUpdater } from "@utils/react"; import definePlugin, { OptionType } from "@utils/types"; -import { Button, ChannelStore, Forms, NavigationRouter, Select, Switch, TextInput, useState } from "@webpack/common"; -import { Message } from "discord-types/general/index.js"; +import { findByCodeLazy, findByPropsLazy } from "@webpack"; +import { Button, ChannelStore, Forms, Select, SelectedChannelStore, Switch, TabBar, TextInput, Tooltip, UserStore, useState } from "@webpack/common"; +import { Message, User } from "discord-types/general/index.js"; +import type { PropsWithChildren } from "react"; + +type IconProps = JSX.IntrinsicElements["svg"]; type KeywordEntry = { regex: string, listIds: Array, listType: ListType, ignoreCase: boolean; }; let keywordEntries: Array = []; +let currentUser: User; +let keywordLog: Array = []; +const recentMentionsPopoutClass = findByPropsLazy("recentMentionsPopout"); +const tabClass = findByPropsLazy("tab"); +const buttonClass = findByPropsLazy("size36"); + +const MenuHeader = findByCodeLazy(".getMessageReminders()).length"); +const Popout = findByCodeLazy(".Messages.UNBLOCK_TO_JUMP_TITLE", "canCloseAllMessages:"); +const createMessageRecord = findByCodeLazy(".createFromServer(", ".isBlockedForMessage", "messageReference:"); const KEYWORD_ENTRIES_KEY = "KeywordNotify_keywordEntries"; +const KEYWORD_LOG_KEY = "KeywordNotify_log"; const cl = classNameFactory("vc-keywordnotify-"); @@ -52,6 +66,32 @@ enum ListType { Whitelist = "Whitelist" } +interface BaseIconProps extends IconProps { + viewBox: string; +} + +function highlightKeywords(str: string, entries: Array) { + let regexes: Array; + try { + regexes = entries.map(e => new RegExp(e.regex, "g" + (e.ignoreCase ? "i" : ""))); + } catch (err) { + return [str]; + } + + const matches = regexes.map(r => str.match(r)).flat().filter(e => e != null) as Array; + if (matches.length === 0) { + return [str]; + } + + const idx = str.indexOf(matches[0]); + + return [ + {str.substring(0, idx)}, + {matches[0]}, + {str.substring(idx + matches[0].length)} + ]; +} + function Collapsible({ title, children }) { const [isOpen, setIsOpen] = useState(false); @@ -115,7 +155,7 @@ function ListedIds({ listIds, setListIds }) { ); } -function ListTypeSelector({ listType, setListType }) { +function ListTypeSelector({ listType, setListType }: { listType: ListType, setListType: (v: ListType) => void; }) { return (