Merge branch 'dev'

This commit is contained in:
thororen1234 2025-04-02 16:26:44 -04:00
commit aa1a938860
No known key found for this signature in database
38 changed files with 1440 additions and 1396 deletions

View file

@ -122,7 +122,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
- QuestCompleter by Amia - QuestCompleter by Amia
- QuestionMarkReplacement by nyx - QuestionMarkReplacement by nyx
- Quoter by Samwich - Quoter by Samwich
- RandomVoice by omaw - RandomVoice by xijexo & omaw
- Remix by MrDiamond - Remix by MrDiamond
- RemixMe by kvba - RemixMe by kvba
- RepeatMessage by Tolgchu - RepeatMessage by Tolgchu

View file

@ -26,8 +26,8 @@
"generatePluginJson": "tsx scripts/generatePluginList.ts", "generatePluginJson": "tsx scripts/generatePluginList.ts",
"generateEquicordPluginJson": "tsx scripts/generateEquicordPluginList.ts", "generateEquicordPluginJson": "tsx scripts/generateEquicordPluginList.ts",
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types --allowJs false", "generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types --allowJs false",
"inject": "node scripts/runInstaller.mjs", "inject": "node scripts/runInstaller.mjs -- --install",
"uninject": "node scripts/runInstaller.mjs", "uninject": "node scripts/runInstaller.mjs -- --uninstall",
"lint": "eslint", "lint": "eslint",
"lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins", "lint-styles": "stylelint \"src/**/*.css\" --ignore-pattern src/userplugins",
"lint:fix": "pnpm lint --fix", "lint:fix": "pnpm lint --fix",
@ -50,26 +50,26 @@
"jsqr": "1.4.0", "jsqr": "1.4.0",
"idb": "8.0.0", "idb": "8.0.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"nanoid": "^5.0.9", "nanoid": "^5.1.5",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"usercss-meta": "^0.12.0", "usercss-meta": "^0.12.0",
"openai": "^4.30.0", "openai": "^4.30.0",
"virtual-merge": "^1.0.1" "virtual-merge": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "^4.0.0", "@stylistic/eslint-plugin": "^4.2.0",
"@electron/asar": "^3.2.10", "@electron/asar": "^3.2.10",
"@types/chrome": "^0.0.304", "@types/chrome": "^0.0.312",
"@types/diff": "^7.0.1", "@types/diff": "^7.0.2",
"@types/lodash": "^4.17.14", "@types/lodash": "^4.17.14",
"@types/node": "^22.10.5", "@types/node": "^22.13.13",
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@types/yazl": "^2.4.5", "@types/yazl": "^2.4.5",
"diff": "^7.0.0", "diff": "^7.0.0",
"discord-types": "^1.3.26", "discord-types": "^1.3.26",
"esbuild": "^0.25.0", "esbuild": "^0.25.1",
"eslint": "^9.20.1", "eslint": "9.20.1",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-react": "^7.37.3", "eslint-plugin-react": "^7.37.3",
"eslint-plugin-simple-header": "^1.2.1", "eslint-plugin-simple-header": "^1.2.1",
@ -78,18 +78,18 @@
"highlight.js": "11.11.1", "highlight.js": "11.11.1",
"html-minifier-terser": "^7.2.0", "html-minifier-terser": "^7.2.0",
"moment": "^2.22.2", "moment": "^2.22.2",
"puppeteer-core": "^24.2.1", "puppeteer-core": "^24.4.0",
"standalone-electron-types": "^34.2.0", "standalone-electron-types": "^34.2.0",
"stylelint": "^16.12.0", "stylelint": "^16.17.0",
"stylelint-config-standard": "^37.0.0", "stylelint-config-standard": "^37.0.0",
"ts-patch": "^3.3.0", "ts-patch": "^3.3.0",
"ts-pattern": "^5.6.0", "ts-pattern": "^5.6.0",
"tsx": "^4.19.2", "tsx": "^4.19.3",
"type-fest": "^4.31.0", "type-fest": "^4.38.0",
"typed-emitter": "^2.1.0", "typed-emitter": "^2.1.0",
"typescript": "^5.7.2", "typescript": "^5.8.2",
"typescript-eslint": "^8.19.0", "typescript-eslint": "^8.28.0",
"typescript-transform-paths": "^3.5.3", "typescript-transform-paths": "^3.5.5",
"zip-local": "^0.3.5", "zip-local": "^0.3.5",
"zustand": "^3.7.2" "zustand": "^3.7.2"
}, },

