Compare commits

..

67 commits

Author SHA1 Message Date
OTKUSteyler
d7255b2d39
Merge branch 'kmmiio99o:main' into main 2026-05-07 15:55:10 +02:00
OTKUSteyler
4894043612
Update README.md 2026-05-07 13:54:00 +02:00
OTKUSteyler
82cb9823bf
Merge branch 'kmmiio99o:main' into main 2026-04-27 18:23:13 +02:00
OTKUSteyler
6709c4929c Update loader.ts 2026-04-27 18:20:38 +02:00
OTKUSteyler
4d6a23f1f8
Update Loader.ts 2026-04-27 18:20:11 +02:00
OTKUSteyler
e599664b34
Update index.ts 2026-04-07 18:02:29 +02:00
OTKUSteyler
2f28882fa2
Update index.ts 2026-04-07 09:59:27 +02:00
OTKUSteyler
b0fbeb40c3
Update index.ts 2026-04-07 00:35:40 +02:00
OTKUSteyler
0ad2557275
Update index.ts 2026-04-07 00:29:44 +02:00
OTKUSteyler
b77421f3ce
Update index.ts 2026-04-07 00:15:49 +02:00
OTKUSteyler
0ad0b79091
Update index.ts 2026-04-07 00:15:16 +02:00
OTKUSteyler
51daf66491
Add files via upload 2026-04-07 00:14:46 +02:00
OTKUSteyler
9d65780297
Update index.ts 2026-04-07 00:14:19 +02:00
OTKUSteyler
08a0f04922
Delete src/core/plugins/max verstappen directory 2026-04-07 00:14:03 +02:00
OTKUSteyler
fb729d429e
Update index.ts 2026-04-07 00:00:43 +02:00
OTKUSteyler
30f9238e9d
Rename index.ts to index.ts 2026-04-06 23:59:28 +02:00
OTKUSteyler
20ac74cfe7
Update index.ts 2026-04-06 23:56:45 +02:00
OTKUSteyler
982fba36f6
Add files via upload 2026-04-06 23:55:49 +02:00
OTKUSteyler
f3864b7b6b
Delete src/core/plugins/max verstappen/aa 2026-04-06 23:55:26 +02:00
OTKUSteyler
c8282b30d8
Create index.ts 2026-04-06 23:54:30 +02:00
OTKUSteyler
6650f6d1db
Create aa 2026-04-06 23:53:57 +02:00
OTKUSteyler
d685025ce9
Delete src/core/plugins/max verstappen 2026-04-06 23:52:48 +02:00
OTKUSteyler
eeb9ec6ff1
Create max verstappen 2026-04-06 23:51:55 +02:00
OTKUSteyler
1332fa1fc2
Delete src/core/plugins/Max Verstappen directory 2026-04-06 23:51:15 +02:00
OTKUSteyler
76d411cbe0
Update index.ts 2026-04-06 23:49:58 +02:00
OTKUSteyler
ff53c6176c
Update index.ts 2026-04-06 23:48:24 +02:00
OTKUSteyler
739e6744c7
Add files via upload 2026-04-06 23:47:03 +02:00
OTKUSteyler
2ca955c46a
Delete src/core/plugins/Max Verstappen/tutuututu 2026-04-06 23:46:45 +02:00
OTKUSteyler
f4b15f3945
Create index.ts 2026-04-06 23:46:34 +02:00
OTKUSteyler
cc7e9712e8
Create tutuututu 2026-04-06 23:43:45 +02:00
OTKUSteyler
f4adb93c98
Rename index.js to index.ts 2026-04-02 19:43:14 +02:00
OTKUSteyler
84cbee35e7
Update index.ts 2026-04-02 19:40:49 +02:00
OTKUSteyler
fbd5bbe72c
Update index.ts 2026-04-02 19:39:49 +02:00
OTKUSteyler
2e9bfdfe0f
Update index.js 2026-04-02 19:35:57 +02:00
OTKUSteyler
4dbaddf579
Update index.js 2026-04-02 19:30:06 +02:00
OTKUSteyler
4e8a302268
Delete src/core/plugins/nodelete/a 2026-04-02 19:29:51 +02:00
OTKUSteyler
021e0c15fc
Add files via upload 2026-04-02 19:29:07 +02:00
OTKUSteyler
d45e9ef61f
Create a 2026-04-02 19:28:45 +02:00
OTKUSteyler
eb6dc9ad58
Update index.ts 2026-04-02 19:03:39 +02:00
OTKUSteyler
5fcc13cae5
Delete src/core/plugins/favouriteanything/a 2026-04-02 19:01:54 +02:00
OTKUSteyler
3c86ab104e
Update index.ts 2026-04-02 19:00:13 +02:00
OTKUSteyler
c14c714745
Add files via upload 2026-04-02 18:55:02 +02:00
OTKUSteyler
d48a8634d6
Create a 2026-04-02 18:54:26 +02:00
OTKUSteyler
fcd396602d
Delete src/core/plugins/favouriteanything 2026-04-02 18:53:59 +02:00
OTKUSteyler
2227e0d328
Create 2026-04-02 18:52:46 +02:00
OTKUSteyler
3aafa28a3b
Update index.ts 2026-04-02 17:26:36 +02:00
OTKUSteyler
4854b998a2
Delete src/core/plugins/NoDelete directory 2026-04-02 17:26:06 +02:00
OTKUSteyler
a731776639
Update index.js 2026-04-02 17:22:53 +02:00
OTKUSteyler
6c475e69be
Update index.ts 2026-04-02 17:16:22 +02:00
OTKUSteyler
3a3d61971a
Update index.ts 2026-04-02 17:12:51 +02:00
OTKUSteyler
b604a02fbc
Delete src/core/plugins/NoDelete/a 2026-04-02 17:09:00 +02:00
OTKUSteyler
ba3499834e
Add files via upload 2026-04-02 17:05:54 +02:00
OTKUSteyler
b63941bc3c
Create a 2026-04-02 17:05:34 +02:00
OTKUSteyler
9fd5d46f19
Update debug.ts 2026-03-23 14:46:35 +01:00
OTKUSteyler
597a871bd5
Update debug.ts 2026-03-23 14:43:23 +01:00
OTKUSteyler
49447a7be7
Update reload.ts 2026-03-23 14:42:03 +01:00
OTKUSteyler
a7bff4c900
Update index.ts 2026-03-23 06:43:38 +01:00
OTKUSteyler
c7c8b800ee
Update index.ts 2026-03-21 21:43:51 +01:00
OTKUSteyler
0885fe0c7a
Update index.tsx 2026-03-21 21:34:36 +01:00
OTKUSteyler
8e89a70e6a
Update text in About component 2026-03-21 21:32:30 +01:00
OTKUSteyler
f477466fdf
Add files via upload 2026-03-21 21:30:57 +01:00
OTKUSteyler
4cb42a13e0
Update build.mjs 2026-03-21 21:20:52 +01:00
OTKUSteyler
d5143127d5
Update index.tsx 2026-03-21 21:18:37 +01:00
OTKUSteyler
f8a1154083
Update release.yml 2026-03-21 21:14:52 +01:00
OTKUSteyler
4df48b31c1
Update default.json 2026-03-21 21:14:16 +01:00
OTKUSteyler
42238a9421
Update README for GoonCord installation instructions 2026-03-21 21:09:29 +01:00
OTKUSteyler
43c667838f
Update README.md 2026-03-21 21:04:31 +01:00
25 changed files with 1849 additions and 766 deletions

