Skip to content

Commit 4d1193c

Browse files
committed
Merge branch 'dev'
2 parents ec6b6ea + 2221bfa commit 4d1193c

79 files changed

Lines changed: 1498 additions & 2582 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"@ffmpeg/ffmpeg": "^0.12.10",
4343
"@ffmpeg/util": "^0.12.1",
4444
"@intrnl/xxhash64": "^0.1.2",
45-
"@song-spotlight/api": "^2.0.1",
45+
"@song-spotlight/api": "^2.1.3",
4646
"@streamparser/json": "^0.0.22",
4747
"@types/less": "^3.0.8",
4848
"@types/stylus": "^0.48.42",

packages/vencord-types/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@equicord/types",
33
"private": false,
4-
"version": "1.14.1",
4+
"version": "1.14.2",
55
"description": "",
66
"types": "index.d.ts",
77
"scripts": {
@@ -26,7 +26,7 @@
2626
"type-fest": "^4.35.0"
2727
},
2828
"peerDependencies": {
29-
"@types/react": "18.3.1",
30-
"@types/react-dom": "18.3.1"
29+
"@types/react": "^19.0.10",
30+
"@types/react-dom": "19.0.4"
3131
}
3232
}

pnpm-lock.yaml

Lines changed: 12 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/PluginManager.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export { Plugins as plugins };
4242
import { traceFunction } from "../debug/Tracer";
4343
import { addAudioProcessor, removeAudioProcessor } from "./AudioPlayer";
4444
import { addChannelToolbarButton, addHeaderBarButton, removeChannelToolbarButton, removeHeaderBarButton } from "./HeaderBar";
45+
import { addProfileCollection, removeProfileCollection } from "./ProfileCollections";
4546
import { addUserAreaButton, removeUserAreaButton } from "./UserArea";
4647

4748
const logger = new Logger("PluginManager", "#a6d189");
@@ -196,7 +197,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
196197
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
197198
renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton,
198199
// Custom
199-
renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton
200+
renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton, renderProfileCollection
200201
} = p;
201202

202203
if (p.start) {
@@ -267,6 +268,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p:
267268
}
268269
if (audioProcessor) addAudioProcessor(name, audioProcessor);
269270
if (userAreaButton) addUserAreaButton(name, userAreaButton.render, userAreaButton.priority);
271+
if (renderProfileCollection) addProfileCollection(name, renderProfileCollection);
270272

271273
return true;
272274
}, p => `startPlugin ${p.name}`);
@@ -277,7 +279,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
277279
onBeforeMessageEdit, onBeforeMessageSend, onMessageClick,
278280
renderChatBarButton, chatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton, messagePopoverButton,
279281
// Custom
280-
renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton
282+
renderNicknameIcon, headerBarButton, audioProcessor, userAreaButton, renderProfileCollection
281283
} = p;
282284

283285
if (p.stop) {
@@ -342,6 +344,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu
342344
}
343345
if (audioProcessor) removeAudioProcessor(name);
344346
if (userAreaButton) removeUserAreaButton(name);
347+
if (renderProfileCollection) removeProfileCollection(name);
345348