1261
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -118,8 +118,11 @@ const installerBin = await ensureBinary();
console.log("Now running Installer..."); console.log("Now running Installer...");
const argStart = process.argv.indexOf("--");
const args = argStart === -1 ? [] : process.argv.slice(argStart + 1);
try { try {
execFileSync(installerBin, { execFileSync(installerBin, args, {
stdio: "inherit", stdio: "inherit",
env: { env: {
...process.env, ...process.env,

View file

@ -51,6 +51,7 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
onChange={handleChange} onChange={handleChange}
placeholder={option.placeholder ?? "Enter a value"} placeholder={option.placeholder ?? "Enter a value"}
disabled={option.disabled?.call(definedSettings) ?? false} disabled={option.disabled?.call(definedSettings) ?? false}
maxLength={null}
{...option.componentProps} {...option.componentProps}
/> />
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>} {error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}

View file

@ -128,8 +128,8 @@ function EquicordSettings() {
isEquicordDonor(user?.id) && isVencordDonor(user?.id) isEquicordDonor(user?.id) && isVencordDonor(user?.id)
? "All Vencord users can see your Vencord donor badge, and Equicord users can see your Equicord donor badge. To change your Vencord donor badge, contact @vending.machine. For your Equicord donor badge, make a ticket in Equicord's server." ? "All Vencord users can see your Vencord donor badge, and Equicord users can see your Equicord donor badge. To change your Vencord donor badge, contact @vending.machine. For your Equicord donor badge, make a ticket in Equicord's server."
: isVencordDonor(user?.id) : isVencordDonor(user?.id)
? "All Vencord users can see your badge! You can change it at any time by messaging @vending.machine." ? "All Vencord users can see your badge! You can manage your perks by messaging @vending.machine."
: "All Equicord users can see your badge! You can change it at any time by making a ticket in Equicord's server." : "All Equicord users can see your badge! You can manage your perks by making a ticket in Equicord's server."
} }
cardImage={VENNIE_DONATOR_IMAGE} cardImage={VENNIE_DONATOR_IMAGE}
backgroundImage={DONOR_BACKGROUND_IMAGE} backgroundImage={DONOR_BACKGROUND_IMAGE}

View file

@ -12,7 +12,7 @@
} }
.visual-refresh .vc-addon-card { .visual-refresh .vc-addon-card {
background-color: var(--button-secondary-background); background-color: var(--card-primary-bg);
border: 1px solid var(--border-subtle); border: 1px solid var(--border-subtle);
} }
@ -27,7 +27,8 @@
} }
.visual-refresh .vc-addon-card:hover { .visual-refresh .vc-addon-card:hover {
background-color: var(--button-secondary-background-hover); /* same as non-hover, here to overwrite the non-refresh hover background */
background-color: var(--card-primary-bg);
} }
.vc-addon-header { .vc-addon-header {

View file

@ -0,0 +1,28 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { UserStore } from "@webpack/common";
import { ActivityTooltipProps } from "../types";
import { ActivityView, cl } from "../utils";
export function ActivityTooltip({ activity, application, user }: Readonly<ActivityTooltipProps>) {
const currentUser = UserStore.getCurrentUser();
if (!currentUser) return null;
return (
<ErrorBoundary>
<div className={cl("activity-tooltip")}>
<ActivityView
activity={activity}
user={user}
application={application}
currentUser={currentUser}
/>
</div>
</ErrorBoundary>
);
}

View file

@ -0,0 +1,77 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { React, Tooltip } from "@webpack/common";
import { CarouselControlsProps } from "../types";
import { cl } from "../utils";
import { Caret } from "./Caret";
export function CarouselControls({ activities, currentActivity, onActivityChange }: CarouselControlsProps) {
const currentIndex = activities.indexOf(currentActivity);
return (
<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={() => {
if (currentIndex - 1 >= 0) {
onActivityChange(activities[currentIndex - 1]);
} else {
onActivityChange(activities[activities.length - 1]);
}
}}
>
<Caret
disabled={currentIndex < 1}
direction="left" />
</span>;
}}</Tooltip>
<div className="carousel">
{activities.map((activity, index) => (
<div
key={"dot--" + index}
onClick={() => onActivityChange(activity)}
className={`dot ${currentActivity === activity ? "selected" : ""}`} />
))}
</div>
<Tooltip text="Right" tooltipClassName={cl("controls-tooltip")}>{({
onMouseEnter,
onMouseLeave
}) => {
return <span
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={() => {
if (currentIndex + 1 < activities.length) {
onActivityChange(activities[currentIndex + 1]);
} else {
onActivityChange(activities[0]);
}
}}
>
<Caret
disabled={currentIndex >= activities.length - 1}
direction="right" />
</span>;
}}</Tooltip>
</div>
);
}

View file

@ -6,467 +6,31 @@
import "./styles.css"; import "./styles.css";
import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import { migratePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin from "@utils/types";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { PresenceStore, React, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
import { User } from "discord-types/general";
import { JSX } from "react";
import { Caret } from "./components/Caret"; import { patchActivityList } from "./patch-helpers/activityList";
import { SpotifyIcon } from "./components/SpotifyIcon"; import { showAllActivitiesComponent } from "./patch-helpers/popout";
import { TwitchIcon } from "./components/TwitchIcon"; import { settings } from "./settings";
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 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;
currentUser: User;
}>('location:"UserProfileActivityCard",');
// 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; }>) => {
const currentUser = UserStore.getCurrentUser();
if (!currentUser) return null;
return (
<ErrorBoundary>
<div className={cl("activity-tooltip")}>
<ActivityView
activity={activity}
user={user}
application={application}
currentUser={currentUser}
/>
</div>
</ErrorBoundary>
);
};
function getActivityApplication(activity: Activity | null) {
if (!activity) return undefined;
const { 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"); migratePluginSettings("BetterActivities", "MemberListActivities");
export default definePlugin({ export default definePlugin({
name: "BetterActivities", name: "BetterActivities",
description: "Shows activity icons in the member list and allows showing all activities", description: "Shows activity icons in the member list and allows showing all activities",
authors: [Devs.D3SOX, Devs.Arjix, Devs.AutumnVN], authors: [
Devs.D3SOX,
Devs.Arjix,
Devs.AutumnVN
],
tags: ["activity"], tags: ["activity"],
settings, settings,
patchActivityList: ({ activities, user, hideTooltip }: { activities: Activity[], user: User, hideTooltip: boolean; }): JSX.Element | null => { patchActivityList,
const icons: ActivityListIcon[] = [];
if (user.bot || hideTooltip) return null; showAllActivitiesComponent,
const applicationIcons = getApplicationIcons(activities);
if (applicationIcons.length) {
const compareImageSource = (a: ApplicationIcon, b: ApplicationIcon) => {
return a.image.src === b.image.src;
};
const uniqueIcons = applicationIcons.filter((element, index, array) => {
return array.findIndex(el => compareImageSource(el, element)) === index;
});
for (const appIcon of uniqueIcons) {
icons.push({
iconElement: <img {...appIcon.image} />,
tooltip: <ActivityTooltip activity={appIcon.activity} application={appIcon.application} user={user} />
});
}
}
const addActivityIcon = (activityName: string, IconComponent: React.ComponentType) => {
const activityIndex = activities.findIndex(({ name }) => name === activityName);
if (activityIndex !== -1) {
const activity = activities[activityIndex];
const iconObject: ActivityListIcon = {
iconElement: <IconComponent />,
tooltip: <ActivityTooltip activity={activity} user={user} />
};
if (settings.store.specialFirst) {
icons.unshift(iconObject);
} else {
icons.splice(activityIndex, 0, iconObject);
}
}
};
addActivityIcon("Twitch", TwitchIcon);
addActivityIcon("Spotify", SpotifyIcon);
if (icons.length) {
const iconStyle: IconCSSProperties = {
"--icon-size": `${settings.store.iconSize}px`,
};
return <ErrorBoundary noop>
<div className={cl("row")}>
{icons.map(({ iconElement, tooltip }, i) => (
<div key={i} className={cl("icon")} style={iconStyle}>
{tooltip ? <Tooltip text={tooltip}>
{({ onMouseEnter, onMouseLeave }) => (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}>
{iconElement}
</div>
)}
</Tooltip> : iconElement}
</div>
))}
</div>
</ErrorBoundary>;
} else {
// Show default icon when there are no custom icons
// We need to filter out custom statuses
const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length;
if (shouldShow) {
return <DefaultActivityIcon />;
}
}
return null;
},
showAllActivitiesComponent({ activity, user, ...props }: Readonly<{ activity: Activity; user: User; application: Application; type: string; }>) {
const currentUser = UserStore.getCurrentUser();
if (!currentUser) return null;
const [currentActivity, setCurrentActivity] = useState<Activity | null>(
activity?.type !== 4 ? activity! : null
);
const activities = useStateFromStores<Activity[]>(
[PresenceStore], () => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity.type !== 4)
) ?? [];
useEffect(() => {
if (!activities.length) {
setCurrentActivity(null);
return;
}
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 (
<ErrorBoundary noop onError={() => { }}>
<div style={{ display: "flex", flexDirection: "column" }}>
{activity && currentActivity?.id === activity.id ? (
<ActivityView
activity={currentActivity}
user={user}
currentUser={currentUser}
{...props}
/>
) : (
<ActivityView
activity={currentActivity}
user={user}
// fetch optional application
application={getActivityApplication(currentActivity!)}
currentUser={currentUser}
{...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]);
} else {
setCurrentActivity(activities[activities.length - 1]);
}
}}
>
<Caret
disabled={activities.indexOf(currentActivity!) < 1}
direction="left" />
</span>;
}}</Tooltip>
<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]);
} else {
setCurrentActivity(activities[0]);
}
}}
>
<Caret
disabled={activities.indexOf(currentActivity!) >= activities.length - 1}
direction="right" />
</span>;
}}</Tooltip>
</div>
}
</div>
</ErrorBoundary>
);
} else {
return (
<ErrorBoundary noop onError={() => { }}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
{activities.map((activity, index) =>
index === 0 ? (
<ActivityView
key={index}
activity={activity}
user={user}
currentUser={currentUser}
{...props}
/>) : (
<ActivityView
key={index}
activity={activity}
user={user}
application={getActivityApplication(activity)}
currentUser={currentUser}
{...generalProps}
/>
))}
</div>
</ErrorBoundary>
);
}
},
patches: [ patches: [
{ {

View file

@ -0,0 +1,94 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { findComponentByCodeLazy } from "@webpack";
import { React, Tooltip } from "@webpack/common";
import { JSX } from "react";
import { ActivityTooltip } from "../components/ActivityTooltip";
import { SpotifyIcon } from "../components/SpotifyIcon";
import { TwitchIcon } from "../components/TwitchIcon";
import { settings } from "../settings";
import { ActivityListIcon, ActivityListProps, ApplicationIcon, IconCSSProperties } from "../types";
import { cl, getApplicationIcons } from "../utils";
// 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");
export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null {
const icons: ActivityListIcon[] = [];
if (user.bot || hideTooltip) return null;
const applicationIcons = getApplicationIcons(activities);
if (applicationIcons.length) {
const compareImageSource = (a: ApplicationIcon, b: ApplicationIcon) => {
return a.image.src === b.image.src;
};
const uniqueIcons = applicationIcons.filter((element, index, array) => {
return array.findIndex(el => compareImageSource(el, element)) === index;
});
for (const appIcon of uniqueIcons) {
icons.push({
iconElement: <img {...appIcon.image} />,
tooltip: <ActivityTooltip activity={appIcon.activity} application={appIcon.application} user={user} />
});
}
}
const addActivityIcon = (activityName: string, IconComponent: React.ComponentType) => {
const activityIndex = activities.findIndex(({ name }) => name === activityName);
if (activityIndex !== -1) {
const activity = activities[activityIndex];
const iconObject: ActivityListIcon = {
iconElement: <IconComponent />,
tooltip: <ActivityTooltip activity={activity} user={user} />
};
if (settings.store.specialFirst) {
icons.unshift(iconObject);
} else {
icons.splice(activityIndex, 0, iconObject);
}
}
};
addActivityIcon("Twitch", TwitchIcon);
addActivityIcon("Spotify", SpotifyIcon);
if (icons.length) {
const iconStyle: IconCSSProperties = {
"--icon-size": `${settings.store.iconSize}px`,
};
return <ErrorBoundary noop>
<div className={cl("row")}>
{icons.map(({ iconElement, tooltip }, i) => (
<div key={i} className={cl("icon")} style={iconStyle}>
{tooltip ? <Tooltip text={tooltip}>
{({ onMouseEnter, onMouseLeave }) => (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}>
{iconElement}
</div>
)}
</Tooltip> : iconElement}
</div>
))}
</div>
</ErrorBoundary>;
} else {
// Show default icon when there are no custom icons
// We need to filter out custom statuses
const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length;
if (shouldShow) {
return <DefaultActivityIcon />;
}
}
return null;
}

View file

@ -0,0 +1,111 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { PresenceStore, React, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
import { JSX } from "react";
import { CarouselControls } from "../components/CarouselControls";
import { settings } from "../settings";
import { Activity, AllActivitiesProps } from "../types";
import { ActivityView, getActivityApplication } from "../utils";
export function showAllActivitiesComponent({ activity, user, ...props }: Readonly<AllActivitiesProps>): JSX.Element | null {
const currentUser = UserStore.getCurrentUser();
if (!currentUser) return null;
const [currentActivity, setCurrentActivity] = useState<Activity | null>(
activity?.type !== 4 ? activity! : null
);
const activities = useStateFromStores(
[PresenceStore],
() => PresenceStore.getActivities(user.id).filter((activity: Activity) => activity.type !== 4)
);
useEffect(() => {
if (!activities.length) {
setCurrentActivity(null);
return;
}
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 (
<ErrorBoundary noop>
<div style={{ display: "flex", flexDirection: "column" }}>
{activity && currentActivity?.id === activity.id ? (
<ActivityView
activity={currentActivity}
user={user}
currentUser={currentUser}
{...props}
/>
) : (
<ActivityView
activity={currentActivity}
user={user}
// fetch optional application
application={getActivityApplication(currentActivity!)}
currentUser={currentUser}
{...generalProps}
/>
)}
{activities.length > 1 && currentActivity && (
<CarouselControls
activities={activities}
currentActivity={currentActivity}
onActivityChange={setCurrentActivity}
/>
)}
</div>
</ErrorBoundary>
);
} else {
return (
<ErrorBoundary noop>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
{activities.map((activity, index) =>
index === 0 ? (
<ActivityView
key={index}
activity={activity}
user={user}
currentUser={currentUser}
{...props}
/>) : (
<ActivityView
key={index}
activity={activity}
user={user}
application={getActivityApplication(activity)}
currentUser={currentUser}
{...generalProps}
/>
))}
</div>
</ErrorBoundary>
);
}
}

View file

@ -0,0 +1,71 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 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";
export 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,
},
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",
},
]
}
});