View file

@ -32,10 +32,8 @@ Bundle based on ShiggyCord, made just for fun
bun run build
```
1. In the newly created `dist` directory, run a HTTP server. I recommend [http-server](https://www.npmjs.com/package/http-server).
1. Go to Settings > Developer enabled earlier. Enable `Load from custom url` and input the IP address and port of the server (e.g. `http://192.168.1.236:4040/gooncord.js`) in the new input box labelled `GoonCord URL`.
1. Go to Settings > Developer enabled earlier. Enable `Load from custom url` and input the IP address and port of the server (e.g. `http://192.168.1.236:4040/shiggycord.js`) in the new input box labelled `ShiggyCord URL`.
1. Restart Discord. Upon reload, you should notice that your device will download GoonCord's bundled code from your server, rather than GitHub.
1. Make your changes, rebuild, reload, go wild!
Alternatively, you can directly *serve* the bundled code by running `bun run serve`. `gooncord.js` will be served on your local address under the port 4040. You will then insert `http://<local ip address>:4040/gooncord.js` as a custom url and reload. Whenever you restart your mobile client, the script will rebuild the bundle as your client fetches it.
![OTKUSteyler](https://count.getloli.com/@OTKUSteyler?name=OTKUSteyler&theme=miku&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=1)

1967
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,9 @@
{
"name": "GoonCord",
"name": "ShiggyCord",
"private": true,
"author": {
"name": "OTKUSteyler",
"url": "https://github.com/OTKUSteyler"
"name": "kmmiio99o",
"url": "https://github.com/kmmiio99o"
},
"scripts": {
"build": "node scripts/build.mjs",
@ -13,40 +13,45 @@
},
"license": "BSD-3-Clause",
"devDependencies": {
"@swc/core": "^1.15.47",
"@swc/helpers": "^0.5.23",
"@types/node": "^26.1.2",
"@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^7.1.0",
"@semantic-release/git": "^10.0.1",
"semantic-release": "^25.0.3",
"@swc/core": "^1.15.24",
"@swc/helpers": "^0.5.21",
"@types/node": "^25.5.2",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.65.0",
"@typescript-eslint/typescript-estree": "^8.65.0",
"chalk": "^6.0.0",
"esbuild": "^0.28.1",
"@typescript-eslint/eslint-plugin": "^8.58.0",
"@typescript-eslint/parser": "^8.58.0",
"@typescript-eslint/typescript-estree": "^8.58.0",
"chalk": "^5.6.2",
"esbuild": "^0.28.0",
"esbuild-plugin-globals": "^0.2.0",
"eslint": "^10.8.0",
"eslint": "^10.2.0",
"eslint-plugin-import-alias": "^1.2.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-import-sort": "^14.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.4.1",
"typescript": "^7.0.2",
"typescript": "^6.0.2",
"yargs-parser": "^22.0.0"
},
"dependencies": {
"@gullerya/object-observer": "^7.0.0",
"@tanstack/react-query": "^5.101.4",
"@gullerya/object-observer": "^6.1.4",
"@tanstack/react-query": "^5.96.2",
"@types/chroma-js": "~3.1.2",
"@types/lodash": "~4.17.24",
"@types/react": "19.2.17",
"es-toolkit": "^1.50.0",
"@types/react": "19.2.14",
"es-toolkit": "^1.45.1",
"fuzzysort": "^3.1.0",
"intl-messageformat": "^11.2.12",
"intl-messageformat": "^11.2.0",
"moment": "2.30.1",
"react-devtools": "^7.0.1",
"react-native": "0.86.2",
"react-native-reanimated": "^4.5.3",
"react-native-webview": "^14.0.1",
"react-native": "0.84.1",
"react-native-reanimated": "^4.3.0",
"react-native-webview": "^13.16.1",
"spitroast": "^2.1.6",
"type-fest": "^5.8.0"
"type-fest": "^5.5.0"
},
"trustedDependencies": [
"@swc/core",

View file

@ -56,8 +56,6 @@ const config = {
"!ShiggyCord-deps-shim!": "./shims/depsModule.ts",
spitroast: "./node_modules/spitroast",
"react/jsx-runtime": "./shims/jsxRuntime",
"@gullerya/object-observer":
path.resolve("./node_modules/@gullerya/object-observer/dist/object-observer.min.js"),
},
plugins: [
globalPlugin({
@ -77,7 +75,7 @@ const config = {
constModules: {
globals: {
"bunny-build-info": {
version: `"1.4.1.8"`,
version: `"1.4.1.4"`,
},
},
},

View file

@ -18,20 +18,16 @@ export default () =>
},
],
execute([ephemeral], ctx) {
const info = getDebugInfo();
if(info.device.codename === undefined) { info.device.codename = ""}
else{ info.device.codename = "(" + info.device.codename + ")" }
const content = [
"**💦GoonCord💦 Debug Info**",
`> GoonCord: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
`> Discord: ${info.discord.version} (${info.discord.build})`,
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
`> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
`> Device: ${info.device.model} ${info.device.codename}`,
].join("\n");
const info = getDebugInfo();
const content = [
"**💦GoonCord💦 Debug Info**",
`> 💦GoonCord💦: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
`> Discord: ${info.discord.version} (${info.discord.build})`,
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
`> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
`> Device: ${info.device.model}`,
].join("\n");
if (ephemeral?.value) {
messageUtil.sendBotMessage(ctx.channel.id, content);

View file

@ -11,7 +11,7 @@
"IMAGE_FILES": "Image Files",
"TEXT_FILES": "Text Files",
"BRAND": "Brand",
"SHIGGYCORD": "GoonCord",
"SHIGGYCORD": "💦GoonCord💦",
"SHIGGYCORD_URL": "GoonCord URL",
"BROWSER": "Browser",
"BYTECODE": "Bytecode",
@ -94,7 +94,7 @@
"PLUGINS": "Plugins",
"REFETCH": "Refetch",
"RELOAD": "Reload",
"RELOAD_DISCORD": "ReGoon your Discord",
"RELOAD_DISCORD": "💦ReGoon💦 your Discord",
"RELOAD_IN_NORMAL_MODE": "Reload in Normal Mode",
"RELOAD_IN_NORMAL_MODE_DESC": "Safe mode currently enabled, tap to reload in normal mode",
"RELOAD_IN_SAFE_MODE": "Reload in Safe Mode",

View file

@ -156,16 +156,6 @@ export function formatString<T = void>(
val: Record<string, T>,
): FormatStringRet<T> {
const str = Strings[key];
if (!str) return str as FormatStringRet<T>;
try {
// @ts-ignore
return new IntlMessageFormat(str).format(val);
} catch (e: any) {
if (e?.name === "SyntaxError") {
logger.warn(`[i18n] Failed to parse message for key "${String(key)}":`, e.message);
return str as FormatStringRet<T>;
}
logger.warn(`[i18n] Failed to format message for key "${String(key)}":`, e?.message ?? e);
return str as FormatStringRet<T>;
}
// @ts-ignore
return new IntlMessageFormat(str).format(val);
}

View file

@ -1,6 +1,7 @@
import { after } from "@lib/api/patcher";
import { onJsxCreate } from "@lib/api/react/jsx";
import { findByNameLazy } from "@metro";
import { findByName, findByNameLazy } from "@metro";
import { useEffect, useState } from "react";
import { defineCorePlugin } from "..";
import { FluxDispatcher } from "@metro/common";
@ -9,9 +10,14 @@ interface Badge {
url: string;
}
interface CustomBadge {
label: string;
url: string;
}
interface UserBadgeData {
roles?: string[];
custom?: Badge[];
custom?: CustomBadge[];
}
interface BadgeData {
@ -27,11 +33,6 @@ interface RolesData {
[roleName: string]: RoleData;
}
interface EquicordBadge {
tooltip: string;
badge: string;
}
const useBadgesModule = findByNameLazy("useBadges", false);
const badgesCache = new Map<string, Badge[]>();
@ -39,20 +40,20 @@ const badgeProps = new Map<string, Record<string, any>>();
const pendingRequests = new Set<string>();
export default defineCorePlugin({
manifest: {
id: "bunny.badges",
version: "1.2.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "Badges",
description: "Adds badges to user's profile",
authors: [{ name: "cocobo1" }, { name: "pylixonly" }],
},
manifest: {
id: "bunny.badges",
version: "1.1.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "Badges",
description: "Adds badges to user's profile",
authors: [{ name: "cocobo1" }, { name: "pylixonly" }],
},
},
start() {
start() {
onJsxCreate("ProfileBadge", (component, ret) => {
if (ret.props.id?.startsWith("rain-")) {
const cachedProps = badgeProps.get(ret.props.id);
@ -73,67 +74,27 @@ export default defineCorePlugin({
}
});
// Small helper so one failing/optional source can't take down the others.
const safeFetchJson = async <T,>(url: string, fallback: T): Promise<T> => {
try {
const res = await fetch(url);
if (!res.ok) return fallback;
return (await res.json()) as T;
} catch {
return fallback;
}
};
const fetchAndProcessBadges = async (userId: string) => {
if (pendingRequests.has(userId)) return;
pendingRequests.add(userId);
try {
const [
goonBadgesData,
goonRolesData,
rainBadgesData,
rainRolesData,
equicordData,
] = await Promise.all([
safeFetchJson<BadgeData>(
"https://codeberg.org/chocomint-chan/GoonCord_Badges/raw/branch/main/badges.json",
{}
),
safeFetchJson<RolesData>(
"https://codeberg.org/chocomint-chan/GoonCord_Badges/raw/branch/main/assets/roles/roles.json",
{}
),
safeFetchJson<BadgeData>(
"https://codeberg.org/raincord/badges/raw/branch/main/badges.json",
{}
),
safeFetchJson<RolesData>(
"https://codeberg.org/raincord/badges/raw/branch/main/assets/roles/roles.json",
{}
),
safeFetchJson<Record<string, EquicordBadge[]>>(
"https://badge.equicord.org/badges.json",
{}
),
const [badgesRes, rolesRes] = await Promise.all([
fetch("https://codeberg.org/chocomint-chan/GoonCord_Badges/raw/branch/main/badges.json"),
fetch("https://codeberg.org/chocomint-chan/GoonCord_Badges/raw/branch/main/assets/roles/roles.json"),
]);
const badgesData: BadgeData = await badgesRes.json();
const rolesData: RolesData = await rolesRes.json();
const userBadgeData = badgesData[userId] || { roles: [], custom: [] };
const allBadges: Badge[] = [];
// GoonCord and raincord badge/role sets are keyed independently,
// so each source is resolved against its own roles file.
const badgeSources: Array<{ badges: BadgeData; roles: RolesData }> = [
{ badges: goonBadgesData, roles: goonRolesData },
{ badges: rainBadgesData, roles: rainRolesData },
];
badgeSources.forEach(({ badges, roles }) => {
const userBadgeData = badges[userId];
if (!userBadgeData) return;
// process role badges
userBadgeData.roles?.forEach(roleName => {
const roleData = roles[roleName];
// process role badges
if (userBadgeData.roles) {
userBadgeData.roles.forEach(roleName => {
const roleData = rolesData[roleName];
if (roleData) {
allBadges.push({
label: roleData.label,
@ -141,18 +102,12 @@ export default defineCorePlugin({
});
}
});
}
// process custom badges
if (userBadgeData.custom) {
allBadges.push(...userBadgeData.custom);
}
});
// process equicord badges
const equicordUserBadges = equicordData[userId] ?? [];
equicordUserBadges.forEach(b => {
allBadges.push({ label: b.tooltip, url: b.badge });
});
// process custom badges
if (userBadgeData.custom) {
allBadges.push(...userBadgeData.custom);
}
badgesCache.set(userId, allBadges);
@ -167,14 +122,12 @@ export default defineCorePlugin({
});
FluxDispatcher.dispatch({ type: "USER_UPDATE", user: { id: userId } });
} catch (err) {
console.error("[bunny.badges] Failed to fetch/process badges:", err);
} finally {
pendingRequests.delete(userId);
}
};
return after("default", useBadgesModule, ([user], result) => {
after("default", useBadgesModule, ([user], result) => {
if (!user) return;
const userId = user.userId;

View file

@ -42,19 +42,19 @@ function transformLinks(content: string, config: FixEmbedSettings): string {
if (config.instagram) {
result = result.replace(
/https?:\/\/(?:www\.)?instagram\.com(\/[^\s]+)/gi,
"https://kkinstagram.com$1",
"https://uuinstagram.com$1",
);
}
// TikTok: Replace domain, keep path
if (config.tiktok) {
result = result.replace(
/https?:\/\/(?:www\.)?tiktok\.com(\/[^\s]+)/gi,
"https://tiktokez.com$1",
"https://tnktok.com$1",
);
// TikTok short links (vm.tiktok.com)
result = result.replace(
/https?:\/\/vm\.tiktok\.com\/([A-Za-z0-9]+)\/?/gi,
"https://vm.tiktokez.com/$1",
"https://vm.tnktok.com/$1",
);
}
// Reddit: Replace domain, keep path

View file

@ -39,7 +39,6 @@ export const getCorePlugins = (): CorePluginRecord => ({
"bunny.nodelete": makeLazyPlugin(() => require("./nodelete")),
"bunny.maxverstappen": makeLazyPlugin(() => require("./maxverstappen")),
});
/**

View file

@ -23,54 +23,51 @@ const { showSimpleActionSheet } = lazyDestructure(() =>
// TODO: These styles work weirdly. Low DPI Has cramped text. Fix?
const useStyles = createStyles({
card: {
backgroundColor: semanticColors?.CARD_SECONDARY_BG,
borderRadius: 12,
overflow: "hidden"
},
header: {
padding: 0,
},
headerLeading: {
flex: 1,
flexDirection: "column",
justifyContent: "center",
scale: 1.2
},
headerTrailing: {
display: "flex",
flexDirection: "row",
gap: 15,
alignItems: "center"
},
headerLabel: {
...TextStyleSheet["heading-lg/semibold"],
},
headerLabelCompact: {
...TextStyleSheet["text-md/semibold"],
},
headerSubtitle: {
...TextStyleSheet["text-sm/semibold"],
color: semanticColors.TEXT_MUTED,
},
descriptionLabel: {
...TextStyleSheet["text-md/semibold"],
color: "text-strong",
},
actions: {
flexDirection: "row-reverse",
alignItems: "center",
gap: 5
},
iconStyle: {
tintColor: semanticColors.LOGO_PRIMARY,
opacity: 0.2,
height: 64,
width: 64,
left: void 0,
right: "30%",
top: "-10%"
}
card: {
backgroundColor: semanticColors?.CARD_SECONDARY_BG,
borderRadius: 12,
overflow: "hidden",
},
header: {
padding: 0,
},
headerLeading: {
flexDirection: "column",
justifyContent: "center",
scale: 1.2,
},
headerTrailing: {
display: "flex",
flexDirection: "row",
gap: 15,
alignItems: "center",
},
headerLabel: {
...TextStyleSheet["heading-md/semibold"],
color: semanticColors.MOBILE_TEXT_HEADING_PRIMARY,
},
headerSubtitle: {
...TextStyleSheet["text-md/semibold"],
color: semanticColors.TEXT_MUTED,
},
descriptionLabel: {
...TextStyleSheet["text-md/semibold"],
color: semanticColors.TEXT_NORMAL,
},
actions: {
flexDirection: "row-reverse",
alignItems: "center",
gap: 5,
},
iconStyle: {
tintColor: semanticColors.INTERACTIVE_ICON_DEFAULT,
opacity: 0.2,
height: 64,
width: 64,
left: void 0,
right: "30%",
top: "-10%",
},
});
interface Action {
@ -111,9 +108,9 @@ export default function AddonCard(props: CardProps) {
<Stack spacing={16}>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<View style={styles.headerLeading}>
<Text numberOfLines={1} ellipsizeMode="tail" style={styles.headerLabel}>{props.headerLabel}</Text>
<Text style={styles.headerLabel}>{props.headerLabel}</Text>
{props.headerSublabel && (
<Text numberOfLines={1} ellipsizeMode="tail" style={styles.headerSubtitle}>{props.headerSublabel}</Text>
<Text style={styles.headerSubtitle}>{props.headerSublabel}</Text>
)}
</View>
<View style={[styles.headerTrailing, { marginLeft: "auto" }]}>

View file

@ -19,6 +19,7 @@ import {
TableSwitchRow,
Stack,
} from "@metro/common/components";
import { semanticColors } from "@ui/color";
import { ScrollView, View } from "react-native";
import ErrorComponentStackCard from "./ErrorComponentStackCard";
@ -73,8 +74,8 @@ export default function ErrorBoundaryScreen(props: {
<ScrollView fadingEdgeLength={64} contentContainerStyle={{ gap: 12 }}>
<Text
variant="heading-md/extrabold"
color="text-default"
style={{
color: semanticColors.HEADER_PRIMARY,
marginBottom: 8,
textAlign: "center",
}}

View file

@ -97,7 +97,7 @@ export default function About() {
variant="text-md/medium"
style={{ textAlign: "center", marginTop: 4 }}
>
💦Goon💦 your discord client!
💦Goon your discord client💦!
</Text>
</View>

View file

@ -125,7 +125,7 @@ export default function General() {
>
<TableRowGroup title="App Information">
<TableRow
label="GoonCord"
label="💦GoonCord💦"
icon={<TableRow.Icon source={{ uri: shiggycordIcon ?? "" }} />}
trailing={<TableRow.TrailingText text={debugInfo.bunny.version} />}
/>
@ -144,7 +144,7 @@ export default function General() {
subLabel={`${debugInfo.bunny.loader.name} loader`}
icon={<TableRow.Icon source={findAssetId("DownloadIcon")} />}
trailing={
<TableRow.TrailingText text={debugInfo.bunny.loader.version ?? ""} />
<TableRow.TrailingText text={debugInfo.bunny.loader.version} />
}
/>
</TableRowGroup>

View file

@ -11,7 +11,7 @@ async function initializeShiggyCord() {
Object.freeze = Object.seal = Object;
await require("@metro/internals/caches").initMetroCache();
require(".").default();
await require(".").default();
} catch (e) {
const { ClientInfoManager } = require("@lib/api/native/modules");
const stack = e instanceof Error ? e.stack : undefined;

View file

@ -11,8 +11,13 @@ import { injectFluxInterceptor } from "@lib/api/flux";
import { patchJsx } from "@lib/api/react/jsx";
import { logger } from "@lib/utils/logger";
import { patchSettings } from "@ui/settings";
import { updaterSettings } from "@lib/api/settings";
import { InteractionManager } from "react-native";
import { getDebugInfo, initDebugger } from "@lib/api/debug";
// Debug toggle helper (temporary runtime fallback). The helper is dynamically
// imported when needed (to avoid bundling it permanently) and removed after use.
import * as lib from "./lib";
import { timings } from "@lib/utils/timings";
@ -61,19 +66,52 @@ export default async () => {
const { VdPluginManager } = await import("@core/vendetta/plugins");
const { initPlugins, updatePlugins } = await import("@lib/addons/plugins");
await Promise.all([
VdPluginManager.initPlugins()
.then((u) => u && lib.unload.push(u))
.catch((e) => logger.log("Vendetta init failed:", e)),
initPlugins()
.catch((e) => logger.log("initPlugins failed:", e)),
]);
// Initialize Vendetta plugins (may start many plugins) — do not block UI.
VdPluginManager.initPlugins()
.then((u) => lib.unload.push(u))
.catch((e) => logger.log("Vendetta init failed:", e));
updatePlugins()
.catch((e) => logger.log("updatePlugins failed:", e));
// Start ShiggyCord (Bunny) plugins now without forcing repository updates.
// Plugin repository fetching is deferred so the app can finish launching first.
// Stagger plugin startup to reduce CPU/memory spikes: use smaller batches and a small interval.
// This keeps the UI responsive while plugins initialize in the background.
initPlugins({ staggerInterval: 500, batchSize: 2 });
// Attempt a lightweight recovery toggle if some core plugins failed to start.
try {
// Dynamically import the helper (if present) but suppress verbose errors.
const mod = await import("@core/debug/toggleCorePlugins").catch(
() => null,
);
if (mod?.default) {
// Run helper with minimal noise; ignore failures.
mod.default({ offDuration: 1500 }).catch(() => {});
}
// Try to remove the helper source file (best-effort, ignore failures).
await import("@lib/api/native/fs")
.then((fs) => fs.removeFile("src/core/debug/toggleCorePlugins.ts"))
.catch(() => {});
} catch {
// suppressed
}
// Update fonts in background
updateFonts().catch((e) => logger.log("updateFonts failed:", e));
// Schedule plugin repository update after a delay (5 minutes) so update work
// does not impact initial launch performance.
setTimeout(
() => {
updatePlugins().catch((e) =>
logger.log("updatePlugins failed (deferred 5min):", e),
);
},
5 * 60 * 1000,
);
// Note: we intentionally moved the call to `updatePlugins()` above so core
// plugins are registered prior to calling `initPlugins()`.
};
// Preferred: wait until interactions finish (animations / navigation).
@ -88,5 +126,5 @@ export default async () => {
}
// Final ready log for basic UI availability.
logger.log("ShiggyCord is ready.");
logger.log("GoonCord is ready.");
};

View file

@ -752,54 +752,27 @@ export async function updatePlugins() {
}),
);
}
const enabledIds = [...registeredPlugins.keys()].filter(
(id) => isPluginEnabled(id) && !pluginInstances.has(id),
);
await Promise.allSettled(
enabledIds.map(async (id) => {
try {
await startPlugin(id).catch((e) => {
console.error(`Failed to start plugin ${id}:`, e);
});
} catch (error) {
console.error(`Unexpected error while starting plugin ${id}:`, error);
}
}),
);
}
/**
* Register all installed plugins from local storage manifests without any
* network fetch. This lets initPlugins() start plugins immediately using
* previously-cached scripts, keeping startup snappy.
* Initialize plugins but stagger startup to avoid CPU spikes during app start.
*
* Safe to call multiple times — skips already-registered plugins.
* This replaces the original "start all enabled plugins in parallel" approach
* with a configurable staggered startup: plugins are started in small batches and
* we await a small interval between batches. This reduces short-lived CPU and I/O contention
* that causes jank on low-end devices.
*
* Backwards-compatible: callers can still call initPlugins() with no arguments.
*
* Options:
* - staggerInterval (ms) - wait time between batches (default: 100)
* - batchSize - how many plugins to start concurrently in a single batch (default: 3)
*/
export async function registerInstalledPlugins() {
await awaitStorage(pluginRepositories, pluginSettings);
export async function initPlugins(
options: { staggerInterval?: number; batchSize?: number } = {},
) {
const { staggerInterval = 100, batchSize = 3 } = options;
const ids = Object.keys(pluginSettings ?? {});
if (ids.length === 0) return;
// Load all manifest files from local storage in parallel.
const results = await Promise.allSettled(
ids.map(async (id) => {
if (registeredPlugins.has(id) || isCorePlugin(id)) return;
const loaded = await preloadStorageIfExists(`plugins/manifests/${id}.json`);
if (loaded) {
const manifest = getPreloadedStorage<t.BunnyPluginManifest>(
`plugins/manifests/${id}.json`,
);
if (manifest) {
registeredPlugins.set(id, manifest);
}
}
}),
);
}
export async function initPlugins() {
// Ensure core plugins are registered prior to any plugin startup logic so
// settings pages and UI that read registered/core plugins show them.
// registerCorePlugins is synchronous and idempotent, but call it here as a
@ -811,7 +784,6 @@ export async function initPlugins() {
}
await awaitStorage(pluginRepositories, pluginSettings);
await registerInstalledPlugins();
// Collect enabled plugin ids
const enabledIds = [...registeredPlugins.keys()].filter((id) =>
@ -820,18 +792,32 @@ export async function initPlugins() {
if (enabledIds.length === 0) return;
// Start all enabled plugins in parallel
await Promise.allSettled(
enabledIds.map(async (id) => {
try {
// Only attempt to start if still enabled and not already started
if (!isPluginEnabled(id) || pluginInstances.has(id)) return;
await startPlugin(id).catch((e) => {
console.error(`Failed to start plugin ${id}:`, e);
});
} catch (error) {
console.error(`Unexpected error while starting plugin ${id}:`, error);
}
}),
);
// Helper sleep
const sleep = (ms: number) => new Promise<void>((res) => setTimeout(res, ms));
// Start plugins in batches to avoid starting all at once.
for (let i = 0; i < enabledIds.length; i += batchSize) {
const batch = enabledIds.slice(i, i + batchSize);
// Start the batch in parallel but don't let exceptions bubble to halt everything.
await Promise.allSettled(
batch.map(async (id) => {
try {
// Only attempt to start if still enabled and not already started
if (!isPluginEnabled(id) || pluginInstances.has(id)) return;
await startPlugin(id).catch((e) => {
// startPlugin may throw; ensure it doesn't crash the whole batch
console.error(`Failed to start plugin ${id}:`, e);
});
} catch (error) {
console.error(`Unexpected error while starting plugin ${id}:`, error);
}
}),
);
// If there are more plugins to start, wait before starting the next batch.
if (i + batchSize < enabledIds.length) {
await sleep(staggerInterval);
}
}
}

View file

@ -40,55 +40,19 @@ export function updateBunnyColor(colorManifest: ColorManifest | null, { update =
});
if (internalDef != null) {
// Register the synthetic key BEFORE calling native, and verify it actually
// landed in every table native reads from. If any registration step fails,
// bail out to a known-good theme instead of handing native an unknown key.
try {
tokenRef.Theme[ref.key.toUpperCase()] = ref.key;
tokenRef.Theme[ref.key.toUpperCase()] = ref.key;
FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference];
if (!FormDivider.DIVIDER_COLORS[ref.current!.reference]) {
throw new Error(`Missing divider color reference: ${ref.current!.reference}`);
}
FormDivider.DIVIDER_COLORS[ref.key] = FormDivider.DIVIDER_COLORS[ref.current!.reference];
Object.keys(tokenRef.Shadow).forEach(k => {
if (!(ref.current!.reference in tokenRef.Shadow[k])) {
throw new Error(`Missing shadow reference: ${ref.current!.reference}`);
}
tokenRef.Shadow[k][ref.key] = tokenRef.Shadow[k][ref.current!.reference];
});
Object.keys(tokenRef.SemanticColor).forEach(k => {
if (!(ref.current!.reference in tokenRef.SemanticColor[k])) {
throw new Error(`Missing semantic color reference: ${ref.current!.reference}`);
}
tokenRef.SemanticColor[k][ref.key] = {
...tokenRef.SemanticColor[k][ref.current!.reference]
};
});
} catch (e) {
console.error("Failed to register custom theme key, falling back", e);
ref.current = null; // force fallback path below
}
Object.keys(tokenRef.Shadow).forEach(k => tokenRef.Shadow[k][ref.key] = tokenRef.Shadow[k][ref.current!.reference]);
Object.keys(tokenRef.SemanticColor).forEach(k => {
tokenRef.SemanticColor[k][ref.key] = {
...tokenRef.SemanticColor[k][ref.current!.reference]
};
});
}
if (update) {
AppearanceManager.setShouldSyncAppearanceSettings(false);
const targetTheme = ref.current != null ? ref.key : ref.lastSetDiscordTheme;
// This is the actual crash site: a raw call into native code with no
// guard. Wrap it so a native-side rejection can't take the whole
// bridge/thread down — fall back to the last known-good Discord theme.
try {
AppearanceManager.updateTheme(targetTheme);
} catch (e) {
console.error(`Native rejected theme "${targetTheme}", reverting to "${ref.lastSetDiscordTheme}"`, e);
try {
AppearanceManager.updateTheme(ref.lastSetDiscordTheme);
} catch (e2) {
console.error("Fallback theme also failed to apply", e2);
}
}
AppearanceManager.updateTheme(internalDef != null ? ref.key : ref.lastSetDiscordTheme);
}
}

View file

@ -141,7 +141,7 @@ export function getCurrentTheme() {
* @internal
*/
export function getThemeFromLoader(): VdThemeInfo | null {
return getStoredTheme() ?? getCurrentTheme();
return getStoredTheme();
}
/**
@ -157,7 +157,6 @@ export async function initThemes() {
}
await newAwaitStorage(colorsPref);
await awaitStorage(themes);
const currentTheme = getThemeFromLoader();
initColors(currentTheme?.data ?? null);

View file

@ -10,9 +10,6 @@ const rainLoaderIdentity = globalThis.__RAIN_LOADER__;
// @ts-ignore
const vendettaLoaderIdentity = globalThis.__vendetta_loader;
// @ts-ignore
const shiggyLoaderIdentity = globalThis.__SHIGGY_LOADER__;
export interface VendettaLoaderIdentity {
name: string;
features: {
@ -39,10 +36,6 @@ export function isRa1nLoader() {
return rainLoaderIdentity != null;
}
export function isShiggyLoader() {
return shiggyLoaderIdentity != null;
}
function polyfillVendettaLoaderIdentity() {
if (!isPyonLoader() || isVendettaLoader() || !isRa1nLoader()) return null;
@ -105,8 +98,6 @@ export function getLoaderIdentity() {
return getVendettaLoaderIdentity();
} else if (isRa1nLoader()) {
return rainLoaderIdentity();
} else if (isShiggyLoader()) {
return shiggyLoaderIdentity;
}
return null;
@ -125,7 +116,6 @@ export function getLoaderName() {
if (isPyonLoader()) return pyonLoaderIdentity.loaderName;
else if (isRa1nLoader()) return rainLoaderIdentity.loadername;
else if (isVendettaLoader()) return vendettaLoaderIdentity.name;
else if (isShiggyLoader()) return shiggyLoaderIdentity.loaderName;
return "Unknown";
}
@ -133,7 +123,6 @@ export function getLoaderName() {
export function getLoaderVersion(): string | null {
if (isPyonLoader()) return pyonLoaderIdentity.loaderVersion;
else if (isRa1nLoader()) return rainLoaderIdentity.loaderVersion;
else if (isShiggyLoader()) return shiggyLoaderIdentity.loaderVersion;
return null;
}
@ -155,9 +144,7 @@ export function isThemeSupported() {
} else if (isVendettaLoader()) {
return vendettaLoaderIdentity!!.features.themes != null;
} else if (isRa1nLoader()) {
return false; // Rain has theme support disabled, this is here just to make sure it doesnt think it does
} else if (isShiggyLoader()) {
return shiggyLoaderIdentity.hasThemeSupport;
return false; // Ra1n has theme support disabled, this is here just to make sure it doesnt think it does
}
return false;
@ -171,8 +158,6 @@ export function getStoredTheme(): VdThemeInfo | null {
if (!themeProp) return null;
// @ts-ignore
return globalThis[themeProp] || null;
} else if (isShiggyLoader()) {
return shiggyLoaderIdentity.storedTheme || null;
}
return null;
@ -183,8 +168,6 @@ export function getThemeFilePath() {
return "pyoncord/current-theme.json";
} else if (isVendettaLoader()) {
return "vendetta_theme.json";
} else if (isShiggyLoader()) {
return "ShiggyCord/current-theme.json";
}
return null;
@ -258,7 +241,6 @@ export function getLoaderConfigPath() {
export function isFontSupported() {
if (isPyonLoader()) return pyonLoaderIdentity.fontPatch === 2;
else if (isShiggyLoader()) return shiggyLoaderIdentity.fontPatch === 2;
return false;
}
@ -266,4 +248,4 @@ export function isFontSupported() {
export async function clearBundle() {
// TODO: This should be not be hardcoded, maybe put in loader.json?
return void await removeCacheFile("bundle.js");
}
}

View file

@ -1,64 +1,49 @@
import { ImageLoader } from "./";
import type { LoaderPayload } from "./types";
type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>;
export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload;
export function isModuleRegistered(module: string) {
return loaderPayload.loader.modules[module] !== undefined;
}
export function isFunctionRegistered(module: string, function_: string) {
return loaderPayload.loader.modules[module]?.functions[function_] !== undefined;
}
export async function callFunction(module: string, functionName: string, args: unknown[]) {
if (!isFunctionRegistered(module, functionName)) {
throw new Error(`Function ${module}.${functionName} is not registered`);
}
const promise: InteropReturn = ImageLoader.queryCache([
"__pyon_bridge",
JSON.stringify({ m: module, f: functionName, a: args }),
]);
const result = await promise;
if ("ret" in result) {
return JSON.parse(result.ret);
}
if ("cancelled" in result) {
throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`);
}
if ("err" in result) {
throw new Error(result.err);
}
}
export interface LoaderModule {
hasFunction(functionName: string): boolean;
callFunction(functionName: string, args: unknown[]): Promise<unknown>;
}
type ModuleProps<T extends LoaderModule> = {
name: string;
argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>;
};
export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) {
const module = loaderPayload.loader.modules[name];
if (!module) {
throw new Error(`Module ${name} is not registered`);
}
const baseObject: Record<string, any> = {
hasFunction: (functionName: string) => isFunctionRegistered(name, functionName),
callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args),
};
for (const [functionName] of Object.entries(module.functions)) {
baseObject[functionName] = (...args: unknown[]) =>
callFunction(
@ -68,10 +53,8 @@ export function getModule<T extends LoaderModule>({ name, argumentProcessors: pr
processArgs && functionName in processArgs ? processArgs[functionName](args) : args,
);
}
return baseObject as T;
}
/**
* Determines if safe mode is currently enabled. The result remains consistent over time.
* This will not change until the app is restarted. For draft changes, use the InitConfigStore.

View file

@ -1,64 +1,49 @@
import { ImageLoader } from "./";
import type { LoaderPayload } from "./types";
type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>;
export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload;
export function isModuleRegistered(module: string) {
return loaderPayload.loader.modules[module] !== undefined;
}
export function isFunctionRegistered(module: string, function_: string) {
return loaderPayload.loader.modules[module]?.functions[function_] !== undefined;
}
export async function callFunction(module: string, functionName: string, args: unknown[]) {
if (!isFunctionRegistered(module, functionName)) {
throw new Error(`Function ${module}.${functionName} is not registered`);
}
const promise: InteropReturn = ImageLoader.queryCache([
"__pyon_bridge",
JSON.stringify({ m: module, f: functionName, a: args }),
]);
const result = await promise;
if ("ret" in result) {
return JSON.parse(result.ret);
}
if ("cancelled" in result) {
throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`);
}
if ("err" in result) {
throw new Error(result.err);
}
}
export interface LoaderModule {
hasFunction(functionName: string): boolean;
callFunction(functionName: string, args: unknown[]): Promise<unknown>;
}
type ModuleProps<T extends LoaderModule> = {
name: string;
argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>;
};
export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) {
const module = loaderPayload.loader.modules[name];
if (!module) {
throw new Error(`Module ${name} is not registered`);
}
const baseObject: Record<string, any> = {
hasFunction: (functionName: string) => isFunctionRegistered(name, functionName),
callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args),
};
for (const [functionName] of Object.entries(module.functions)) {
baseObject[functionName] = (...args: unknown[]) =>
callFunction(
@ -68,10 +53,8 @@ export function getModule<T extends LoaderModule>({ name, argumentProcessors: pr
processArgs && functionName in processArgs ? processArgs[functionName](args) : args,
);
}
return baseObject as T;
}
/**
* Determines if safe mode is currently enabled. The result remains consistent over time.
* This will not change until the app is restarted. For draft changes, use the InitConfigStore.
@ -79,4 +62,4 @@ export function getModule<T extends LoaderModule>({ name, argumentProcessors: pr
*/
export function isSafeModeEnabled() {
return Boolean(loaderPayload.loader.initConfig.safeMode);
}
}

View file

@ -5,14 +5,15 @@ import type { ImageURISource } from "react-native";
import { patchTabsUI } from "./patches/tabs";
export interface RowConfig {
key: string;
title: () => string;
onPress?: () => any;
render?: Parameters<typeof lazy>[0];
icon?: ImageURISource | number;
IconComponent?: React.ReactNode,
usePredicate?: () => boolean,
useTrailing?: () => string | JSX.Element
key: string;
title: () => string;
onPress?: () => any;
render?: Parameters<typeof lazy>[0];
icon?: ImageURISource | number;
IconComponent?: React.ReactNode;
usePredicate?: () => boolean;
useTrailing?: () => string | JSX.Element;
rawTabsConfig?: Record<string, any>;
}
export const registeredSections = {} as {

View file

@ -2,6 +2,7 @@ import { after } from "@lib/api/patcher";
import { TableRow } from "@metro/common/components";
import { findByNameLazy, findByPropsLazy } from "@metro/wrappers";
import { registeredSections } from "@ui/settings";
import { Strings } from "@core/i18n";
import { CustomPageRenderer, wrapOnPress } from "./shared";
import { findInReactTree } from "@lib/utils";
@ -29,8 +30,11 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
usePredicate: row.usePredicate,
useTrailing: row.useTrailing,
useTitle: row.title,
useBadge: () => undefined,
useIcon: () => row.icon,
onPress: wrapOnPress(row.onPress, null, row.render, row.title()),
withArrow: true,
...row.rawTabsConfig,
},
})),
)
@ -48,6 +52,9 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
type: "route",
title: () => "ShiggyCord",
useTitle: () => "ShiggyCord",
useIcon: () => undefined,
useBadge: () => undefined,
IconComponent: () => undefined,
screen: {
route: "VendettaCustomPage",
getComponent: () => CustomPageRenderer,
@ -57,6 +64,9 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
type: "route",
title: () => "ShiggyCord",
useTitle: () => "ShiggyCord",
useIcon: () => undefined,
useBadge: () => undefined,
IconComponent: () => undefined,
screen: {
route: "SHIGGYCORD_CUSTOM_PAGE",
getComponent: () => CustomPageRenderer,
@ -66,6 +76,9 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
type: "route",
title: () => "ShiggyCord",
useTitle: () => "ShiggyCord",
useIcon: () => undefined,
useBadge: () => undefined,
IconComponent: () => undefined,
screen: {
route: "BUNNY_CUSTOM_PAGE",
getComponent: () => CustomPageRenderer,

View file

@ -117,13 +117,17 @@ for (const [id, moduleExports] of cacher.getModules()) {
}
const exportsKeysLength = Reflect.ownKeys(actualExports).length;
if (_source[prop] && exportsKeysLength <= _source[prop]) {
if (_source[prop] && exportsKeysLength >= _source[prop]) {
continue;
}
_module[prop] = actualExports[prop];
_source[prop] = Reflect.ownKeys(actualExports).length;
cacher.cacheId(id);
if (exportsKeysLength === 1) {
redesignProps.delete(prop);
}
}
}