346349
return true;
347350
}, p => `stopPlugin ${p.name}`);
@@ -354,7 +357,7 @@ export const initPluginManager = onlyOnce(function init() {
354357
"onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick",
355358
"renderChatBarButton", "renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration", "renderMessagePopoverButton",
356359
// Custom
357-
"renderNicknameIcon"
360+
"renderNicknameIcon", "renderProfileCollection"
358361
];
359362

360363
const neededApiPlugins = new Set<string>();
@@ -396,6 +399,7 @@ export const initPluginManager = onlyOnce(function init() {
396399
if (p.headerBarButton) neededApiPlugins.add("HeaderBarAPI");
397400
if (p.audioProcessor) neededApiPlugins.add("AudioPlayerAPI");
398401
if (p.userAreaButton) neededApiPlugins.add("UserAreaAPI");
402+
if (p.renderProfileCollection) neededApiPlugins.add("ProfileCollectionsAPI");
399403

400404
for (const key of pluginKeysToBind) {
401405
p[key] &&= p[key].bind(p) as any;

src/api/ProfileCollections.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Vencord, a Discord client mod
3+
* Copyright (c) 2025 Vendicated and contributors
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
import ErrorBoundary from "@components/ErrorBoundary";
8+
import { Logger } from "@utils/Logger";
9+
import { useEffect, useState } from "@webpack/common";
10+
import type { ReactNode } from "react";
11+
12+
const logger = new Logger("ProfileCollectionAPI");
13+
14+
export type ProfileCollectionFactory = (props: any) => ReactNode | null;
15+
16+
interface CollectionEntry {
17+
render: ProfileCollectionFactory;
18+
}
19+
20+
const profileCollections = new Map<string, CollectionEntry>();
21+
const profileCollectionListeners = new Set<() => void>();
22+
23+
/**
24+
* Adds a collection to the user profile panel.
25+
*
26+
* @param id - Unique identifier for the collection (e.g., "my-plugin-profile-collection")
27+
* @param render - Function that returns the collection JSX. Receives the full profile props object.
28+
* @param priority - Higher values appear first. Default: 0
29+
*
30+
* @example
31+
* addProfileCollection("my-collection", (props) => (
32+
* <div>Custom content for {props.user.id}</div>
33+
* ));
34+
*/
35+
export function addProfileCollection(id: string, render: ProfileCollectionFactory, priority = 0) {
36+
profileCollections.set(id, { render });
37+
profileCollectionListeners.forEach(listener => listener());
38+
}
39+
40+
/**
41+
* Removes a collection from the user profile panel.
42+
*
43+
* @param id - The identifier used when adding the collection
44+
*/
45+
export function removeProfileCollection(id: string) {
46+
profileCollections.delete(id);
47+
profileCollectionListeners.forEach(listener => listener());
48+
}
49+
50+
function ProfileCollections({ props }: { props: any; }) {
51+
const [, forceUpdate] = useState(0);
52+
53+
useEffect(() => {
54+
const listener = () => forceUpdate(n => n + 1);
55+
profileCollectionListeners.add(listener);
56+
return () => { profileCollectionListeners.delete(listener); };
57+
}, []);
58+
59+
return Array.from(profileCollections)
60+
.map(([id, { render: Collection }]) => (
61+
<ErrorBoundary noop key={id} onError={e => logger.error(`Failed to render profile collection: ${id}`, e.error)}>
62+
{Collection({ ...props, user: props.user ?? props.currentUser })}
63+
</ErrorBoundary>
64+
));
65+
}
66+
67+
/** @internal Injected by ProfileCollectionAPI patch (do NOT call directly) */
68+
export function renderProfileCollections(props: any) {
69+
return <ProfileCollections props={props} />;
70+
}

src/api/Settings.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,6 @@ export function migrateSettingToPlugin(newName: string, oldName: string, setting
312312

313313
export function migrateSettingsFromPlugin(newPlugin: string, oldPlugin: string, ...settings: string[]) {
314314
const { plugins } = SettingsStore.plain;
315-
316315
const oldSettings = plugins[oldPlugin];
317316
const newSettings = plugins[newPlugin];
318317
if (!oldSettings || !newSettings) return;
@@ -330,6 +329,21 @@ export function migrateSettingsFromPlugin(newPlugin: string, oldPlugin: string,
330329
SettingsStore.markAsChanged();
331330
}
332331

332+
export function migrateOldSettingToNewPlugin(newPlugin: string, newSetting: string, oldPlugin: string, oldSetting: string,) {
333+
const { plugins } = SettingsStore.plain;
334+
const oldSettings = plugins[oldPlugin];
335+
const newSettings = plugins[newPlugin];
336+
if (!oldSettings || !newSettings) return;
337+
338+
if (!Object.hasOwn(oldSettings, oldSetting) || Object.hasOwn(newSettings, newSetting)) return;
339+
340+
logger.info(`Migrating plugin setting "${oldSetting}" from ${oldPlugin} to "${newSetting}" on ${newPlugin}`);
341+
342+
newSettings[newSetting] = oldSettings[oldSetting];
343+
delete oldSettings[oldSetting];
344+
SettingsStore.markAsChanged();
345+
}
346+
333347
export function definePluginSettings<
334348
Def extends SettingsDefinition,
335349
Checks extends SettingsChecks<Def>,

src/api/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import * as $Notices from "./Notices";
3434
import * as $Notifications from "./Notifications";
3535
import * as $UserArea from "./UserArea";
3636
export * as PluginManager from "./PluginManager";
37+
import * as $ProfileCollections from "./ProfileCollections";
3738
import * as $ServerList from "./ServerList";
3839
import * as $Settings from "./Settings";
3940
import * as $Styles from "./Styles";
@@ -170,3 +171,8 @@ export const UserArea = $UserArea;
170171
* Just used to identify if user is on Equicord as Vencord doesn't have this.
171172
*/
172173
export const isEquicord = true;
174+
175+
/**
176+
* An API allowing you to add other collections where discords game collection is.
177+
*/
178+
export const ProfileCollections = $ProfileCollections;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Vencord, a Discord client mod
3+
* Copyright (c) 2025 Vendicated and contributors
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
import { Devs } from "@utils/constants";
8+
import definePlugin from "@utils/types";
9+
10+
export default definePlugin({
11+
name: "ProfileCollectionsAPI",
12+
description: "API to add collections to the user profile panel like discords game collection.",
13+
authors: [Devs.thororen],
14+
patches: [
15+
{
16+
find: /\.POPOUT,onClose:\i}\),nicknameIcons:.+?\.isProvisional/,
17+
replacement: {
18+
match: /user:\i,widgets:.{0,100}?\}\),/,
19+
replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),",
20+
}
21+
},
22+
{
23+
find: '"UserProfileAccountPopout"',
24+
replacement: {
25+
match: /user:\i,widgets:.{0,100}}\),/,
26+
replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections(arguments[0]),",
27+
},
28+
},
29+
{
30+
find: ".SIDEBAR,disableToolbar:",
31+
replacement: {
32+
match: /user:(\i),widgets:.{0,100}?\}\),/,
33+
replace: "$&Vencord.Api.ProfileCollections.renderProfileCollections({...arguments[0],isSideBar:true}),"
34+
}
35+
}
36+
]
37+
});

