Merge branch 'dev' into Combats-Corner

This commit is contained in:
thororen1234 2024-08-16 01:04:15 -04:00
commit 92fccecdc2
48 changed files with 1755 additions and 1275 deletions

View file

@ -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"

View file

@ -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:

View file

@ -8,7 +8,6 @@ on:
jobs:
TestPlugins:
name: Test Patches
if: github.repository == 'Equicord/Equicord'
runs-on: ubuntu-latest
steps:

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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

View file

@ -23,7 +23,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
- Request for plugins from Discord.
<details>
<summary>Extra included plugins (119 additional plugins)</summary>
<summary>Extra included plugins (122 additional plugins)</summary>
- 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

View file

@ -36,7 +36,7 @@ const commonOptions: esbuild.BuildOptions = {
external: ["~plugins", "~git-hash", "/assets/*"],
plugins: [
globPlugins("web"),
...commonOpts.plugins,
...commonOpts.plugins
],
target: ["esnext"],
define: {

View file

@ -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<string | undefined> {
return await (handle as JSHandle<Error>)?.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<string>;
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<string | undefined> {
return await (handle as JSHandle<Error>)?.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<string>;
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);
}

View file

@ -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<typeof import("@api/Styles").classNameFactory>;
}
export default function ActivityTooltip({ activity, application, user, cl }: Readonly<ActivityTooltipProps>) {
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 (
<ErrorBoundary>
<div className={cl("activity")}>
{image && <img className={cl("activity-image")} src={image} alt="Activity logo" />}
<div className={cl("activity-title")}>{activity.name}</div>
{hasDetails && <div className={cl("activity-divider")} />}
<div className={cl("activity-details")}>
<div>{activity.details}</div>
<div>{activity.state}</div>
{!timestamps && startTime &&
<div className={cl("activity-time-bar")}>
{formatElapsedTime(moment(startTime / 1000), moment())}
</div>
}
</div>
{timestamps && (
<TimeBar start={timestamps.start}
end={timestamps.end}
themed={false}
className={cl("activity-time-bar")}
/>
)}
</div>
</ErrorBoundary>
);
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
* 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: () => (
<div style={{
width: "100%",
height: 1,
borderTop: "thin solid var(--background-modifier-accent)",
paddingTop: 5,
paddingBottom: 5
}} />
),
},
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<ActivityViewProps>(",onOpenGameProfileModal:");
const ApplicationStore: {
getApplication: (id: string) => Application | null;
} = findStoreLazy("ApplicationStore");
const { fetchApplication }: {
fetchApplication: (id: string) => Promise<Application | null>;
} = 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<string, Application | null>();
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 (
<ErrorBoundary>
<div className={cl("activity-tooltip")}>
<ActivityView
activity={activity}
user={user}
application={application}
type="BiteSizePopout"
/>
</div>
</ErrorBoundary>
);
};
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: <img {...appIcon.image} />,
tooltip: <ActivityTooltip
activity={appIcon.activity}
application={appIcon.application}
user={user}
cl={cl}
/>
tooltip: <ActivityTooltip activity={appIcon.activity} application={appIcon.application} user={user} />
});
}
}
@ -84,7 +262,7 @@ export default definePlugin({
const activity = activities[activityIndex];
const iconObject: ActivityListIcon = {
iconElement: <IconComponent />,
tooltip: <ActivityTooltip activity={activity} user={user} cl={cl} />
tooltip: <ActivityTooltip activity={activity} user={user} />
};
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<Activity | null>(
showAllActivitiesComponent({ activity, user, ...props }: Readonly<{ activity: Activity; user: User; application: Application; type: string; }>) {
const [currentActivity, setCurrentActivity] = useState<Activity | null>(
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 (
<div style={{ display: "flex", flexDirection: "column" }}>
<ActivityView
activity={currentActivity}
user={user}
guild={guild}
channelId={channelId}
onClose={onClose} />
<div
className={cl("controls")}
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Tooltip text="Left" tooltipClassName={cl("controls-tooltip")}>{({
onMouseEnter,
onMouseLeave
}) => {
return <span
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={() => {
const index = activities.indexOf(currentActivity!);
if (index - 1 >= 0)
setCurrentActivity(activities[index - 1]);
}}
>
<Caret
disabled={activities.indexOf(currentActivity!) < 1}
direction="left" />
</span>;
}}</Tooltip>
{currentActivity?.id === activity?.id ? (
<ActivityView
activity={currentActivity}
user={user}
{...props}
/>
) : (
<ActivityView
activity={currentActivity}
user={user}
// fetch optional application
application={getActivityApplication(currentActivity!)}
{...generalProps}
/>
)}
{activities.length > 1 &&
<div
className={cl("controls")}
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Tooltip text="Left" tooltipClassName={cl("controls-tooltip")}>{({
onMouseEnter,
onMouseLeave
}) => {
return <span
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={() => {
const index = activities.indexOf(currentActivity!);
if (index - 1 >= 0)
setCurrentActivity(activities[index - 1]);
}}
>
<Caret
disabled={activities.indexOf(currentActivity!) < 1}
direction="left" />
</span>;
}}</Tooltip>
<div className="carousell">
{activities.map((activity, index) => (
<div
key={"dot--" + index}
onClick={() => setCurrentActivity(activity)}
className={`dot ${currentActivity === activity ? "selected" : ""}`} />
))}
<div className="carousel">
{activities.map((activity, index) => (
<div
key={"dot--" + index}
onClick={() => setCurrentActivity(activity)}
className={`dot ${currentActivity === activity ? "selected" : ""}`} />
))}
</div>
<Tooltip text="Right" tooltipClassName={cl("controls-tooltip")}>{({
onMouseEnter,
onMouseLeave
}) => {
return <span
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={() => {
const index = activities.indexOf(currentActivity!);
if (index + 1 < activities.length)
setCurrentActivity(activities[index + 1]);
}}
>
<Caret
disabled={activities.indexOf(currentActivity!) >= activities.length - 1}
direction="right" />
</span>;
}}</Tooltip>
</div>
<Tooltip text="Right" tooltipClassName={cl("controls-tooltip")}>{({
onMouseEnter,
onMouseLeave
}) => {
return <span
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={() => {
const index = activities.indexOf(currentActivity!);
if (index + 1 < activities.length)
setCurrentActivity(activities[index + 1]);
}}
>
<Caret
disabled={activities.indexOf(currentActivity!) >= activities.length - 1}
direction="right" />
</span>;
}}</Tooltip>
</div>
}
</div>
);
} else {
@ -228,16 +423,22 @@ export default definePlugin({
gap: "5px",
}}
>
{activities.map((activity, index) => (
<ActivityView
key={index}
activity={activity}
user={user}
guild={guild}
channelId={channelId}
onClose={onClose}
/>
))}
{activities.map((activity, index) =>
index === 0 ? (
<ActivityView
key={index}
activity={activity}
user={user}
{...props}
/>) : (
<ActivityView
key={index}
activity={activity}
user={user}
application={getActivityApplication(activity)}
{...generalProps}
/>
))}
</div>
);
}
@ -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
}
},
],
});

View file

@ -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: () => (
<div style={{
width: "100%",
height: 1,
borderTop: "thin solid var(--background-modifier-accent)",
paddingTop: 5,
paddingBottom: 5
}} />
),
},
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;

View file

@ -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));
}

View file

@ -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;
}

View file

