YouBar+: createElement interception, notification center rework

This commit is contained in:
Ally Founder 2026-08-20 11:56:05 -04:00
parent 35972b836d
commit ba0b6b6bfd

View file

@ -1,21 +1,18 @@
import { findByProps, findByName } from "@vendetta/metro";
import { findByProps, findByTypeName, findByName } from "@vendetta/metro";
import { React } from "@vendetta/metro/common";
import { instead } from "@vendetta/patcher";
import { storage } from "@vendetta/plugin";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { registerTypeDetector, registerIntercept, registerPropsTransform, patchCreateElement } from "@shared/lib/createElementIntercept";
import { registerPropsTransform, patchCreateElement } from "@shared/lib/createElementIntercept";
import NotificationCenter from "../ui/NotificationCenter";
// Retry logic lives one level up in index.ts, calling this repeatedly until it succeeds - keep
// this function itself simple.
export let updateYouBar = () => {};
// findByTypeName + instead("type", ...) used to patch this, but that mutates whatever object
// Metro's search hands back - which isn't reliably the same reference Discord's own render call
// site reads from in this bundle (confirmed live: the property write didn't stick, and a fresh
// findByTypeName call moments later returned undefined). registerTypeDetector instead captures
// the exact type value at the actual createElement/jsx call site, so there's no reference
// mismatch to go stale.
export default function patchYouBarButtons(): () => void {
/** Returns null (not undefined) when YouBarNotificationsButton isn't registered in Metro yet, so a caller can tell "not ready" apart from "patched, here's the unpatch fn". */
export default function patchYouBarButtons(): (() => void) | null {
const YouBarNotificationsButton = findByTypeName("YouBarNotificationsButton");
const userSettingsAction = findByProps("openUserSettings");
const transitionModule = findByProps("transitionToGuild");
@ -28,6 +25,8 @@ export default function patchYouBarButtons(): () => void {
const SettingsIcon = getAssetIDByName("SettingsIcon");
const ChatIcon = getAssetIDByName("ChatIcon");
if (!YouBarNotificationsButton) return null;
const openInbox = () => {
if (!Navigator || !Navigation?.push) {
console.error("[YouBar+] Inbox: Navigator not available");
@ -48,65 +47,59 @@ export default function patchYouBarButtons(): () => void {
));
};
const cleanups: (() => void)[] = [];
patchCreateElement(cleanups);
// The native bell is one of four sibling buttons YouBarNotificationsButton renders - the
// other three are unrelated icon buttons with raw numeric asset ids. accessibilityLabel is
// the only reliable way to pick the real bell out (confirmed live via the fiber tree).
const elementCleanups: (() => void)[] = [];
patchCreateElement(elementCleanups);
registerPropsTransform(
(props) => !!storage.showInboxButton && props?.accessibilityLabel === "Notifications",
(props) => ({ ...props, onPress: openInbox }),
);
registerTypeDetector(
"you-bar-plus-notifications-button",
(type) => typeof type === "function" && type.name === "YouBarNotificationsButton",
(YouBarNotificationsButton: any) => {
const PatchedYouBarNotificationsButton = (props: any) => {
const [, forceUpdate] = React.useReducer((x: number) => ~x, 0);
updateYouBar = () => forceUpdate();
const unpatchType = instead("type", YouBarNotificationsButton, (args: any[], OriginalRender: (...a: any[]) => any) => {
const [, forceUpdate] = React.useReducer((x: number) => ~x, 0);
const res = YouBarNotificationsButton(props);
updateYouBar = () => forceUpdate();
if (!res?.props?.children) return res;
const res = OriginalRender(...args);
const IconButton = res.props.children.type;
const originalProps = res.props.children.props;
if (!res?.props?.children) return res;
return (
<React.Fragment>
{storage.showDMButton && (
<IconButton
variant={originalProps?.variant || "tertiary"}
size={originalProps?.size || "sm"}
icon={ChatIcon}
onPress={() => {
transitionModule?.transitionToGuild?.("@me");
}}
/>
)}
const IconButton = res.props.children.type;
const originalProps = res.props.children.props;
{storage.showSettingsButton && (
<IconButton
variant={originalProps?.variant || "tertiary"}
size={originalProps?.size || "sm"}
icon={SettingsIcon}
onPress={() => {
userSettingsAction?.openUserSettings?.();
}}
/>
)}
return (
<React.Fragment>
{storage.showDMButton && (
<IconButton
variant={originalProps?.variant || "tertiary"}
size={originalProps?.size || "sm"}
icon={ChatIcon}
onPress={() => {
transitionModule?.transitionToGuild?.("@me");
}}
/>
)}
{res}
</React.Fragment>
);
};
{storage.showSettingsButton && (
<IconButton
variant={originalProps?.variant || "tertiary"}
size={originalProps?.size || "sm"}
icon={SettingsIcon}
onPress={() => {
userSettingsAction?.openUserSettings?.();
}}
/>
)}
registerIntercept(YouBarNotificationsButton, PatchedYouBarNotificationsButton);
},
{ persistent: true },
);
{res}
</React.Fragment>
);
});
return () => cleanups.forEach((fn) => fn());
return () => {
unpatchType();
elementCleanups.forEach((fn) => fn());
};
}