src/equicordplugins/characterCounter/index.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import "./style.css";
88

99
import { definePluginSettings } from "@api/Settings";
10+
import ErrorBoundary from "@components/ErrorBoundary";
1011
import { Devs, EquicordDevs } from "@utils/constants";
1112
import { classNameFactory } from "@utils/css";
1213
import definePlugin, { OptionType } from "@utils/types";
@@ -33,7 +34,7 @@ export default definePlugin({
3334
replacement: [
3435
{
3536
match: /(textValue:.{0,50}channelId:\i\.id\}\),)\i/,
36-
replace: "$1$self.getCharCounter(arguments[0].textValue)"
37+
replace: "$1$self.renderCharCounter(arguments[0].textValue)"
3738
}
3839
]
3940
},
@@ -45,8 +46,8 @@ export default definePlugin({
4546
}
4647
}
4748
],
48-
getCharCounter(text: string) {
49-
const premiumType = (UserStore.getCurrentUser().premiumType ?? 0);
49+
renderCharCounter: ErrorBoundary.wrap(text => {
50+
const premiumType = (UserStore.getCurrentUser()?.premiumType ?? 0);
5051
const charMax = premiumType === 2 ? 4000 : 2000;
5152
const { length } = text;
5253

@@ -59,11 +60,12 @@ export default definePlugin({
5960
else color = "var(--red-360)";
6061
}
6162

63+
if (!length) return null;
6264
return (
6365
<div className={cl("counter")} style={{ color }}>
6466
<span className={cl("count")} >{length}</span>/
6567
<span className={cl("max")} >{charMax}</span>
6668
</div>
6769
);
68-
}
70+
}, { noop: true })
6971
});

0 commit comments

Comments
 (0)