@ -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<Application | null>;
} = 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<string, Application | null>();
// 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<Timestamp> | null {
if (activity.timestamps?.start !== undefined && activity.timestamps?.end !== undefined) {
return activity.timestamps as Required<Timestamp>;
}
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`;
}

View file

@ -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: () => <img src="https://files.catbox.moe/j8y2gt.webp" width="568px" border-radius="30px" ></img>
});

View file

@ -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(
<this.JumpscareComponent />
);
},
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 <img className={`jumpscare-img ${isPlaying ? "jumpscare-animate" : ""}`} src={settings.store.imageSource} />;
}
});

View file

@ -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;
}
}

View file

@ -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<string>, listType: ListType, ignoreCase: boolean; };
let keywordEntries: Array<KeywordEntry> = [];
let currentUser: User;
let keywordLog: Array<any> = [];
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<KeywordEntry>) {
let regexes: Array<RegExp>;
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<string>;
if (matches.length === 0) {
return [str];
}
const idx = str.indexOf(matches[0]);
return [
<span>{str.substring(0, idx)}</span>,
<span className="highlight">{matches[0]}</span>,
<span>{str.substring(idx + matches[0].length)}</span>
];
}
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 (
<Select
options={[
@ -185,7 +225,6 @@ function KeywordEntries() {
>
Ignore Case
</Switch>
<Forms.FormDivider className={[Margins.top8, Margins.bottom8].join(" ")} />
<Forms.FormTitle tag="h5">Whitelist/Blacklist</Forms.FormTitle>
<Flex flexDirection="row">
<div style={{ flexGrow: 1 }}>
@ -215,22 +254,60 @@ function KeywordEntries() {
);
}
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
return (
<svg
className={classes(className, "vc-icon")}
role="img"
width={width}
height={height}
viewBox={viewBox}
{...svgProps}
>
{children}
</svg>
);
}
// Ideally I would just add this to Icons.tsx, but I cannot as this is a user-plugin :/
function DoubleCheckmarkIcon(props: IconProps) {
return (
<Icon
{...props}
className={classes(props.className, "vc-double-checkmark-icon")}
viewBox="0 0 24 24"
>
<path fill="currentColor"
d="M16.7 8.7a1 1 0 0 0-1.4-1.4l-3.26 3.24a1 1 0 0 0 1.42 1.42L16.7 8.7ZM3.7 11.3a1 1 0 0 0-1.4 1.4l4.5 4.5a1 1 0 0 0 1.4-1.4l-4.5-4.5Z"
/>
<path fill="currentColor"
d="M21.7 9.7a1 1 0 0 0-1.4-1.4L13 15.58l-3.3-3.3a1 1 0 0 0-1.4 1.42l4 4a1 1 0 0 0 1.4 0l8-8Z"
/>
</Icon>
);
}
const settings = definePluginSettings({
ignoreBots: {
type: OptionType.BOOLEAN,
description: "Ignore messages from bots",
default: true
},
amountToKeep: {
type: OptionType.NUMBER,
description: "Amount of messages to keep in the log",
default: 50
},
keywords: {
type: OptionType.COMPONENT,
description: "Keywords to detect",
description: "Manage keywords",
component: () => <KeywordEntries />
}
});
export default definePlugin({
name: "KeywordNotify",
authors: [EquicordDevs.camila314, EquicordDevs.thororen],
authors: [EquicordDevs.camila314, EquicordDevs.x3rt, EquicordDevs.thororen],
description: "Sends a notification if a given message matches certain keywords or regexes",
settings,
patches: [
@ -240,20 +317,53 @@ export default definePlugin({
match: /}_dispatch\((\i),\i\){/,
replace: "$&$1=$self.modify($1);"
}
},
{
find: "Messages.UNREADS_TAB_LABEL}",
replacement: {
match: /\i\?\(0,\i\.jsxs\)\(\i\.TabBar\.Item/,
replace: "$self.keywordTabBar(),$&"
}
},
{
find: "location:\"RecentsPopout\"})",
replacement: {
match: /:(\i)===\i\.\i\.MENTIONS\?\(0,.+?setTab:(\i),onJump:(\i),badgeState:\i,closePopout:(\i)/,
replace: ": $1 === 8 ? $self.tryKeywordMenu($2, $3, $4) $&"
}
},
{
find: ".guildFilter:null",
replacement: {
match: /function (\i)\(\i\){let{message:\i,gotoMessage/,
replace: "$self.renderMsg = $1; $&"
}
},
{
find: ".guildFilter:null",
replacement: {
match: /onClick:\(\)=>(\i\.\i\.deleteRecentMention\((\i)\.id\))/,
replace: "onClick: () => $2._keyword ? $self.deleteKeyword($2.id) : $1"
}
}
],
async start() {
this.onUpdate = () => null;
currentUser = UserStore.getCurrentUser();
keywordEntries = await DataStore.get(KEYWORD_ENTRIES_KEY) ?? [];
await DataStore.set(KEYWORD_ENTRIES_KEY, keywordEntries);
(await DataStore.get(KEYWORD_LOG_KEY) ?? []).map(e => JSON.parse(e)).forEach(e => {
this.addToLog(e);
});
},
applyKeywordEntries(m: Message) {
let matches = false;
let match = "";
for (const entry of keywordEntries) {
if (entry.regex === "") {
return;
continue;
}
let listed = entry.listIds.some(id => id === m.channel_id || id === m.author.id);
@ -267,31 +377,30 @@ export default definePlugin({
const whitelistMode = entry.listType === ListType.Whitelist;
if (!whitelistMode && listed) {
return;
continue;
}
if (whitelistMode && !listed) {
return;
continue;
}
if (settings.store.ignoreBots && m.author.bot && (!whitelistMode || !entry.listIds.includes(m.author.id))) {
return;
continue;
}
const flags = entry.ignoreCase ? "i" : "";
if (safeMatchesRegex(m.content, entry.regex, flags)) {
matches = true;
match = m.content;
}
for (const embed of m.embeds as any) {
if (safeMatchesRegex(embed.description, entry.regex, flags) || safeMatchesRegex(embed.title, entry.regex, flags)) {
matches = true;
match = m.content;
} else if (embed.fields != null) {
for (const field of embed.fields as Array<{ name: string, value: string; }>) {
if (safeMatchesRegex(field.value, entry.regex, flags) || safeMatchesRegex(field.name, entry.regex, flags)) {
matches = true;
match = m.content;
} else {
for (const embed of m.embeds as any) {
if (safeMatchesRegex(embed.description, entry.regex, flags) || safeMatchesRegex(embed.title, entry.regex, flags)) {
matches = true;
break;
} else if (embed.fields != null) {
for (const field of embed.fields as Array<{ name: string, value: string; }>) {
if (safeMatchesRegex(field.value, entry.regex, flags) || safeMatchesRegex(field.name, entry.regex, flags)) {
matches = true;
break;
}
}
}
}
@ -299,14 +408,112 @@ export default definePlugin({
}
if (matches) {
showNotification({
title: "Keyword Notify",
body: `${m.author.username} matched the keyword ${match}`,
onClick: () => NavigationRouter.transitionTo(`/channels/${ChannelStore.getChannel(m.channel_id)?.guild_id ?? "@me"}/${m.channel_id}${m.id ? "/" + m.id : ""}`)
});
// @ts-ignore
m.mentions.push({ id: currentUser.id });
if (m.author.id !== currentUser.id)
this.addToLog(m);
}
},
addToLog(m: Message) {
if (m == null || keywordLog.some(e => e.id === m.id))
return;
DataStore.get(KEYWORD_LOG_KEY).then(log => {
DataStore.set(KEYWORD_LOG_KEY, [...log, JSON.stringify(m)]);
});
const thing = createMessageRecord(m);
keywordLog.push(thing);
keywordLog.sort((a, b) => b.timestamp - a.timestamp);
if (keywordLog.length > settings.store.amountToKeep)
keywordLog.pop();
this.onUpdate();
},
deleteKeyword(id) {
keywordLog = keywordLog.filter(e => e.id !== id);
this.onUpdate();
},
keywordTabBar() {
return (
<TabBar.Item className={classes(tabClass.tab, tabClass.expanded)} id={8}>
Keywords
</TabBar.Item>
);
},
tryKeywordMenu(setTab, onJump, closePopout) {
const header = (
<MenuHeader tab={8} setTab={setTab} closePopout={closePopout} badgeState={{ badgeForYou: false }} children={
<Tooltip text="Clear All">
{({ onMouseLeave, onMouseEnter }) => (
<Button
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
look={Button.Looks.BLANK}
size={Button.Sizes.ICON}
onClick={() => {
keywordLog = [];
DataStore.set(KEYWORD_LOG_KEY, []);
this.onUpdate();
}}>
<div className={classes(buttonClass.button, buttonClass.secondary, buttonClass.size32)}>
<DoubleCheckmarkIcon />
</div>
</Button>
)}
</Tooltip>
} />
);
const channel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
const [tempLogs, setKeywordLog] = useState(keywordLog);
this.onUpdate = () => {
const newLog = Array.from(keywordLog);
setKeywordLog(newLog);
};
const messageRender = (e, t) => {
e._keyword = true;
e.customRenderedContent = {
content: highlightKeywords(e.content, keywordEntries)
};
const msg = this.renderMsg({
message: e,
gotoMessage: t,
dismissible: true
});
return [msg];
};
return (
<>
<Popout
className={classes(recentMentionsPopoutClass.recentMentionsPopout)}
renderHeader={() => header}
renderMessage={messageRender}
channel={channel}
onJump={onJump}
onFetch={() => null}
onCloseMessage={this.deleteKeyword}
loadMore={() => null}
messages={tempLogs}
renderEmptyState={() => null}
canCloseAllMessages={true}
/>
</>
);
},
modify(e) {
if (e.type === "MESSAGE_CREATE") {
this.applyKeywordEntries(e.message);

View file

@ -12,7 +12,7 @@ import { Button, Forms, i18n, Menu, TabBar } from "@webpack/common";
import { ReactElement } from "react";
import { preload, unload } from "./images";
import { cl, QrCodeCameraIcon } from "./ui";
import { cl, QrCodeIcon } from "./ui";
import openQrModal from "./ui/modals/QrModal";
export default definePlugin({
@ -106,7 +106,7 @@ export default definePlugin({
<Menu.MenuItem
id="scan-qr"
label={i18n.Messages.USER_SETTINGS_SCAN_QR_CODE}
icon={QrCodeCameraIcon}
icon={QrCodeIcon}
action={openQrModal}
showIconFirst
focusedClassName={menuItemFocused}

View file

@ -37,11 +37,10 @@ export const { Spinner } = proxyLazy(() => Forms as any as {
SpinnerTypes: typeof SpinnerTypes;
});
export const { QrCodeCameraIcon } = findByPropsLazy("QrCodeCameraIcon") as {
QrCodeCameraIcon: ComponentType<{
size: number;
}>;
};
const icons = findByPropsLazy("PencilIcon");
export const QrCodeIcon = proxyLazy(() => icons.QrCodeCameraIcon ?? icons.QrCodeIcon) as ComponentType<{
size: number;
}>;
export const cl = classNameFactory("qrlogin-");

View file

@ -0,0 +1,21 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
function SpeedIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 -960 960 960"
>
<path d="M418-340q24 24 62 23.5t56-27.5l224-336-336 224q-27 18-28.5 55t22.5 61zm62-460q59 0 113.5 16.5T696-734l-76 48q-33-17-68.5-25.5T480-720q-133 0-226.5 93.5T160-400q0 42 11.5 83t32.5 77h552q23-38 33.5-79t10.5-85q0-36-8.5-70T766-540l48-76q30 47 47.5 100T880-406q1 57-13 109t-41 99q-11 18-30 28t-40 10H204q-21 0-40-10t-30-28q-26-45-40-95.5T80-400q0-83 31.5-155.5t86-127Q252-737 325-768.5T480-800zm7 313z"></path>
</svg>
);
}
export default SpeedIcon;

View file

@ -0,0 +1,151 @@
/*
* 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 { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { ContextMenuApi, FluxDispatcher, Heading, Menu, React, Tooltip, useEffect } from "@webpack/common";
import { RefObject } from "react";
import SpeedIcon from "./components/SpeedIcon";
const cl = classNameFactory("vc-media-playback-speed-");
const min = 0.25;
const max = 3.5;
const speeds = makeRange(min, max, 0.25);
const settings = definePluginSettings({
test: {
type: OptionType.COMPONENT,
description: "",
component() {
return <Heading variant="heading-lg/bold" selectable={false}>
Default playback speeds
</Heading>;
}
},
defaultVoiceMessageSpeed: {
type: OptionType.SLIDER,
default: 1,
description: "Voice messages",
markers: speeds,
},
defaultVideoSpeed: {
type: OptionType.SLIDER,
default: 1,
description: "Videos",
markers: speeds,
},
defaultAudioSpeed: {
type: OptionType.SLIDER,
default: 1,
description: "Audios",
markers: speeds,
},
});
type MediaRef = RefObject<HTMLMediaElement> | undefined;
export default definePlugin({
name: "MediaPlaybackSpeed",
description: "Allows changing the (default) playback speed of media embeds",
authors: [Devs.D3SOX],
settings,
PlaybackSpeedComponent({ mediaRef }: { mediaRef: MediaRef; }) {
const changeSpeed = (speed: number) => {
const media = mediaRef?.current;
if (media) {
media.playbackRate = speed;
}
};
useEffect(() => {
if (!mediaRef?.current) return;
const media = mediaRef.current;
if (media.tagName === "AUDIO") {
const isVoiceMessage = media.className.includes("audioElement_");
changeSpeed(isVoiceMessage ? settings.store.defaultVoiceMessageSpeed : settings.store.defaultAudioSpeed);
} else if (media.tagName === "VIDEO") {
changeSpeed(settings.store.defaultVideoSpeed);
}
}, [mediaRef]);
return (
<Tooltip text="Playback speed">
{tooltipProps => (
<button
{...tooltipProps}
className={cl("icon")}
onClick={e => {
ContextMenuApi.openContextMenu(e, () =>
<Menu.Menu
navId="vc-playback-speed"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label="Playback speed control"
>
<Menu.MenuGroup
label="Playback speed"
>
{speeds.map(speed => (
<Menu.MenuItem
key={speed}
id={"speed-" + speed}
label={`${speed}x`}
action={() => changeSpeed(speed)}
/>
))}
</Menu.MenuGroup>
</Menu.Menu>
);
}}>
<SpeedIcon />
</button>
)}
</Tooltip>
);
},
renderComponent(mediaRef: MediaRef) {
return <ErrorBoundary noop>
<this.PlaybackSpeedComponent mediaRef={mediaRef} />
</ErrorBoundary>;
},
patches: [
// voice message embeds
{
find: "\"--:--\"",
replacement: {
match: /onVolumeShow:\i,onVolumeHide:\i\}\)(?<=useCallback\(\(\)=>\{let \i=(\i).current;.+?)/,
replace: "$&,$self.renderComponent($1)"
}
},
// audio & video embeds
{
// need to pass media ref via props to make it easily accessible from inside controls
find: "renderControls(){",
replacement: {
match: /onToggleMuted:this.toggleMuted,/,
replace: "$&mediaRef:this.mediaRef,"
}
},
{
find: "AUDIO:\"AUDIO\"",
replacement: {
match: /onVolumeHide:\i,iconClassName:\i.controlIcon,iconColor:"currentColor",sliderWrapperClassName:\i.volumeSliderWrapper\}\)\}\),/,
replace: "$&$self.renderComponent(this.props.mediaRef),"
}
}
]
});

View file

@ -0,0 +1,10 @@
.vc-media-playback-speed-icon {
background-color: transparent;
height: 100%;
z-index: 2;
color: var(--interactive-normal);
}
.vc-media-playback-speed-icon:hover {
color: var(--interactive-active);
}

View file

@ -0,0 +1,63 @@
/*
* 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";
export const enum RenderType {
BLOCK,
FOREGROUND,
BACKGROUND,
}
export const settings = definePluginSettings({
renderType: {
type: OptionType.SELECT,
description: "How to render colors",
options: [
{
label: "Text color",
value: RenderType.FOREGROUND,
default: true,
},
{
label: "Block nearby",
value: RenderType.BLOCK,
},
{
label: "Background color",
value: RenderType.BACKGROUND
},
]
}
});
export const enum ColorType {
RGB,
RGBA,
HEX,
HSL
}
// It's sooo hard to read regex without this, it makes it at least somewhat bearable
export const replaceRegexp = (reg: string) => {
const n = new RegExp(reg
// \c - 'comma'
// \v - 'value'
// \f - 'float'
.replaceAll("\\f", "[+-]?([0-9]*[.])?[0-9]+")
.replaceAll("\\c", "(?:,|\\s)")
.replaceAll("\\v", "\\s*?\\d+?\\s*?"), "g");
return n;
};
export const regex = [
{ reg: /rgb\(\v\c\v\c\v\)/g, type: ColorType.RGB },
{ reg: /rgba\(\v\c\v\c\v(\c|\/?)\s*\f\)/g, type: ColorType.RGBA },
{ reg: /hsl\(\v°?\c\s*?\d+%?\s*?\c\s*?\d+%?\s*?\)/g, type: ColorType.HSL },
{ reg: /#(?:[0-9a-fA-F]{3}){1,2}/g, type: ColorType.HEX }
].map(v => { v.reg = replaceRegexp(v.reg.source); return v; });

View file

@ -0,0 +1,203 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
import { React } from "@webpack/common";
import {
ColorType,
regex,
RenderType,
replaceRegexp,
settings,
} from "./constants";
const source = regex.map(r => r.reg.source).join("|");
const matchAllRegExp = new RegExp(`^(${source})`, "i");
interface ParsedColorInfo {
type: "color";
color: string;
colorType: ColorType;
text: string;
}
const requiredFirstCharacters = ["r", "h", "#"].flatMap(v => [
v,
v.toUpperCase(),
]);
export default definePlugin({
authors: [EquicordDevs.Hen],
name: "MessageColors",
description: "Displays color codes like #FF0042 inside of messages",
settings,
patches: [
// Create a new markdown rule, so it parses just like any other features
// Like bolding, spoilers, mentions, etc
{
find: "roleMention:{order:",
group: true,
replacement: {
match: /roleMention:\{order:(\i\.\i\.order)/,
replace: "color:$self.getColor($1),$&",
},
},
// Changes text md rule regex, so it stops right before hsl( | rgb(
// Without it discord will try to pass a string without those to color rule
{
find: ".defaultRules.text,match:",
group: true,
replacement: {
// $)/)
match: /\$\)\/\)}/,
// hsl(|rgb(|$&
replace: requiredFirstCharacters.join("|") + "|$&",
},
},
// Discord just requires it to be here
// Or it explodes (bad)
{
find: "Unknown markdown rule:",
group: true,
replacement: {
match: /roleMention:{type:/,
replace: 'color:{type:"inlineObject"},$&',
},
},
],
getColor(order: number) {
return {
order,
// Don't even try to match if the message chunk doesn't start with...
requiredFirstCharacters,
// Match -> Parse -> React
// Result of previous action is dropped as a first argument of the next one
match(content: string) {
return matchAllRegExp.exec(content);
},
parse(
matchedContent: RegExpExecArray,
_,
parseProps: Record<string, any>,
): ParsedColorInfo | { type: "text"; content: string; } {
// This check makes sure that it doesn't try to parse color
// When typing/editing message
//
// Discord doesn't know how to deal with color and crashes
if (!parseProps.messageId)
return {
type: "text",
content: matchedContent[0],
};
const content = matchedContent[0];
try {
const type = getColorType(content);
return {
type: "color",
colorType: type,
color: parseColor(content, type),
text: content,
};
} catch (e) {
console.error(e);
return {
type: "text",
content: matchedContent[0],
};
}
},
// react(args: ReturnType<typeof this.parse>)
react({ text, colorType, color }: ParsedColorInfo) {
if (settings.store.renderType === RenderType.FOREGROUND) {
return <span style={{ color: color }}>{text}</span>;
}
const styles = {
"--color": color,
} as React.CSSProperties;
if (settings.store.renderType === RenderType.BACKGROUND) {
const isDark = isColorDark(color, colorType);
const className = `vc-color-bg ${!isDark ? "vc-color-bg-invert" : ""}`;
return (
<span className={className} style={styles}>
{text}
</span>
);
}
return (
<>
{text}
<span className="vc-color-block" style={styles}></span>
</>
);
},
};
},
});
// https://en.wikipedia.org/wiki/Relative_luminance
const calcRGBLightness = (r: number, g: number, b: number) => {
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const isColorDark = (color: string, type: ColorType): boolean => {
switch (type) {
case ColorType.RGBA:
case ColorType.RGB: {
const match = color.match(/\d+/g)!;
const lightness = calcRGBLightness(+match[0], +match[1], +match[2]);
return lightness < 140;
}
case ColorType.HEX: {
var rgb = parseInt(color.substring(1), 16);
const r = (rgb >> 16) & 0xff;
const g = (rgb >> 8) & 0xff;
const b = (rgb >> 0) & 0xff;
const lightness = calcRGBLightness(r, g, b);
return lightness < 140;
}
case ColorType.HSL: {
const match = color.match(/\d+/g)!;
const lightness = +match[2];
return lightness < 50;
}
}
};
const getColorType = (color: string): ColorType => {
color = color.toLowerCase().trim();
if (color.startsWith("#")) return ColorType.HEX;
if (color.startsWith("hsl")) return ColorType.HSL;
if (color.startsWith("rgba")) return ColorType.RGBA;
if (color.startsWith("rgb")) return ColorType.RGB;
throw new Error(`Can't resolve color type of ${color}`);
};
function parseColor(str: string, type: ColorType): string {
str = str
.toLowerCase()
.trim()
.replaceAll(/(\s|,)+/g, " ");
switch (type) {
case ColorType.RGB:
return str;
case ColorType.RGBA:
if (!str.includes("/"))
return str.replaceAll(replaceRegexp(/\f(?=\s*?\))/.source), "/$&");
return str;
case ColorType.HEX:
return str[0] === "#" ? str : `#${str}`;
case ColorType.HSL:
return str.replace("°", "");
}
}