View file

@ -19,12 +19,8 @@
border-radius: 50%; border-radius: 50%;
} }
[class*="tooltip_"]:has(.vc-bactivities-activity-tooltip) {
max-width: 280px;
}
.vc-bactivities-activity-tooltip { .vc-bactivities-activity-tooltip {
margin: -5px; padding: 1px;
} }
.vc-bactivities-caret-left, .vc-bactivities-caret-left,

View file

@ -1,9 +1,10 @@
/* /*
* Vencord, a Discord client mod * Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors * Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { User } from "discord-types/general";
import { CSSProperties, ImgHTMLAttributes, JSX } from "react"; import { CSSProperties, ImgHTMLAttributes, JSX } from "react";
export interface Timestamp { export interface Timestamp {
@ -80,3 +81,36 @@ export interface ActivityListIcon {
export interface IconCSSProperties extends CSSProperties { export interface IconCSSProperties extends CSSProperties {
"--icon-size": string; "--icon-size": string;
} }
export interface ActivityListProps {
activities: Activity[];
user: User;
hideTooltip: boolean;
}
export interface ActivityTooltipProps {
activity: Activity;
application?: Application;
user: User;
}
export interface AllActivitiesProps {
activity: Activity;
user: User;
application: Application;
type: string;
[key: string]: any;
}
export interface CarouselControlsProps {
activities: Activity[];
currentActivity: Activity;
onActivityChange: (activity: Activity) => void;
}
export interface ActivityViewProps {
activity: Activity | null;
user: User;
application?: Application;
currentUser: User;
}

View file

@ -0,0 +1,127 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { settings } from "./settings";
import { Activity, ActivityViewProps, Application, ApplicationIcon } from "./types";
const ApplicationStore: {
getApplication: (id: string) => Application | null;
} = findStoreLazy("ApplicationStore");
const { fetchApplication }: {
fetchApplication: (id: string) => Promise<Application | null>;
} = findByPropsLazy("fetchApplication");
const fetchedApplications = new Map<string, Application | null>();
const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png"; // TODO: replace with "renderXboxImage"?
export const ActivityView = findComponentByCodeLazy<ActivityViewProps>('location:"UserProfileActivityCard",');
export const cl = classNameFactory("vc-bactivities-");
export function getActivityApplication(activity: Activity | null) {
if (!activity) return undefined;
const { 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;
}
export function getApplicationIcons(activities: Activity[], preferSmall = false): ApplicationIcon[] {
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 { small_image, small_text, large_image, large_text } = assets;
const smallText = small_text ?? "Small Text";
const largeText = large_text ?? "Large Text";
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
});
}
};
if (preferSmall) {
if (small_image) {
addImage(small_image, smallText);
} else if (large_image) {
addImage(large_image, largeText);
}
} else {
if (large_image) {
addImage(large_image, largeText);
} else if (small_image) {
addImage(small_image, 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;
}

View file

@ -81,23 +81,22 @@ export default definePlugin({
patches: [ patches: [
{ {
// this also affects name headers in chats outside of servers // this also affects name headers in chats outside of servers
find: ".USERNAME),{", find: '="SYSTEM_TAG"',
replacement: { replacement: {
match: /style:"username"===.{0,25}void 0/, match: /(?<=\i.gradientClassName]\),style:.{0,80}:void 0,)/,
replace: "style:{color:$self.colorIfServer(arguments[0])}" replace: "style:{color:$self.colorIfServer(arguments[0])},"
}, },
noWarn: true, predicate: () => !Settings.plugins.IrcColors.enabled
}, },
{ {
predicate: () => settings.store.dmList,
find: "PrivateChannel.renderAvatar", find: "PrivateChannel.renderAvatar",
replacement: { replacement: {
match: /(highlighted:\i,)/, match: /(highlighted:\i,)/,
replace: "$1style:{color:`${$self.colorDMList(arguments[0])}`}," replace: "$1style:{color:`${$self.colorDMList(arguments[0])}`},"
}, },
predicate: () => settings.store.dmList,
}, },
{ {
predicate: () => settings.store.dmList,
find: "!1,wrapContent", find: "!1,wrapContent",
replacement: [ replacement: [
{ {
@ -109,6 +108,7 @@ export default definePlugin({
replace: "style:style||{}," replace: "style:style||{},"
}, },
], ],
predicate: () => settings.store.dmList,
}, },
], ],

View file

@ -13,7 +13,8 @@ import {
definePluginSettings, definePluginSettings,
Settings, Settings,
} from "@api/Settings"; } from "@api/Settings";
import { Devs } from "@utils/constants"; import { makeRange } from "@components/PluginSettings/components";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
const presendObject: MessageSendListener = (channelId, msg) => { const presendObject: MessageSendListener = (channelId, msg) => {
@ -21,17 +22,52 @@ const presendObject: MessageSendListener = (channelId, msg) => {
}; };
const settings = definePluginSettings({ const settings = definePluginSettings({
quickDisable: {
type: OptionType.BOOLEAN,
description: "Quick disable. Turns off message modifying without requiring a client reload.",
default: false,
},
blockedWords: { blockedWords: {
type: OptionType.STRING, type: OptionType.STRING,
description: "Words that will not be capitalised", description: "Words that will not be capitalized (comma separated).",
default: "", default: "",
}, },
// fixApostrophes is the only one that defaults to enabled because in the version before this one,
// the other features did not exist / had a bug making them not work.
fixApostrophes: {
type: OptionType.BOOLEAN,
description: "Ensure contractions contain apostrophes.",
default: true,
},
expandContractions: {
type: OptionType.BOOLEAN,
description: "Expand contractions.",
default: false,
},
fixCapitalization: {
type: OptionType.BOOLEAN,
description: "Capitalize sentences.",
default: false,
},
fixPunctuation: {
type: OptionType.BOOLEAN,
description: "Punctate sentences.",
default: false,
},
fixPunctuationFrequency: {
type: OptionType.SLIDER,
description: "Percent period frequency (this majorly annoys some people).",
markers: makeRange(0, 100, 10),
stickToMarkers: false,
default: 100,
}
}); });
export default definePlugin({ export default definePlugin({
name: "PolishWording", name: "PolishWording",
description: "Tweaks your messages to make them look nicer and have better grammar", description: "Tweaks your messages to make them look nicer and have better grammar. See settings",
authors: [Devs.Samwich], authors: [Devs.Samwich, EquicordDevs.WKoA],
dependencies: ["MessageEventsAPI"], dependencies: ["MessageEventsAPI"],
start: () => addMessagePreSendListener(presendObject), start: () => addMessagePreSendListener(presendObject),
stop: () => removeMessagePreSendListener(presendObject), stop: () => removeMessagePreSendListener(presendObject),
@ -39,43 +75,93 @@ export default definePlugin({
}); });
function textProcessing(input: string) { function textProcessing(input: string) {
// Quick disable, without having to reload the client
if (settings.store.quickDisable) return input;
let text = input; let text = input;
text = cap(text);
text = apostrophe(text); // Preserve code blocks
const codeBlockRegex = /```[\s\S]*?```|`[\s\S]*?`/g;
const codeBlocks: string[] = [];
text = text.replace(codeBlockRegex, match => {
codeBlocks.push(match);
return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
});
// Run message through formatters.
if (settings.store.fixApostrophes || settings.store.expandContractions) text = ensureApostrophe(text); // Note: if expanding contractions, fix them first.
if (settings.store.fixCapitalization) text = capitalize(text);
if (settings.store.fixPunctuation && (Math.random() * 100 < settings.store.fixPunctuationFrequency)) text = addPeriods(text);
if (settings.store.expandContractions) text = expandContractions(text);
text = text.replace(/__CODE_BLOCK_(\d+)__/g, (_, index) => codeBlocks[parseInt(index)]);
return text; return text;
} }
function apostrophe(textInput: string): string { // Injecting apostrophe as well as contraction expansion rely on this mapping
const corrected = const contractionsMap: { [key: string]: string; } = {
"wasn't, can't, don't, won't, isn't, aren't, haven't, hasn't, hadn't, doesn't, didn't, shouldn't, wouldn't, couldn't, i'm, you're, he's, she's, it's, they're, that's, who's, what's, there's, here's, how's, where's, when's, why's, let's, you'll, I'll, they'll, it'll, I've, you've, we've, they've, you'd, he'd, she'd, it'd, we'd, they'd, y'all".toLowerCase(); "wasn't": "was not",
const words: string[] = corrected.split(", "); "can't": "cannot",
const wordsInputted = textInput.split(" "); "don't": "do not",
"won't": "will not",
"isn't": "is not",
"aren't": "are not",
"haven't": "have not",
"hasn't": "has not",
"hadn't": "had not",
"doesn't": "does not",
"didn't": "did not",
"shouldn't": "should not",
"wouldn't": "would not",
"couldn't": "could not",
"that's": "that is",
"what's": "what is",
"there's": "there is",
"how's": "how is",
"where's": "where is",
"when's": "when is",
"who's": "who is",
"why's": "why is",
"you'll": "you will",
"i'll": "I will",
"they'll": "they will",
"it'll": "it will",
"i'm": "I am",
"you're": "you are",
"they're": "they are",
"he's": "he is",
"she's": "she is",
"i've": "I have",
"you've": "you have",
"we've": "we have",
"they've": "they have",
"you'd": "you would",
"he'd": "he would",
"she'd": "she would",
"it'd": "it would",
"we'd": "we would",
"they'd": "they would",
"y'all": "you all",
"here's": "here is",
};
wordsInputted.forEach(element => { const missingApostropheMap: { [key: string]: string; } = {};
words.forEach(wordelement => { for (const contraction in contractionsMap) {
if (removeApostrophes(wordelement) === element.toLowerCase()) { const withoutApostrophe = removeApostrophes(contraction.toLowerCase());
wordsInputted[wordsInputted.indexOf(element)] = restoreCap( missingApostropheMap[withoutApostrophe] = contraction;
wordelement,
getCapData(element),
);
}
});
});
return wordsInputted.join(" ");
} }
function getCapData(str: string) { function getCapData(str: string) {
const booleanArray: boolean[] = []; const booleanArray: boolean[] = [];
for (const char of str) { for (const char of str) {
booleanArray.push(char === char.toUpperCase()); if (char.match(/[a-zA-Z]/)) { // Only record capitalization for letters
booleanArray.push(char === char.toUpperCase());
}
} }
return booleanArray; return booleanArray;
} }
function removeApostrophes(str: string): string {
return str.replace(/'/g, "");
}
function restoreCap(str: string, data: boolean[]): string { function restoreCap(str: string, data: boolean[]): string {
let resultString = ""; let resultString = "";
let dataIndex = 0; let dataIndex = 0;
@ -87,30 +173,152 @@ function restoreCap(str: string, data: boolean[]): string {
continue; continue;
} }
const isUppercase = data[dataIndex++]; const isUppercase = data[dataIndex];
resultString += isUppercase ? char.toUpperCase() : char.toLowerCase(); resultString += isUppercase ? char.toUpperCase() : char.toLowerCase();
// Increment index unless the data in shorter than the string, in which case we use the most recent for the rest
if (dataIndex < data.length - 1) dataIndex++;
} }
return resultString; return resultString;
} }
function cap(textInput: string): string { function ensureApostrophe(textInput: string): string {
const sentences = textInput.split(/(?<=\w\.)\s/); // This function makes sure all contractions have apostrophes
const blockedWordsArray: string[] = const potentialContractions = Object.keys(missingApostropheMap);
Settings.plugins.PolishWording.blockedWords.split(", "); if (potentialContractions.length === 0) {
return textInput; // Nothing to check if the map is empty
}
return sentences const findMissingRegex = new RegExp(
.map(element => { `\\b(${potentialContractions.join("|")})\\b`, // Match any of the keys as whole words
if ( "gi" // Global (all occurrences), Case-insensitive
!blockedWordsArray.some(word => );
element.toLowerCase().startsWith(word.toLowerCase()),
) return textInput.replace(findMissingRegex, match => {
) { const lowerCaseMatch = match.toLowerCase();
return element.charAt(0).toUpperCase() + element.slice(1);
} else { if (Object.prototype.hasOwnProperty.call(missingApostropheMap, lowerCaseMatch)) {
return element; const correctContraction = missingApostropheMap[lowerCaseMatch];
} return restoreCap(correctContraction, getCapData(match));
}) }
.join(" "); return match;
});
}
function expandContractions(textInput: string) {
const contractionRegex = new RegExp(
`\\b(${Object.keys(contractionsMap).join("|")})\\b`,
"gi"
);
return textInput.replace(contractionRegex, match => {
const lowerCaseMatch = match.toLowerCase();
if (Object.prototype.hasOwnProperty.call(contractionsMap, lowerCaseMatch)) {
return restoreCap(contractionsMap[lowerCaseMatch], getCapData(match));
}
return match;
});
}
function removeApostrophes(str: string): string {
return str.replace(/'/g, "");
}
function capitalize(textInput: string): string {
// This one split ellipsis
// const sentenceSplitRegex = /((?<!\w\.\w.)(?<!\b[A-Z][a-z]\.)(?<![A-Z]\.)(?<=[.?!])\s+|\n+)/;
// Regex modified from several stack overflows, if you change make sure it's safe against https://devina.io/redos-checker
const sentenceSplitRegex = /((?<!\w\.\w.)(?<!\b[A-Z][a-z]\.)(?<![A-Z]\.)(?<!\.)(?<=[.?!])\s+|\n+)/;
const parts = textInput.split(sentenceSplitRegex);
const filteredParts = parts.filter(part => part !== undefined && part !== null);
const blockedWordsArray: string[] = (Settings.plugins.PolishWording.blockedWords || "")
.split(/,\s?/)
.filter(bw => bw)
.map(bw => bw.toLowerCase());
// Process alternating content and delimiters
let result = "";
for (let i = 0; i < filteredParts.length; i++) {
const element = filteredParts[i];
const isSentence = !sentenceSplitRegex.test(element); // if it matches the delimiter regex, it's a delimiter
if (isSentence) {
// Check if this is just whitespace
if (!element) continue;
else if (element.trim() === "") {
result += element;
continue;
}
// Find the first actual word character for capitalization check
const firstWordMatch = element.match(/^\s*([\w'-]+)/);
const firstWord = firstWordMatch ? firstWordMatch[1].toLowerCase() : "";
const isBlocked = firstWord ? blockedWordsArray.includes(firstWord) : false;
if (
!isBlocked &&
!element.startsWith("http") // Don't break links
) {
// Capitalize the first non-whitespace character (sentence splits can include newlines etc)
result += element.replace(/^(\s*)(\S)/, (match, leadingSpace, firstChar) => {
return leadingSpace + firstChar.toUpperCase();
});
} else {
result += element;
}
} else {
// This a delimiter (whitespace/newline regex), so we'll add it to the string to properly reconstruct without being lossy
if (element) {
result += element;
}
}
}
// We'll fix capitalization of I's
result = result.replace(/\bi[\b']/g, "I");
return result;
}
function addPeriods(textInput: string) {
if (!textInput) {
return "";
}
const lines = textInput.split("\n");
const processedLines: string[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const strippedLine = line.trimEnd();
const urlRegex = /https?:\/\/\S+$|www\.\S+$/;
if (!strippedLine) {
if (i < lines.length - 1) {
processedLines.push("");
}
} else {
const lastChar = strippedLine.slice(-1);
if (
/[A-Za-z0-9]/.test(lastChar) && // If it doesn't already end with punctuation
!urlRegex.test(strippedLine) // If it doesn't end with a link
) {
processedLines.push(strippedLine + ".");
continue;
}
processedLines.push(strippedLine);
}
}
return processedLines.join("\n");
} }

View file

@ -141,7 +141,7 @@ const settings = definePluginSettings({
export default definePlugin({ export default definePlugin({
name: "RandomVoice", name: "RandomVoice",
description: "Adds a Button near the Mute button to join a random voice call.", description: "Adds a Button near the Mute button to join a random voice call.",
authors: [EquicordDevs.omaw], authors: [EquicordDevs.xijexo, EquicordDevs.omaw],
patches: [ patches: [
{ {
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}", find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
@ -725,4 +725,4 @@ function autoCamera() {
camera.click(); camera.click();
} }
}, 50); }, 50);
} }

View file

@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab"; import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
import { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, EQUIBOP_CONTRIB_ROLE_ID, EQUICORD_TEAM, GUILD_ID, SUPPORT_CHANNEL_ID, SUPPORT_CHANNEL_IDS, VC_CONTRIB_ROLE_ID, VC_DONOR_ROLE_ID, VC_GUILD_ID, VC_KNOWN_ISSUES_CHANNEL_ID, VC_REGULAR_ROLE_ID, VC_SUPPORT_CHANNEL_ID, VENBOT_USER_ID, VENCORD_CONTRIB_ROLE_ID } from "@utils/constants"; import { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, EQUCORD_HELPERS, EQUIBOP_CONTRIB_ROLE_ID, EQUICORD_TEAM, GUILD_ID, SUPPORT_CHANNEL_ID, VC_CONTRIB_ROLE_ID, VC_DONOR_ROLE_ID, VC_GUILD_ID, VC_REGULAR_ROLE_ID, VC_SUPPORT_CHANNEL_ID, VENCORD_CONTRIB_ROLE_ID } from "@utils/constants";
import { sendMessage } from "@utils/discord"; import { sendMessage } from "@utils/discord";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins"; import { Margins } from "@utils/margins";
@ -32,7 +32,7 @@ import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text"; import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { checkForUpdates, isOutdated, update } from "@utils/updater"; import { checkForUpdates, isOutdated, update } from "@utils/updater";
import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common"; import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
import { JSX } from "react"; import { JSX } from "react";
import gitHash from "~git-hash"; import gitHash from "~git-hash";
@ -196,7 +196,8 @@ export default definePlugin({
flux: { flux: {
async CHANNEL_SELECT({ channelId }) { async CHANNEL_SELECT({ channelId }) {
if (!SUPPORT_CHANNEL_IDS.includes(channelId)) return; const isSupportChannel = channelId === SUPPORT_CHANNEL_ID;
if (!isSupportChannel) return;
const selfId = UserStore.getCurrentUser()?.id; const selfId = UserStore.getCurrentUser()?.id;
if (!selfId || isPluginDev(selfId) || isEquicordPluginDev(selfId)) return; if (!selfId || isPluginDev(selfId) || isEquicordPluginDev(selfId)) return;
@ -281,11 +282,12 @@ export default definePlugin({
renderMessageAccessory(props) { renderMessageAccessory(props) {
const buttons = [] as JSX.Element[]; const buttons = [] as JSX.Element[];
const equicordSupport = GuildMemberStore.getMember(GUILD_ID, props.message.author.id)?.roles?.includes(EQUCORD_HELPERS);
const shouldAddUpdateButton = const shouldAddUpdateButton =
!IS_UPDATER_DISABLED !IS_UPDATER_DISABLED
&& ( && (
(props.channel.id === VC_KNOWN_ISSUES_CHANNEL_ID) || (props.channel.id === SUPPORT_CHANNEL_ID && equicordSupport)
(props.channel.id === VC_SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID)
) )
&& props.message.content?.includes("update"); && props.message.content?.includes("update");
@ -311,7 +313,7 @@ export default definePlugin({
); );
} }
if (props.channel.id === SUPPORT_CHANNEL_ID) { if (props.channel.id === SUPPORT_CHANNEL_ID && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel)) {
if (props.message.content.includes("/equicord-debug") || props.message.content.includes("/equicord-plugins")) { if (props.message.content.includes("/equicord-debug") || props.message.content.includes("/equicord-plugins")) {
buttons.push( buttons.push(
<Button <Button
@ -334,7 +336,7 @@ export default definePlugin({
); );
} }
if (props.message.author.id === VENBOT_USER_ID) { if (equicordSupport) {
const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || ""); const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || "");
if (match) { if (match) {
buttons.push( buttons.push(

View file

@ -24,8 +24,7 @@ import { Clipboard, Toasts } from "@webpack/common";
export default definePlugin({ export default definePlugin({
name: "BetterRoleDot", name: "BetterRoleDot",
authors: [Devs.Ven, Devs.AutumnVN], authors: [Devs.Ven, Devs.AutumnVN],
description: description: "Copy role colour on RoleDot (accessibility setting) click. Also allows using both RoleDot and coloured names simultaneously",
"Copy role colour on RoleDot (accessibility setting) click. Also allows using both RoleDot and coloured names simultaneously",
patches: [ patches: [
{ {

View file

@ -27,12 +27,6 @@ export default definePlugin({
{ {
find: '"ChannelAttachButton"', find: '"ChannelAttachButton"',
replacement: [ replacement: [
{
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,30}?\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
noWarn: true
},
{ {
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,100}\},(\i)\).{0,100}children:\i/, match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),.{0,100}\},(\i)\).{0,100}children:\i/,
replace: "$&,onClick:$1,onContextMenu:$2.onClick,", replace: "$&,onClick:$1,onContextMenu:$2.onClick,",

View file

@ -49,18 +49,6 @@ export default definePlugin({
{ {
find: ".decorationGridItem,", find: ".decorationGridItem,",
replacement: [ replacement: [
{
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /(?<==)\i=>{let{children.{20,200}decorationGridItem/,
replace: "$self.DecorationGridItem=$&",
noWarn: true
},
{
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /(?<==)\i=>{let{user:\i,avatarDecoration/,
replace: "$self.DecorationGridDecoration=$&",
noWarn: true
},
{ {
match: /(?<==)\i=>{var{children.{20,200}decorationGridItem/, match: /(?<==)\i=>{var{children.{20,200}decorationGridItem/,
replace: "$self.DecorationGridItem=$&", replace: "$self.DecorationGridItem=$&",

View file

@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { getCustomColorString } from "@equicordplugins/customUserColors";
import { hash as h64 } from "@intrnl/xxhash64"; import { hash as h64 } from "@intrnl/xxhash64";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
@ -66,7 +67,7 @@ export default definePlugin({
{ {
find: '="SYSTEM_TAG"', find: '="SYSTEM_TAG"',
replacement: { replacement: {
match: /(?<=className:\i\.username,style:.{0,50}:void 0,)/, match: /(?<=\i.gradientClassName]\),style:.{0,80}:void 0,)/,
replace: "style:{color:$self.calculateNameColorForMessageContext(arguments[0])}," replace: "style:{color:$self.calculateNameColorForMessageContext(arguments[0])},"
} }
}, },
@ -84,30 +85,36 @@ export default definePlugin({
const userId: string | undefined = context?.message?.author?.id; const userId: string | undefined = context?.message?.author?.id;
const colorString = context?.author?.colorString; const colorString = context?.author?.colorString;
const color = calculateNameColorForUser(userId); const color = calculateNameColorForUser(userId);
const customColor = userId && Settings.plugins.CustomUserColors.enabled ? getCustomColorString(userId, true) : null;
// Color preview in role settings // Color preview in role settings
if (context?.message?.channel_id === "1337" && userId === "313337") if (context?.message?.channel_id === "1337" && userId === "313337")
return colorString; return customColor ?? colorString;
if (settings.store.applyColorOnlyInDms && !context?.channel?.isPrivate()) { if (settings.store.applyColorOnlyInDms && !context?.channel?.isPrivate()) {
return colorString; return customColor ?? colorString;
} }
return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString) if (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString) {
? color return customColor ?? color;
: colorString; } else {
return customColor ?? colorString;
}
}, },
calculateNameColorForListContext(context: any) { calculateNameColorForListContext(context: any) {
const id = context?.user?.id; const id = context?.user?.id;
const colorString = context?.colorString; const colorString = context?.colorString;
const color = calculateNameColorForUser(id); const color = calculateNameColorForUser(id);
const customColor = id && Settings.plugins.CustomUserColors.enabled ? getCustomColorString(id, true) : null;
if (settings.store.applyColorOnlyInDms && !context?.channel?.isPrivate()) { if (settings.store.applyColorOnlyInDms && !context?.channel?.isPrivate()) {
return colorString; return customColor ?? colorString;
} }
return (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString) if (!settings.store.applyColorOnlyToUsersWithoutColor || !colorString) {
? color return customColor ?? color;
: colorString; } else {
return customColor ?? colorString;
}
} }
}); });

View file

@ -66,12 +66,6 @@ export default definePlugin({
{ {
find: "{isSidebarVisible:", find: "{isSidebarVisible:",
replacement: [ replacement: [
{
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /(?<=let\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
replace: ":[$1?.startsWith('members')?$self.render():null,$2",
noWarn: true
},
{ {
match: /(?<=var\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/, match: /(?<=var\{className:(\i),.+?children):\[(\i\.useMemo[^}]+"aria-multiselectable")/,
replace: ":[$1?.startsWith('members')?$self.render():null,$2", replace: ":[$1?.startsWith('members')?$self.render():null,$2",

View file

@ -1,6 +1,6 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors * Copyright (c) 2025 Vendicated and contributors
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -26,17 +26,16 @@ export default definePlugin({
patches: [ patches: [
{ {
find: ".nsfwAllowed=null", find: ".nsfwAllowed=null",
replacement: { replacement: [
match: /(?<=\.nsfwAllowed=)null!==.+?(?=[,;])/, {
replace: "!0", match: /(?<=\.nsfwAllowed=)null!==.+?(?=[,;])/,
}, replace: "true",
}, },
{ {
find: ".ageVerificationStatus=null", match: /(?<=\.ageVerificationStatus=)null!==.+?(?=[,;])/,
replacement: { replace: "3", // VERIFIED_ADULT
match: /(?<=\.ageVerificationStatus=)null!==.+?(?=[,;])/, }
replace: "3", ],
}, }
},
], ],
}); });

View file

@ -30,12 +30,12 @@ export default definePlugin({
// the second is the four guild preview icons // the second is the four guild preview icons
// always show this one (the plain icons) // always show this one (the plain icons)
{ {
match: /\(\w\|\|\w\)&&(\(.{0,40}\(.{1,3}\.animated)/, match: /\(\i\|\|\i\)&&(\(.{0,40}\(\i\.animated)/,
replace: "$1", replace: "$1",
}, },
// and never show this one (the guild preview icons) // and never show this one (the guild preview icons)
{ {
match: /\(\w\|\|!\w\)&&(\(.{0,40}\(.{1,3}\.animated)/, match: /\(\i\|\|!\i\)&&(\(.{0,40}\(\i\.animated)/,
replace: "false&&$1", replace: "false&&$1",
} }
] ]

View file

@ -84,12 +84,6 @@ export default definePlugin({
{ {
find: ".USER_MENTION)", find: ".USER_MENTION)",
replacement: [ replacement: [
{
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /onContextMenu:\i,color:\i,\.\.\.\i(?=,children:)(?<=user:(\i),channel:(\i).{0,500}?)/,
replace: "$&,color:$self.getColorInt($1?.id,$2?.id)",
noWarn: true
},
{ {
match: /(?<=onContextMenu:\i,color:)\i(?=\},\i\),\{children)(?<=user:(\i),channel:(\i).{0,500}?)/, match: /(?<=onContextMenu:\i,color:)\i(?=\},\i\),\{children)(?<=user:(\i),channel:(\i).{0,500}?)/,
replace: "$self.getColorInt($1?.id,$2?.id)", replace: "$self.getColorInt($1?.id,$2?.id)",

View file

@ -56,7 +56,7 @@ export default definePlugin({
{ {
find: '?"@":""', find: '?"@":""',
replacement: { replacement: {
match: /(?<=children:)\(\i\?"@":""\)\+\i(?=,|\})/, match: /(?<=onContextMenu:\i,children:)\i\+\i/,
replace: "$self.renderUsername(arguments[0])" replace: "$self.renderUsername(arguments[0])"
} }
}, },

View file

@ -310,7 +310,6 @@ function Info({ track }: { track: Track; }) {
{track.artists.some(a => a.name) && ( {track.artists.some(a => a.name) && (
<Forms.FormText variant="text-sm/normal" className={cl(["ellipoverflow", "secondary-song-info"])}> <Forms.FormText variant="text-sm/normal" className={cl(["ellipoverflow", "secondary-song-info"])}>
<span className={cl("song-info-prefix")}>by&nbsp;</span> <span className={cl("song-info-prefix")}>by&nbsp;</span>
by&nbsp;
{track.artists.map((a, i) => ( {track.artists.map((a, i) => (
<React.Fragment key={a.name}> <React.Fragment key={a.name}>
<span <span
@ -329,7 +328,6 @@ function Info({ track }: { track: Track; }) {
{track.album.name && ( {track.album.name && (
<Forms.FormText variant="text-sm/normal" className={cl(["ellipoverflow", "secondary-song-info"])}> <Forms.FormText variant="text-sm/normal" className={cl(["ellipoverflow", "secondary-song-info"])}>
<span className={cl("song-info-prefix")}>on&nbsp;</span> <span className={cl("song-info-prefix")}>on&nbsp;</span>
on&nbsp;
<span <span
id={cl("album-title")} id={cl("album-title")}
className={cl("album")} className={cl("album")}

View file

@ -12,8 +12,7 @@
display: none; display: none;
} }
.vc-spotify-artist, .vc-spotify-artist, .vc-spotify-album {
.vc-spotify-album {
color: var(--header-primary); color: var(--header-primary);
} }
@ -27,26 +26,26 @@
width: 100%; width: 100%;
} }
#vc-spotify-progress-bar>[class^="slider"] { #vc-spotify-progress-bar > [class^="slider"] {
flex-grow: 1; flex-grow: 1;
width: 100%; width: 100%;
padding: 0 !important; padding: 0 !important;
} }
#vc-spotify-progress-bar>[class^="slider"] [class^="bar"] { #vc-spotify-progress-bar > [class^="slider"] [class^="bar"] {
height: 3px !important; height: 3px !important;
top: calc(12px - 4px / 2 + var(--bar-offset)); top: calc(12px - 4px / 2 + var(--bar-offset));
} }
#vc-spotify-progress-bar>[class^="slider"] [class^="barFill"] { #vc-spotify-progress-bar > [class^="slider"] [class^="barFill"] {
background-color: var(--interactive-active); background-color: var(--interactive-active);
} }
#vc-spotify-progress-bar>[class^="slider"]:hover [class^="barFill"] { #vc-spotify-progress-bar > [class^="slider"]:hover [class^="barFill"] {
background-color: var(--vc-spotify-green); background-color: var(--vc-spotify-green);
} }
#vc-spotify-progress-bar>[class^="slider"] [class^="grabber"] { #vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] {
background-color: var(--interactive-active); background-color: var(--interactive-active);
width: 16px !important; width: 16px !important;
height: 16px !important; height: 16px !important;
@ -68,15 +67,11 @@
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
.vc-spotify-repeat-context, .vc-spotify-repeat-context, .vc-spotify-repeat-track, .vc-spotify-shuffle-on {
.vc-spotify-repeat-track,
.vc-spotify-shuffle-on {
background-color: var(--vc-spotify-green-90); background-color: var(--vc-spotify-green-90);
} }
.vc-spotify-repeat-context:hover, .vc-spotify-repeat-context:hover, .vc-spotify-repeat-track:hover, .vc-spotify-shuffle-on:hover {
.vc-spotify-repeat-track:hover,
.vc-spotify-shuffle-on:hover {
background-color: var(--vc-spotify-green-80); background-color: var(--vc-spotify-green-80);
} }
} }

View file

@ -28,14 +28,6 @@ export default definePlugin({
patches: [{ patches: [{
find: "#{intl::ACTIVITY_SETTINGS}", find: "#{intl::ACTIVITY_SETTINGS}",
replacement: [ replacement: [
{
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+}\)}\))/,
replace: (_, commaOrSemi, settings, elements) => "" +
`${commaOrSemi}${settings}?.[0]==="CHANGELOG"` +
`&&${elements}.push({section:"StartupTimings",label:"Startup Timings",element:$self.StartupTimingPage})`,
noWarn: true
},
{ {
match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+\)\)\}\))(?=\)\})/, match: /(?<=}\)([,;])(\i\.settings)\.forEach.+?(\i)\.push.+\)\)\}\))(?=\)\})/,
replace: (_, commaOrSemi, settings, elements) => "" + replace: (_, commaOrSemi, settings, elements) => "" +

View file

@ -55,7 +55,7 @@ export default definePlugin({
], ],
getAvatarStyles(src: string | null) { getAvatarStyles(src: string | null) {
if (src == null || src.startsWith("data:")) return {}; if (!src || src.startsWith("data:")) return {};
return Object.fromEntries( return Object.fromEntries(
[128, 256, 512, 1024, 2048, 4096].map(size => [ [128, 256, 512, 1024, 2048, 4096].map(size => [

View file

@ -42,13 +42,6 @@ export default definePlugin({
{ {
find: '="SYSTEM_TAG"', find: '="SYSTEM_TAG"',
replacement: [ replacement: [
{
// Add next to username (compact mode)
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /className:\i\(\)\(\i\.className(?:,\i\.clickable)?,\i\)}\),(?=\i)/g,
replace: "$&$self.CompactPronounsChatComponentWrapper(arguments[0]),",
noWarn: true
},
{ {
// Add next to username (compact mode) // Add next to username (compact mode)
match: /className:\i\(\)\(\i\.className(?:,\i\.clickable)?,\i\)}\)\),(?=\i)/g, match: /className:\i\(\)\(\i\.className(?:,\i\.clickable)?,\i\)}\)\),(?=\i)/g,

View file

@ -194,12 +194,6 @@ export default definePlugin({
{ {
find: ".overlay:void 0,status:", find: ".overlay:void 0,status:",
replacement: [ replacement: [
{
// FIXME(Bundler spread transform related): Remove old compatiblity once enough time has passed, if they don't revert
match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/,
replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)},",
noWarn: true
},
{ {
match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",.{0,100}className:\i,/, match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",.{0,100}className:\i,/,
replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)},", replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openAvatar($1)},",

View file

@ -53,13 +53,16 @@ const settings = definePluginSettings({
addBack: { addBack: {
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
description: "Add back the Discord context menus for images, links and the chat input bar", description: "Add back the Discord context menus for images, links and the chat input bar",
default: false,
restartNeeded: true,
// Web slate menu has proper spellcheck suggestions and image context menu is also pretty good, // 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 // so disable this by default. Vesktop just doesn't, so we force enable it there
default: result, hidden: result,
restartNeeded: true
} }
}); });
const shouldAddBackMenus = () => result || settings.store.addBack;
const MEDIA_PROXY_URL = "https://media.discordapp.net"; const MEDIA_PROXY_URL = "https://media.discordapp.net";
const CDN_URL = "cdn.discordapp.com"; const CDN_URL = "cdn.discordapp.com";
@ -92,7 +95,7 @@ export default definePlugin({
settings, settings,
start() { start() {
if (settings.store.addBack) { if (shouldAddBackMenus()) {
window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb); window.removeEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackWeb);
window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative); window.addEventListener("contextmenu", ctxMenuCallbacks.contextMenuCallbackNative);
this.changedListeners = true; this.changedListeners = true;
@ -155,7 +158,7 @@ export default definePlugin({
{ {
find: 'navId:"image-context"', find: 'navId:"image-context"',
all: true, all: true,
predicate: () => settings.store.addBack, predicate: shouldAddBackMenus,
replacement: { replacement: {
// return IS_DESKTOP ? React.createElement(Menu, ...) // return IS_DESKTOP ? React.createElement(Menu, ...)
match: /return \i\.\i(?=\?|&&)/, match: /return \i\.\i(?=\?|&&)/,
@ -166,7 +169,7 @@ export default definePlugin({
// Add back link context menu // Add back link context menu
{ {
find: '"interactionUsernameProfile"', find: '"interactionUsernameProfile"',
predicate: () => settings.store.addBack, predicate: shouldAddBackMenus,
replacement: { replacement: {
match: /if\((?="A"===\i\.tagName&&""!==\i\.textContent)/, match: /if\((?="A"===\i\.tagName&&""!==\i\.textContent)/,
replace: "if(false&&" replace: "if(false&&"
@ -176,7 +179,7 @@ export default definePlugin({
// Add back slate / text input context menu // Add back slate / text input context menu
{ {
find: 'getElementById("slate-toolbar"', find: 'getElementById("slate-toolbar"',
predicate: () => settings.store.addBack, predicate: shouldAddBackMenus,
replacement: { replacement: {
match: /(?<=handleContextMenu\(\i\)\{.{0,200}isPlatformEmbedded)\)/, match: /(?<=handleContextMenu\(\i\)\{.{0,200}isPlatformEmbedded)\)/,
replace: "||true)" replace: "||true)"
@ -184,7 +187,7 @@ export default definePlugin({
}, },
{ {
find: ".SLASH_COMMAND_SUGGESTIONS_TOGGLED,{", find: ".SLASH_COMMAND_SUGGESTIONS_TOGGLED,{",
predicate: () => settings.store.addBack, predicate: shouldAddBackMenus,
replacement: [ replacement: [
{ {
// if (!IS_DESKTOP) return null; // if (!IS_DESKTOP) return null;
@ -200,7 +203,7 @@ export default definePlugin({
}, },
{ {
find: '"add-to-dictionary"', find: '"add-to-dictionary"',
predicate: () => settings.store.addBack, predicate: shouldAddBackMenus,
replacement: { replacement: {
match: /let\{text:\i=""/, match: /let\{text:\i=""/,
replace: "return [null,null];$&" replace: "return [null,null];$&"

View file

@ -24,6 +24,7 @@ export const GUILD_ID = "1173279886065029291";
export const DONOR_ROLE_ID = "1173316879083896912"; export const DONOR_ROLE_ID = "1173316879083896912";
export const CONTRIB_ROLE_ID = "1222677964760682556"; export const CONTRIB_ROLE_ID = "1222677964760682556";
export const EQUICORD_TEAM = "1173520023239786538"; export const EQUICORD_TEAM = "1173520023239786538";
export const EQUCORD_HELPERS = "1326406112144265257";
export const EQUIBOP_CONTRIB_ROLE_ID = "1287079931645263968"; export const EQUIBOP_CONTRIB_ROLE_ID = "1287079931645263968";
export const VENCORD_CONTRIB_ROLE_ID = "1173343399470964856"; export const VENCORD_CONTRIB_ROLE_ID = "1173343399470964856";
@ -34,6 +35,7 @@ export const VENBOT_USER_ID = "1017176847865352332";
export const VC_DONOR_ROLE_ID = "1042507929485586532"; export const VC_DONOR_ROLE_ID = "1042507929485586532";
export const VC_CONTRIB_ROLE_ID = "1026534353167208489"; export const VC_CONTRIB_ROLE_ID = "1026534353167208489";
export const VC_REGULAR_ROLE_ID = "1026504932959977532"; export const VC_REGULAR_ROLE_ID = "1026504932959977532";
export const VC_SUPPORT_CATEGORY_ID = "1108135649699180705";
export const VC_KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920"; export const VC_KNOWN_ISSUES_CHANNEL_ID = "1222936386626129920";
export const GUILD_IDS = [GUILD_ID, VC_GUILD_ID]; export const GUILD_IDS = [GUILD_ID, VC_GUILD_ID];
@ -807,6 +809,10 @@ export const EquicordDevs = Object.freeze({
name: "Hen", name: "Hen",
id: 279266228151779329n id: 279266228151779329n
}, },
Crxa: {
name: "Crxa",
id: 920290194886914069n
},
vmohammad: { vmohammad: {
name: "vMohammad", name: "vMohammad",
id: 921098159348924457n id: 921098159348924457n
@ -1020,10 +1026,18 @@ export const EquicordDevs = Object.freeze({
name: "talhakf", name: "talhakf",
id: 1140716160560676976n id: 1140716160560676976n
}, },
xijexo: {
name: "xijexo",
id: 1284113557201620995n
},
omaw: { omaw: {
name: "omaw", name: "omaw",
id: 1155026301791514655n id: 1155026301791514655n
}, },
WKoA: {
name: "WKoA",
id: 724416180097384498n
},
} satisfies Record<string, Dev>); } satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly // iife so #__PURE__ works correctly

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, PropsWithRef, ReactNode, Ref } from "react"; import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref } from "react";
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code"; export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
@ -245,7 +245,8 @@ export type TextInput = ComponentType<PropsWithChildren<{
onChange?(value: string, name?: string): void; onChange?(value: string, name?: string): void;
placeholder?: string; placeholder?: string;
editable?: boolean; editable?: boolean;
maxLength?: number; /** defaults to 999. Pass null to disable this default */
maxLength?: number | null;
error?: string; error?: string;
inputClassName?: string; inputClassName?: string;
@ -257,13 +258,14 @@ export type TextInput = ComponentType<PropsWithChildren<{
/** TextInput.Sizes.DEFAULT */ /** TextInput.Sizes.DEFAULT */
size?: string; size?: string;
} & Omit<HTMLProps<HTMLInputElement>, "onChange">>> & { } & Omit<HTMLProps<HTMLInputElement>, "onChange" | "maxLength">>> & {
Sizes: Record<"DEFAULT" | "MINI", string>; Sizes: Record<"DEFAULT" | "MINI", string>;
}; };
export type TextArea = ComponentType<PropsWithRef<Omit<HTMLProps<HTMLTextAreaElement>, "onChange"> & { export type TextArea = ComponentType<Omit<HTMLProps<HTMLTextAreaElement>, "onChange"> & {
onChange(v: string): void; onChange(v: string): void;
}>>; }>;
export interface SelectOption { export interface SelectOption {
disabled?: boolean; disabled?: boolean;
value: any; value: any;