- {activity && currentActivity?.id === activity.id ? (
-
- ) : (
-
- )}
- {activities.length > 1 &&
-
-
{({
- onMouseEnter,
- onMouseLeave
- }) => {
- return {
- const index = activities.indexOf(currentActivity!);
- if (index - 1 >= 0) {
- setCurrentActivity(activities[index - 1]);
- } else {
- setCurrentActivity(activities[activities.length - 1]);
- }
- }}
- >
-
- ;
- }}
-
-
- {activities.map((activity, index) => (
-
setCurrentActivity(activity)}
- className={`dot ${currentActivity === activity ? "selected" : ""}`} />
- ))}
-
-
-
{({
- onMouseEnter,
- onMouseLeave
- }) => {
- return {
- const index = activities.indexOf(currentActivity!);
- if (index + 1 < activities.length) {
- setCurrentActivity(activities[index + 1]);
- } else {
- setCurrentActivity(activities[0]);
- }
- }}
- >
- = activities.length - 1}
- direction="right" />
- ;
- }}
-
- }
-
-
- );
- } else {
- return (
-
{ }}>
-
- {activities.map((activity, index) =>
- index === 0 ? (
-
) : (
-
- ))}
-
-
- );
- }
- },
+ showAllActivitiesComponent,
patches: [
{
diff --git a/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx b/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx
new file mode 100644
index 00000000..9e4b7893
--- /dev/null
+++ b/src/equicordplugins/betterActivities/patch-helpers/activityList.tsx
@@ -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:
![]()
,
+ tooltip:
+ });
+ }
+ }
+
+ 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:
,
+ tooltip:
+ };
+
+ 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
+
+ {icons.map(({ iconElement, tooltip }, i) => (
+
+ {tooltip ?
+ {({ onMouseEnter, onMouseLeave }) => (
+
+ {iconElement}
+
+ )}
+ : iconElement}
+
+ ))}
+
+ ;
+ } 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
;
+ }
+ }
+
+ return null;
+}
diff --git a/src/equicordplugins/betterActivities/patch-helpers/popout.tsx b/src/equicordplugins/betterActivities/patch-helpers/popout.tsx
new file mode 100644
index 00000000..e009cc9e
--- /dev/null
+++ b/src/equicordplugins/betterActivities/patch-helpers/popout.tsx
@@ -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
): JSX.Element | null {
+ const currentUser = UserStore.getCurrentUser();
+ if (!currentUser) return null;
+
+ const [currentActivity, setCurrentActivity] = useState(
+ 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 (
+
+
+ {activity && currentActivity?.id === activity.id ? (
+
+ ) : (
+
+ )}
+ {activities.length > 1 && currentActivity && (
+
+ )}
+
+
+ );
+ } else {
+ return (
+
+
+ {activities.map((activity, index) =>
+ index === 0 ? (
+
) : (
+
+ ))}
+
+
+ );
+ }
+}
diff --git a/src/equicordplugins/betterActivities/settings.tsx b/src/equicordplugins/betterActivities/settings.tsx
new file mode 100644
index 00000000..b27d6e4a
--- /dev/null
+++ b/src/equicordplugins/betterActivities/settings.tsx
@@ -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: () => (
+
+ ),
+ },
+ 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",
+ },
+ ]
+ }
+});
diff --git a/src/equicordplugins/betterActivities/styles.css b/src/equicordplugins/betterActivities/styles.css
index 32c80f55..1ab6d3ad 100644
--- a/src/equicordplugins/betterActivities/styles.css
+++ b/src/equicordplugins/betterActivities/styles.css
@@ -19,12 +19,8 @@
border-radius: 50%;
}
-[class*="tooltip_"]:has(.vc-bactivities-activity-tooltip) {
- max-width: 280px;
-}
-
.vc-bactivities-activity-tooltip {
- margin: -5px;
+ padding: 1px;
}
.vc-bactivities-caret-left,
diff --git a/src/equicordplugins/betterActivities/types.ts b/src/equicordplugins/betterActivities/types.ts
index 8f24dae5..78d21bfd 100644
--- a/src/equicordplugins/betterActivities/types.ts
+++ b/src/equicordplugins/betterActivities/types.ts
@@ -1,9 +1,10 @@
/*
* 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
*/
+import { User } from "discord-types/general";
import { CSSProperties, ImgHTMLAttributes, JSX } from "react";
export interface Timestamp {
@@ -80,3 +81,36 @@ export interface ActivityListIcon {
export interface IconCSSProperties extends CSSProperties {
"--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;
+}
diff --git a/src/equicordplugins/betterActivities/utils.tsx b/src/equicordplugins/betterActivities/utils.tsx
new file mode 100644
index 00000000..4798ad44
--- /dev/null
+++ b/src/equicordplugins/betterActivities/utils.tsx
@@ -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;
+} = findByPropsLazy("fetchApplication");
+
+const fetchedApplications = new Map();
+
+const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png"; // TODO: replace with "renderXboxImage"?
+
+export const ActivityView = findComponentByCodeLazy('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;
+}
diff --git a/src/equicordplugins/customUserColors/index.tsx b/src/equicordplugins/customUserColors/index.tsx
index 51679a07..57134557 100644
--- a/src/equicordplugins/customUserColors/index.tsx
+++ b/src/equicordplugins/customUserColors/index.tsx
@@ -81,23 +81,22 @@ export default definePlugin({
patches: [
{
// this also affects name headers in chats outside of servers
- find: ".USERNAME),{",
+ find: '="SYSTEM_TAG"',
replacement: {
- match: /style:"username"===.{0,25}void 0/,
- replace: "style:{color:$self.colorIfServer(arguments[0])}"
+ match: /(?<=\i.gradientClassName]\),style:.{0,80}:void 0,)/,
+ replace: "style:{color:$self.colorIfServer(arguments[0])},"
},
- noWarn: true,
+ predicate: () => !Settings.plugins.IrcColors.enabled
},
{
- predicate: () => settings.store.dmList,
find: "PrivateChannel.renderAvatar",
replacement: {
match: /(highlighted:\i,)/,
replace: "$1style:{color:`${$self.colorDMList(arguments[0])}`},"
},
+ predicate: () => settings.store.dmList,
},
{
- predicate: () => settings.store.dmList,
find: "!1,wrapContent",
replacement: [
{
@@ -109,6 +108,7 @@ export default definePlugin({
replace: "style:style||{},"
},
],
+ predicate: () => settings.store.dmList,
},
],
diff --git a/src/equicordplugins/polishWording/index.ts b/src/equicordplugins/polishWording/index.ts
index b9605536..142337d5 100644
--- a/src/equicordplugins/polishWording/index.ts
+++ b/src/equicordplugins/polishWording/index.ts
@@ -13,7 +13,8 @@ import {
definePluginSettings,
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";
const presendObject: MessageSendListener = (channelId, msg) => {
@@ -21,17 +22,52 @@ const presendObject: MessageSendListener = (channelId, msg) => {
};
const settings = definePluginSettings({
+ quickDisable: {
+ type: OptionType.BOOLEAN,
+ description: "Quick disable. Turns off message modifying without requiring a client reload.",
+ default: false,
+ },
+
blockedWords: {
type: OptionType.STRING,
- description: "Words that will not be capitalised",
+ description: "Words that will not be capitalized (comma separated).",
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({
name: "PolishWording",
- description: "Tweaks your messages to make them look nicer and have better grammar",
- authors: [Devs.Samwich],
+ description: "Tweaks your messages to make them look nicer and have better grammar. See settings",
+ authors: [Devs.Samwich, EquicordDevs.WKoA],
dependencies: ["MessageEventsAPI"],
start: () => addMessagePreSendListener(presendObject),
stop: () => removeMessagePreSendListener(presendObject),
@@ -39,43 +75,93 @@ export default definePlugin({
});
function textProcessing(input: string) {
+ // Quick disable, without having to reload the client
+ if (settings.store.quickDisable) return 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;
}
-function apostrophe(textInput: string): string {
- const corrected =
- "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();
- const words: string[] = corrected.split(", ");
- const wordsInputted = textInput.split(" ");
+// Injecting apostrophe as well as contraction expansion rely on this mapping
+const contractionsMap: { [key: string]: string; } = {
+ "wasn't": "was not",
+ "can't": "cannot",
+ "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 => {
- words.forEach(wordelement => {
- if (removeApostrophes(wordelement) === element.toLowerCase()) {
- wordsInputted[wordsInputted.indexOf(element)] = restoreCap(
- wordelement,
- getCapData(element),
- );
- }
- });
- });
- return wordsInputted.join(" ");
+const missingApostropheMap: { [key: string]: string; } = {};
+for (const contraction in contractionsMap) {
+ const withoutApostrophe = removeApostrophes(contraction.toLowerCase());
+ missingApostropheMap[withoutApostrophe] = contraction;
}
function getCapData(str: string) {
const booleanArray: boolean[] = [];
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;
}
-function removeApostrophes(str: string): string {
- return str.replace(/'/g, "");
-}
-
function restoreCap(str: string, data: boolean[]): string {
let resultString = "";
let dataIndex = 0;
@@ -87,30 +173,152 @@ function restoreCap(str: string, data: boolean[]): string {
continue;
}
- const isUppercase = data[dataIndex++];
+ const isUppercase = data[dataIndex];
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;
}
-function cap(textInput: string): string {
- const sentences = textInput.split(/(?<=\w\.)\s/);
+function ensureApostrophe(textInput: string): string {
+ // This function makes sure all contractions have apostrophes
- const blockedWordsArray: string[] =
- Settings.plugins.PolishWording.blockedWords.split(", ");
+ const potentialContractions = Object.keys(missingApostropheMap);
+ if (potentialContractions.length === 0) {
+ return textInput; // Nothing to check if the map is empty
+ }
- return sentences
- .map(element => {
- if (
- !blockedWordsArray.some(word =>
- element.toLowerCase().startsWith(word.toLowerCase()),
- )
- ) {
- return element.charAt(0).toUpperCase() + element.slice(1);
- } else {
- return element;
- }
- })
- .join(" ");
+ const findMissingRegex = new RegExp(
+ `\\b(${potentialContractions.join("|")})\\b`, // Match any of the keys as whole words
+ "gi" // Global (all occurrences), Case-insensitive
+ );
+
+ return textInput.replace(findMissingRegex, match => {
+ const lowerCaseMatch = match.toLowerCase();
+
+ if (Object.prototype.hasOwnProperty.call(missingApostropheMap, lowerCaseMatch)) {
+ const correctContraction = missingApostropheMap[lowerCaseMatch];
+ return restoreCap(correctContraction, getCapData(match));
+ }
+ 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 = /((? 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");
}
diff --git a/src/equicordplugins/randomVoice/index.tsx b/src/equicordplugins/randomVoice/index.tsx
index 78f97a96..2b5080c1 100644
--- a/src/equicordplugins/randomVoice/index.tsx
+++ b/src/equicordplugins/randomVoice/index.tsx
@@ -141,7 +141,7 @@ const settings = definePluginSettings({
export default definePlugin({
name: "RandomVoice",
description: "Adds a Button near the Mute button to join a random voice call.",
- authors: [EquicordDevs.omaw],
+ authors: [EquicordDevs.xijexo, EquicordDevs.omaw],
patches: [
{
find: "#{intl::ACCOUNT_SPEAKING_WHILE_MUTED}",
@@ -725,4 +725,4 @@ function autoCamera() {
camera.click();
}
}, 50);
-}
\ No newline at end of file
+}
diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx
index 1a7a7aca..3b299b40 100644
--- a/src/plugins/_core/supportHelper.tsx
+++ b/src/plugins/_core/supportHelper.tsx
@@ -22,7 +22,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
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 { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
@@ -32,7 +32,7 @@ import { onlyOnce } from "@utils/onlyOnce";
import { makeCodeblock } from "@utils/text";
import definePlugin from "@utils/types";
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 gitHash from "~git-hash";
@@ -196,7 +196,8 @@ export default definePlugin({
flux: {
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;
if (!selfId || isPluginDev(selfId) || isEquicordPluginDev(selfId)) return;
@@ -281,11 +282,12 @@ export default definePlugin({
renderMessageAccessory(props) {
const buttons = [] as JSX.Element[];
+ const equicordSupport = GuildMemberStore.getMember(GUILD_ID, props.message.author.id)?.roles?.includes(EQUCORD_HELPERS);
+
const shouldAddUpdateButton =
!IS_UPDATER_DISABLED
&& (
- (props.channel.id === VC_KNOWN_ISSUES_CHANNEL_ID) ||
- (props.channel.id === VC_SUPPORT_CHANNEL_ID && props.message.author.id === VENBOT_USER_ID)
+ (props.channel.id === SUPPORT_CHANNEL_ID && equicordSupport)
)
&& 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")) {
buttons.push(