View file

@ -0,0 +1,20 @@
.vc-color-block {
aspect-ratio: 1/1;
background: var(--color);
height: 1rem;
vertical-align: middle;
border-radius: 4px;
display: inline-block;
user-select: none;
margin-left: 2px;
}
.vc-color-bg {
background: var(--color);
}
/* Light color in dark theme */
.theme-dark .vc-color-bg.vc-color-bg-invert,
.theme-light .vc-color-bg:not(.vc-color-bg-invert) {
color: var(--background-tertiary);
}

View file

@ -0,0 +1,41 @@
/*
* 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: "NoMirroredCamera",
description: "Prevents the camera from being mirrored on your screen",
authors: [Devs.nyx],
patches: [
// When focused on voice channel or group chat voice call
{
find: /\i\?\i.\i.SELF_VIDEO/,
replacement: {
match: /mirror:\i/,
replace: "mirror:!1"
}
},
// Popout camera when not focused on voice channel
{
find: ".mirror]:",
replacement: {
match: /\[(\i).mirror]:\i/,
replace: "[$1.mirror]:!1"
}
},
// Overriding css on Preview Camera/Change Video Background popup
{
find: ".cameraPreview,",
replacement: {
match: /className:\i.camera,/,
replace: "$&style:{transform: \"scalex(1)\"},"
}
}
]
});

View file

@ -1,11 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { SVGProps } from "react";
export function NotificationsOffIcon(props: SVGProps<SVGSVGElement>) {
return (<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}><path fill="currentColor" d="M20 18.69L7.84 6.14L5.27 3.49L4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72zM12 22c1.11 0 2-.89 2-2h-4c0 1.11.89 2 2 2m6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12c-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01c-.23.09-.46.2-.68.31c0 0-.01 0-.01.01z"></path></svg>);
}

View file

@ -1,11 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { SVGProps } from "react";
export function NotificationsOnIcon(props: SVGProps<SVGSVGElement>) {
return (<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}><path fill="currentColor" d="M10 20h4c0 1.1-.9 2-2 2s-2-.9-2-2m4-11c0 2.61 1.67 4.83 4 5.66V17h2v2H4v-2h2v-7c0-2.79 1.91-5.14 4.5-5.8v-.7c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v.7c.71.18 1.36.49 1.95.9A5.902 5.902 0 0 0 14 9m10-1h-3V5h-2v3h-3v2h3v3h2v-3h3z"></path></svg>);
}

View file

@ -1,344 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { showNotification } from "@api/Notifications";
import { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { Menu, PresenceStore, React, SelectedChannelStore, Tooltip, UserStore } from "@webpack/common";
import type { Channel, User } from "discord-types/general";
import { CSSProperties } from "react";
import { NotificationsOffIcon } from "./components/NotificationsOffIcon";
import { NotificationsOnIcon } from "./components/NotificationsOnIcon";
interface PresenceUpdate {
user: {
id: string;
username?: string;
global_name?: string;
};
clientStatus: {
desktop?: string;
web?: string;
mobile?: string;
console?: string;
};
guildId?: string;
status: string;
broadcast?: any; // what's this?
activities: Array<{
session_id: string;
created_at: number;
id: string;
name: string;
details?: string;
type: number;
}>;
}
interface VoiceState {
userId: string;
channelId?: string;
oldChannelId?: string;
deaf: boolean;
mute: boolean;
selfDeaf: boolean;
selfMute: boolean;
selfStream: boolean;
selfVideo: boolean;
sessionId: string;
suppress: boolean;
requestToSpeakTimestamp: string | null;
}
function shouldBeNative() {
if (typeof Notification === "undefined") return false;
const { useNative } = Settings.notifications;
if (useNative === "always") return true;
if (useNative === "not-focused") return !document.hasFocus();
return false;
}
const SessionsStore = findStoreLazy("SessionsStore");
const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes");
function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) {
return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => (
<Tooltip text={tooltip} >
{(tooltipProps: any) => (
<svg
{...tooltipProps}
height={(opts?.height ?? 20) - (small ? 3 : 0)}
width={(opts?.width ?? 20) - (small ? 3 : 0)}
viewBox={opts?.viewBox ?? "0 0 24 24"}
fill={color}
>
<path d={path} />
</svg>
)}
</Tooltip>
);
}
const Icons = {
desktop: Icon("M4 2.5c-1.103 0-2 .897-2 2v11c0 1.104.897 2 2 2h7v2H7v2h10v-2h-4v-2h7c1.103 0 2-.896 2-2v-11c0-1.103-.897-2-2-2H4Zm16 2v9H4v-9h16Z"),
web: Icon("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93Zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39Z"),
mobile: Icon("M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z", { viewBox: "0 0 1000 1500", height: 17, width: 17 }),
console: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }),
};
type Platform = keyof typeof Icons;
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
const tooltip = platform[0].toUpperCase() + platform.slice(1);
const Icon = Icons[platform] ?? Icons.desktop;
return <Icon color={StatusUtils.useStatusFillColor(status)} tooltip={tooltip} small={small} />;
};
interface PlatformIndicatorProps {
user: User;
wantMargin?: boolean;
wantTopMargin?: boolean;
small?: boolean;
style?: CSSProperties;
}
const PlatformIndicator = ({ user, wantMargin = true, wantTopMargin = false, small = false, style = {} }: PlatformIndicatorProps) => {
if (!user || user.bot) return null;
if (user.id === UserStore.getCurrentUser().id) {
const sessions = SessionsStore.getSessions();
if (typeof sessions !== "object") return null;
const sortedSessions = Object.values(sessions).sort(({ status: a }: any, { status: b }: any) => {
if (a === b) return 0;
if (a === "online") return 1;
if (b === "online") return -1;
if (a === "idle") return 1;
if (b === "idle") return -1;
return 0;
});
const ownStatus = Object.values(sortedSessions).reduce((acc: any, curr: any) => {
if (curr.clientInfo.client !== "unknown")
acc[curr.clientInfo.client] = curr.status;
return acc;
}, {});
const { clientStatuses } = PresenceStore.getState();
clientStatuses[UserStore.getCurrentUser().id] = ownStatus;
}
const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record<Platform, string>;
if (!status) return null;
const icons = Object.entries(status).map(([platform, status]) => (
<PlatformIcon
key={platform}
platform={platform as Platform}
status={status}
small={small}
/>
));
if (!icons.length) return null;
return (
<span
className="vc-platform-indicator"
style={{
display: "inline-flex",
justifyContent: "center",
alignItems: "center",
marginLeft: wantMargin ? 4 : 0,
verticalAlign: "top",
position: "relative",
top: wantTopMargin ? 2 : 0,
padding: !wantMargin ? 1 : 0,
gap: 2,
...style
}}
>
{icons}
</span>
);
};
export const settings = definePluginSettings({
notifyStatus: {
type: OptionType.BOOLEAN,
description: "Notify on status changes",
restartNeeded: false,
default: true,
},
notifyVoice: {
type: OptionType.BOOLEAN,
description: "Notify on voice channel changes",
restartNeeded: false,
default: false,
},
persistNotifications: {
type: OptionType.BOOLEAN,
description: "Persist notifications",
restartNeeded: false,
default: false,
},
userIds: {
type: OptionType.STRING,
description: "User IDs (comma separated)",
restartNeeded: false,
default: "",
}
});
function getUserIdList() {
try {
return settings.store.userIds.split(",").filter(Boolean);
} catch (e) {
settings.store.userIds = "";
return [];
}
}
// show rich body with user avatar
const getRichBody = (user: User, text: string | React.ReactNode) => <div
style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "10px" }}>
<div style={{ position: "relative" }}>
<img src={user.getAvatarURL(void 0, 80, true)}
style={{ width: "80px", height: "80px", borderRadius: "15%" }} alt={`${user.username}'s avatar`} />
<PlatformIndicator user={user} style={{ position: "absolute", top: "-8px", right: "-10px" }} />
</div>
<span>{text}</span>
</div>;
function triggerVoiceNotification(userId: string, userChannelId: string | null) {
const user = UserStore.getUser(userId);
const myChanId = SelectedChannelStore.getVoiceChannelId();
const name = user.username;
const title = shouldBeNative() ? `User ${name} changed voice status` : "User voice status change";
if (userChannelId) {
if (userChannelId !== myChanId) {
showNotification({
title,
body: "joined a new voice channel",
noPersist: !settings.store.persistNotifications,
richBody: getRichBody(user, `${name} joined a new voice channel`),
});
}
} else {
showNotification({
title,
body: "left their voice channel",
noPersist: !settings.store.persistNotifications,
richBody: getRichBody(user, `${name} left their voice channel`),
});
}
}
function toggleUserNotify(userId: string) {
const userIds = getUserIdList();
if (userIds.includes(userId)) {
userIds.splice(userIds.indexOf(userId), 1);
} else {
userIds.push(userId);
}
settings.store.userIds = userIds.join(",");
}
interface UserContextProps {
channel?: Channel;
guildId?: string;
user: User;
}
const UserContext: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => {
if (!user || user.id === UserStore.getCurrentUser().id) return;
const isNotifyOn = getUserIdList().includes(user.id);
const label = isNotifyOn ? "Don't notify on changes" : "Notify on changes";
const icon = isNotifyOn ? NotificationsOffIcon : NotificationsOnIcon;
children.splice(-1, 0, (
<Menu.MenuGroup>
<Menu.MenuItem
id="toggle-notify-user"
label={label}
action={() => toggleUserNotify(user.id)}
icon={icon}
/>
</Menu.MenuGroup>
));
};
const lastStatuses = new Map<string, string>();
export default definePlugin({
name: "NotifyUserChanges",
description: "Adds a notify option in the user context menu to get notified when a user changes voice channels or online status",
authors: [Devs.D3SOX],
settings,
contextMenus: {
"user-context": UserContext
},
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
if (!settings.store.notifyVoice || !settings.store.userIds) {
return;
}
for (const { userId, channelId, oldChannelId } of voiceStates) {
if (channelId !== oldChannelId) {
const isFollowed = getUserIdList().includes(userId);
if (!isFollowed) {
continue;
}
if (channelId) {
// move or join new channel
triggerVoiceNotification(userId, channelId);
} else if (oldChannelId) {
// leave
triggerVoiceNotification(userId, null);
}
}
}
},
PRESENCE_UPDATES({ updates }: { updates: PresenceUpdate[]; }) {
if (!settings.store.notifyStatus || !settings.store.userIds) {
return;
}
for (const { user: { id: userId, username }, status } of updates) {
const isFollowed = getUserIdList().includes(userId);
if (!isFollowed) {
continue;
}
// this is also triggered for multiple guilds and when only the activities change, so we have to check if the status actually changed
if (lastStatuses.has(userId) && lastStatuses.get(userId) !== status) {
const user = UserStore.getUser(userId);
const name = username ?? user.username;
showNotification({
title: shouldBeNative() ? `User ${name} changed status` : "User status change",
body: `is now ${status}`,
noPersist: !settings.store.persistNotifications,
richBody: getRichBody(user, `${name}'s status is now ${status}`),
});
}
lastStatuses.set(userId, status);
}
}
},
});

View file

@ -11,7 +11,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--background-secondary);
background-color: var(--background-primary);
height: 48px;
width: 48px;
border-radius: 100%;

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { FluxDispatcher, PresenceStore, UserStore } from "@webpack/common";
import { PresenceStore, UserStore } from "@webpack/common";
let savedStatus = "";
const updateAsync = findByCodeLazy("updateAsync", "status");
const settings = definePluginSettings({
statusToSet: {
type: OptionType.SELECT,
@ -38,26 +38,23 @@ const settings = definePluginSettings({
}
});
migratePluginSettings("StatusWhilePlaying", "DNDWhilePlaying");
export default definePlugin({
name: "StatusWhilePlaying",
description: "Automatically updates your status when playing games",
description: "Automatically updates your online status (online, idle, dnd) when launching games",
authors: [EquicordDevs.thororen],
settings,
runningGamesChange(event) {
let savedStatus = "";
if (event.games.length > 0) {
flux: {
RUNNING_GAMES_CHANGE(event) {
const status = PresenceStore.getStatus(UserStore.getCurrentUser().id);
savedStatus = status;
updateAsync(settings.store.statusToSet);
} else if (event.games.length === 0) {
updateAsync(savedStatus);
}
},
start() {
FluxDispatcher.subscribe("RUNNING_GAMES_CHANGE", this.runningGamesChange);
},
stop() {
FluxDispatcher.unsubscribe("RUNNING_GAMES_CHANGE", this.runningGamesChange);
if (event.games.length > 0) {
if (savedStatus !== "" && savedStatus !== settings.store.statusToSet)
updateAsync(savedStatus);
} else {
if (status !== settings.store.statusToSet) {
savedStatus = status;
updateAsync(settings.store.statusToSet);
}
}
},
}
});

View file

@ -0,0 +1,101 @@
/*
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy, findStoreLazy } from "@webpack";
import { ReadStateStore, useStateFromStores } from "@webpack/common";
import { Channel } from "discord-types/general";
const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
const JoinedThreadsStore = findStoreLazy("JoinedThreadsStore");
const { NumberBadge } = findByPropsLazy("NumberBadge");
const settings = definePluginSettings({
showOnMutedChannels: {
description: "Show unread count on muted channels",
type: OptionType.BOOLEAN,
default: false,
},
notificationCountLimit: {
description: "Show +99 instead of true amount",
type: OptionType.BOOLEAN,
default: false,
},
replaceWhiteDot: {
description: "Replace the white dot with the badge",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true,
},
});
export default definePlugin({
name: "UnreadCountBadge",
authors: [Devs.Joona],
description: "Shows unread message count badges on channels in the channel list",
settings,
patches: [
// Kanged from typingindicators
{
find: "UNREAD_IMPORTANT:",
replacement: [
{
match: /\.name\),.{0,120}\.children.+?:null/,
replace: "$&,$self.CountBadge({channel: arguments[0].channel,})",
predicate: () => !settings.store.replaceWhiteDot
},
{
match: /\(0,\i\.jsx\)\("div",{className:\i\(\)\(\i\.unread,\i\?\i\.unreadImportant:void 0\)}\)/,
replace: "$self.CountBadge({channel: arguments[0].channel, whiteDot:$&})",
predicate: () => settings.store.replaceWhiteDot
}
]
},
// Threads
{
// This is the thread "spine" that shows in the left
find: "M11 9H4C2.89543 9 2 8.10457 2 7V1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1V7C0 9.20914 1.79086 11 4 11H11C11.5523 11 12 10.5523 12 10C12 9.44771 11.5523 9 11 9Z",
replacement: [
{
match: /mentionsCount:\i.{0,50}?null/,
replace: "$&,$self.CountBadge({channel: arguments[0].thread})",
predicate: () => !settings.store.replaceWhiteDot
},
{
match: /\(0,\i\.jsx\)\("div",{className:\i\(\)\(\i\.unread,\i\.unreadImportant\)}\)/,
replace: "$self.CountBadge({channel: arguments[0].thread, whiteDot:$&})",
predicate: () => settings.store.replaceWhiteDot
}
]
},
],
CountBadge: ErrorBoundary.wrap(({ channel, whiteDot }: { channel: Channel, whiteDot?: JSX.Element; }) => {
const unreadCount = useStateFromStores([ReadStateStore], () => ReadStateStore.getUnreadCount(channel.id));
if (!unreadCount) return whiteDot || null;
if (!settings.store.showOnMutedChannels && (UserGuildSettingsStore.isChannelMuted(channel.guild_id, channel.id) || JoinedThreadsStore.isMuted(channel.id)))
return null;
const className = `vc-unreadCountBadge${whiteDot ? "-dot" : ""}${channel.threadMetadata ? "-thread" : ""}`;
return (
<NumberBadge
color="var(--brand-500)"
className={className}
count={
unreadCount > 99 && settings.store.notificationCountLimit
? "+99"
: unreadCount
}
/>
);
}, { noop: true }),
});

View file

@ -0,0 +1,23 @@
.unreadCountBadge {
margin-left: 4px;
}
.vc-unreadCountBadge-dot {
position: absolute;
top: 25%;
margin-left: -3%;
scale: 0.9;
z-index: 1;
}
.vc-unreadCountBadge-dot-thread {
position: absolute;
top: 20%;
margin-left: -18%;
scale: 0.9;
z-index: 1;
}
[class*="modeMuted_"] .unreadCountBadge {
display: none;
}

View file

@ -7,7 +7,7 @@
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs, EquicordDevs } from "@utils/constants";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findStoreLazy } from "@webpack";
import { GuildChannelStore, Menu, React, RestAPI, UserStore } from "@webpack/common";
@ -159,8 +159,10 @@ const settings = definePluginSettings({
export default definePlugin({
name: "VoiceChatUtilities",
description: "This plugin allows you to perform multiple actions on an entire channel (move, mute, disconnect, etc.) (originally by dutake)",
authors: [EquicordDevs.Dams, Devs.D3SOX],
authors: [Devs.D3SOX],
settings,
contextMenus: {
"channel-context": VoiceChannelContext
},

View file

@ -8,8 +8,11 @@ import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Text } from "@webpack/common";
import { ReactNode } from "react";
import ExampleWiggle from "./ui/components/ExampleWiggle";
const settings = definePluginSettings({
intensity: {
type: OptionType.SLIDER,
@ -18,22 +21,6 @@ const settings = definePluginSettings({
default: 4,
stickToMarkers: true,
onChange: () => updateStyles()
},
direction: {
type: OptionType.SELECT,
description: "Swing direction",
options: [{
label: "Circle",
value: "xy",
default: true
}, {
label: "Horizontal",
value: "x",
}, {
label: "Vertical",
value: "y"
}],
onChange: () => updateStyles()
}
});
@ -42,56 +29,103 @@ const dirMap = {
y: "1.2s wiggle-wavy-y linear infinite"
};
const classMap = [
{
chars: ["<", ">"],
className: "wiggle-inner-x",
},
{
chars: ["^", "^"],
className: "wiggle-inner-y",
},
{
chars: [")", "("],
className: "wiggle-inner-xy"
}
];
let styles: HTMLStyleElement;
const updateStyles = () => {
const inten = Vencord.Settings.plugins.WigglyText.intensity + "px";
const dir = Vencord.Settings.plugins.WigglyText.direction as string;
styles.textContent = `
.wiggly-inner {
animation: ${dir.split("").map(dir => dirMap[dir]).join(", ")};
position: relative;
top: 0;
left: 0;
}
.wiggle-example {
list-style-type: disc;
list-style-position: outside;
margin: 4px 0 0 16px;
}
@keyframes wiggle-wavy-x {
from {
left: -${inten};
}
.wiggle-example li {
white-space: break-spaces;
margin-bottom: 4px;
}
to {
left: ${inten};
}
}
.wiggle-inner {
position: relative;
top: 0;
left: 0;
@keyframes wiggle-wavy-y {
0% {
top: 0;
animation-timing-function: ease-out;
}
&.wiggle-inner-x {
animation: ${dirMap.x};
}
25% {
top: -${inten};
animation-timing-function: ease-in;
}
&.wiggle-inner-y {
animation: ${dirMap.y};
}
50% {
top: 0;
animation-timing-function: ease-out;
}
&.wiggle-inner-xy {
animation: ${dirMap.x}, ${dirMap.y};
}
}
75% {
top: ${inten};
animation-timing-function: ease-in;
}
}`;
@keyframes wiggle-wavy-x {
from {
left: -${inten};
}
to {
left: ${inten};
}
}
@keyframes wiggle-wavy-y {
0% {
top: 0;
animation-timing-function: ease-out;
}
25% {
top: -${inten};
animation-timing-function: ease-in;
}
50% {
top: 0;
animation-timing-function: ease-out;
}
75% {
top: ${inten};
animation-timing-function: ease-in;
}
}`;
};
export default definePlugin({
name: "WigglyText",
description: "Adds a new markdown formatting that makes text ~wiggly~",
description: "Adds a new markdown formatting that makes text wiggly.",
authors: [EquicordDevs.nexpid],
settings,
settingsAboutComponent: () => (
<Text>
You can make text wiggle with the following:<br />
<ul className="wiggle-example">
<li><ExampleWiggle wiggle="x">left and right</ExampleWiggle> by typing <code>&lt;~text~&gt;</code></li>
<li><ExampleWiggle wiggle="y">up and down</ExampleWiggle> by typing <code>^~text~^</code></li>
<li><ExampleWiggle wiggle="xy">in a circle</ExampleWiggle> by typing <code>)~text~(</code></li>
</ul>
</Text>
),
patches: [
{
find: "parseToAST:",
@ -104,16 +138,21 @@ export default definePlugin({
wigglyRule: {
order: 24,
match: (source: string) => source.match(/^~([\s\S]+?)~(?!_)/),
match: (source: string) => classMap.map(({ chars }) => source.match(new RegExp(`^(\\${chars[0]})~([\\s\\S]+?)~(\\${chars[1]})(?!_)`))).find(x => x !== null),
parse: (
capture: RegExpMatchArray,
transform: (...args: any[]) => any,
state: any
) => ({
content: transform(capture[1], state),
}),
) => {
const className = classMap.find(({ chars }) => chars[0] === capture[1] && chars[1] === capture[3])?.className ?? "";
return {
content: transform(capture[2], state),
className
};
},
react: (
data: { content: any[]; },
data: { content: any[]; className: string; },
output: (...args: any[]) => ReactNode[]
) => {
let offset = 0;
@ -129,7 +168,7 @@ export default definePlugin({
children[j] = child.split("").map((x, i) => (
<span key={i}>
<span
className="wiggly-inner"
className={`wiggle-inner ${data.className}`}
style={{
animationDelay: `${((offset++) * 25) % 1200}ms`,
}}

View file

@ -0,0 +1,20 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export default function ExampleWiggle({ wiggle, children }: { wiggle: "x" | "y" | "xy", children: string; }) {
return children.split("").map((x, i) => (
<span key={i}>
<span
className={`wiggle-inner wiggle-inner-${wiggle}`}
style={{
animationDelay: `${(i * 25) % 1200}ms`,
}}
>
{x}
</span>
</span>
));
}

View file

@ -91,7 +91,7 @@ export default definePlugin({
replacement: [
// Use Decor avatar decoration hook
{
match: /(?<=\i\)\({avatarDecoration:)(\i).avatarDecoration(?=,)/,
match: /(?<=\i\)\({avatarDecoration:)(\i)(?=,)(?<=currentUser:(\i).+?)/,
replace: "$self.useUserDecorAvatarDecoration($1)??$&"
}
]

View file

@ -21,7 +21,7 @@ import { Devs } from "@utils/constants";
import { isNonNullish } from "@utils/guards";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, UserStore } from "@webpack/common";
import { Avatar, ChannelStore, Clickable, IconUtils, RelationshipStore, ScrollerThin, useMemo, UserStore } from "@webpack/common";
import { Channel, User } from "discord-types/general";
const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
@ -39,6 +39,19 @@ function getGroupDMName(channel: Channel) {
.join(", ");
}
const getMutualGroupDms = (userId: string) =>
ChannelStore.getSortedPrivateChannels()
.filter(c => c.isGroupDM() && c.recipients.includes(userId));
const isBotOrSelf = (user: User) => user.bot || user.id === UserStore.getCurrentUser().id;
function getMutualGDMCountText(user: User) {
const count = getMutualGroupDms(user.id).length;
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
}
const IS_PATCHED = Symbol("MutualGroupDMs.Patched");
export default definePlugin({
name: "MutualGroupDMs",
description: "Shows mutual group dms in profiles",
@ -63,8 +76,8 @@ export default definePlugin({
find: ".MUTUAL_FRIENDS?(",
replacement: [
{
match: /(?<=onItemSelect:\i,children:)(\i)\.map/,
replace: "[...$1, ...($self.isBotOrSelf(arguments[0].user) ? [] : [{section:'MUTUAL_GDMS',text:$self.getMutualGDMCountText(arguments[0].user)}])].map"
match: /\i\.useEffect.{0,100}(\i)\[0\]\.section/,
replace: "$self.pushSection($1, arguments[0].user);$&"
},
{
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
@ -74,15 +87,23 @@ export default definePlugin({
}
],
isBotOrSelf: (user: User) => user.bot || user.id === UserStore.getCurrentUser().id,
isBotOrSelf,
getMutualGDMCountText,
getMutualGDMCountText: (user: User) => {
const count = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).length;
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
pushSection(sections: any[], user: User) {
if (isBotOrSelf(user) || sections[IS_PATCHED]) return;
sections[IS_PATCHED] = true;
sections.push({
section: "MUTUAL_GDMS",
text: getMutualGDMCountText(user)
});
},
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
const mutualDms = useMemo(() => getMutualGroupDms(user.id), [user.id]);
const entries = mutualDms.map(c => (
<Clickable
className={ProfileListClasses.listRow}
onClick={() => {

View file

@ -19,16 +19,11 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findLazy } from "@webpack";
import { Constants, GuildStore, i18n, RestAPI } from "@webpack/common";
const InvitesDisabledExperiment = findLazy(m => m.definition?.id === "2022-07_invites_disabled");
function showDisableInvites(guildId: string) {
// Once the experiment is removed, this should keep working
const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true };
// @ts-ignore
return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
return !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED");
}
function disableInvites(guildId: string) {

View file

@ -24,13 +24,13 @@ const settings = definePluginSettings({
export default definePlugin({
name: "PictureInPicture",
description: "Adds picture in picture to videos (next to the Download button)",
authors: [Devs.Nobody],
authors: [Devs.Lumap],
settings,
patches: [
{
find: ".nonMediaMosaicItem]",
find: ".removeMosaicItemHoverButton),",
replacement: {
match: /\.nonMediaMosaicItem\]:!(\i).{0,50}?children:\[(\S)/,
match: /\.nonMediaMosaicItem\]:!(\i).{0,50}?children:\[\S,(\S)/,
replace: "$&,$1&&$2&&$self.renderPiPButton(),"
},
},

View file

@ -12,7 +12,7 @@
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--background-secondary);
background-color: var(--background-primary);
height: 48px;
width: 48px;
border-radius: 100%;

View file

@ -51,7 +51,7 @@ export default definePlugin({
},
},
{
find: "2022-07_invites_disabled",
find: "INVITES_DISABLED))||",
predicate: () => settings.store.showInvitesPaused,
replacement: {
match: /\i\.\i\.can\(\i\.\i.MANAGE_GUILD,\i\)/,

View file

@ -55,6 +55,7 @@ interface PlayerState {
// added by patch
actual_repeat: Repeat;
shuffle: boolean;
}
interface Device {
@ -182,6 +183,7 @@ export const SpotifyStore = proxyLazyWebpack(() => {
store.isPlaying = e.isPlaying ?? false;
store.volume = e.volumePercent ?? 0;
store.repeat = e.actual_repeat || "off";
store.shuffle = e.shuffle ?? false;
store.position = e.position ?? 0;
store.isSettingPosition = false;
store.emitChange();

View file

@ -70,21 +70,20 @@ export default definePlugin({
replace: "false",
}]
},
// Discord doesn't give you the repeat kind, only a boolean
{
find: 'repeat:"off"!==',
replacement: {
match: /repeat:"off"!==(.{1,3}),/,
replace: "actual_repeat:$1,$&"
}
replacement: [
{
// Discord doesn't give you shuffle state and the repeat kind, only a boolean
match: /repeat:"off"!==(\i),/,
replace: "shuffle:arguments[2]?.shuffle_state??false,actual_repeat:$1,$&"
},
{
match: /(?<=artists.filter\(\i=>).{0,10}\i\.id\)&&/,
replace: ""
}
]
},
{
find: "artists.filter",
replacement: {
match: /(?<=artists.filter\(\i=>).{0,10}\i\.id\)&&/,
replace: ""
}
}
],
start: () => toggleHoverControls(Settings.plugins.SpotifyControls.hoverControls),

View file

@ -22,7 +22,6 @@ import definePlugin, { OptionType } from "@utils/types";
import { saveFile } from "@utils/web";
import { findByPropsLazy } from "@webpack";
import { Clipboard, ComponentDispatch } from "@webpack/common";
const ctxMenuCallbacks = findByPropsLazy("contextMenuCallbackNative");
async function fetchImage(url: string) {
@ -41,7 +40,7 @@ const settings = definePluginSettings({
description: "Add back the Discord context menus for images, links and the chat input bar",
// Web slate menu has proper spellcheck suggestions and image context menu is also pretty good,
// so disable this by default. Vesktop just doesn't, so enable by default
default: IS_VESKTOP || IS_EQUIBOP,
default: IS_VESKTOP && !IS_EQUIBOP || !IS_VESKTOP && IS_EQUIBOP,
restartNeeded: true
}
});

View file

@ -35,10 +35,6 @@ 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: "Vee",
id: 343383572805058560n
@ -547,13 +543,33 @@ export const Devs = /* #__PURE__*/ Object.freeze({
surgedevs: {
name: "Chloe",
id: 1084592643784331324n
}
},
Lumap: {
name: "Lumap",
id: 585278686291427338n,
},
} satisfies Record<string, Dev>);
export const EquicordDevs = Object.freeze({
vmohammad: {
name: "vMohammad",
id: 921098159348924457n
thororen: {
name: "thororen",
id: 848339671629299742n
},
nyx: {
name: "verticalsync",
id: 328165170536775680n,
},
Cortex: {
name: "Cortex",
id: 825069530376044594n,
},
KrystalSkull: {
name: "krystalskullofficial",
id: 929208515883569182n
},
Naibuu: {
name: "hs50",
id: 1120045713867423835n,
},
nexpid: {
name: "Nexpid",
@ -575,10 +591,6 @@ export const EquicordDevs = Object.freeze({
name: "ryan",
id: 479403382994632704n
},
thororen: {
name: "thororen",
id: 848339671629299742n
},
MrDiamond: {
name: "MrDiamond",
id: 523338295644782592n
@ -615,18 +627,10 @@ export const EquicordDevs = Object.freeze({
name: "cooles",
id: 406084422308331522n,
},
KrystalSkull: {
name: "krystalskullofficial",
id: 929208515883569182n
},
SerStars: {
name: "SerStars",
id: 861631850681729045n
},
nyx: {
name: "verticalsync",
id: 328165170536775680n,
},
MaxHerbold: {
name: "MaxHerbold",
id: 1189527130611138663n,
@ -639,13 +643,9 @@ export const EquicordDevs = Object.freeze({
name: "Megal",
id: 387790666484285441n
},
Cortex: {
name: "Cortex",
id: 825069530376044594n,
},
Woosh: {
name: "w00shh.",
id: 689165844835860522n,
id: 919239894327521361n,
},
Hanzy: {
name: "hanzydev",
@ -683,10 +683,6 @@ export const EquicordDevs = Object.freeze({
name: "Fafa",
id: 428188716641812481n,
},
Naibuu: {
name: "hs50",
id: 1120045713867423835n,
},
Colorman: {
name: "colorman",
id: 298842558610800650n,
@ -743,6 +739,18 @@ export const EquicordDevs = Object.freeze({
name: "sadan",
id: 521819891141967883n
},
x3rt: {
name: "x3rt",
id: 131602100332396544n
},
Hen: {
name: "Hen",
id: 279266228151779329n
},
vmohammad: {
name: "vMohammad",
id: 921098159348924457n
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly