YouBar+: reset to upstream's exact original logic
This commit is contained in:
parent
c66ebf57b4
commit
3dc639e305
4 changed files with 54 additions and 132 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "YouBar+",
|
||||
"description": "Adds up to 2 configurable buttons (Direct Messages, Settings, or none) to the YouBar, in whatever order you pick.",
|
||||
"description": "Adds Settings and DM buttons to the YouBar. (May require a restart after enabling.)",
|
||||
"authors": [
|
||||
{ "name": "everestmcarthur" },
|
||||
{ "name": "Purple_Ξye™" }
|
||||
|
|
|
|||
|
|
@ -1,107 +1,67 @@
|
|||
import { findByProps } from "@vendetta/metro";
|
||||
import { findByProps, findByTypeName } 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 { waitFor } from "@shared/lib/waitFor";
|
||||
import { rawFindByTypeName } from "@shared/lib/rawFind";
|
||||
import { forceRerender } from "@shared/lib/forceRerender";
|
||||
|
||||
let internalForceUpdate: (() => void) | null = null;
|
||||
let patchedComponent: any = null;
|
||||
// Reset to Purple-EyeZ's original upstream logic, verbatim aside from the import path
|
||||
// (@revenge-mod/metro -> @vendetta/metro, since this repo only has the vendetta-compat alias
|
||||
// configured) - no retry loop, no raw* passive finder, no forced re-render. Every one of those
|
||||
// layers added this session was meant to fix "sometimes needs a reload," but on the actual device
|
||||
// buttons stopped appearing at all, even after ten restarts - worse than upstream's own documented
|
||||
// "requires a restart" baseline. Resetting to the exact known-quantity upstream version first, to
|
||||
// find out whether the *added* logic was the actual problem or something else in this port
|
||||
// (a renamed lookup, a build difference) was always broken and just wasn't visible under the retry
|
||||
// logic's own try/catch swallowing it.
|
||||
export let updateYouBar = () => {};
|
||||
|
||||
/**
|
||||
* Lets the Settings screen force the patched YouBar to re-render right after a toggle changes.
|
||||
* Prefers the cheap in-tree hook update once our wrapper has actually rendered at least once under
|
||||
* the patch; falls back to forceRerender's fiber-walk otherwise, since a memoized component that
|
||||
* hasn't rendered under the patch yet has no hook state to update through (see forceRerender.ts for
|
||||
* why that fallback is needed at all).
|
||||
*/
|
||||
export function updateYouBar(): void {
|
||||
if (internalForceUpdate) internalForceUpdate();
|
||||
else if (patchedComponent) forceRerender(patchedComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* `YouBarNotificationsButton` isn't guaranteed to be registered in Metro yet at the exact moment
|
||||
* onLoad runs - on a cold app start plugins can load before every screen component has actually
|
||||
* been required. The original version of this plugin did a single synchronous
|
||||
* `findByTypeName` lookup here and gave up for the whole session if it came back empty, which is
|
||||
* the "sometimes the buttons just don't show up, restarting fixes it" bug this port fixes.
|
||||
*
|
||||
* Uses rawFindByTypeName (not @vendetta/metro's findByTypeName) specifically because this call
|
||||
* gets retried by waitFor - Revenge's own findByTypeName permanently caches a "not found" result
|
||||
* and never rescans, so retrying the cached version would just hit that poisoned cache every time
|
||||
* instead of ever actually finding the component once it registers. Confirmed by reading Revenge's
|
||||
* own metro finder source.
|
||||
*/
|
||||
export default function patchYouBarButtons(): () => void {
|
||||
let unpatch: () => void = () => {};
|
||||
|
||||
const handle = waitFor(
|
||||
() => rawFindByTypeName("YouBarNotificationsButton"),
|
||||
(YouBarNotificationsButton) => {
|
||||
unpatch = applyButtonPatch(YouBarNotificationsButton);
|
||||
patchedComponent = YouBarNotificationsButton;
|
||||
// Finding the module and patching it doesn't make anything visible on its own - this
|
||||
// component is React.memo'd with a stable prop, so once mounted it can go the rest of
|
||||
// the session without ever rendering again on its own (see forceRerender.ts). Force
|
||||
// one pass right now instead of waiting on luck for the buttons to actually appear.
|
||||
forceRerender(YouBarNotificationsButton);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
handle.cancel();
|
||||
unpatch();
|
||||
internalForceUpdate = null;
|
||||
patchedComponent = null;
|
||||
};
|
||||
}
|
||||
|
||||
function applyButtonPatch(YouBarNotificationsButton: any): () => void {
|
||||
// Same lookups the original working version of this plugin used - this was a timing bug
|
||||
// (see above), not a renamed-API one, so these targets are left exactly as they were.
|
||||
const YouBarNotificationsButton = findByTypeName("YouBarNotificationsButton");
|
||||
const userSettingsAction = findByProps("openUserSettings");
|
||||
const transitionModule = findByProps("transitionToGuild");
|
||||
const ChatIcon = getAssetIDByName("ChatIcon");
|
||||
|
||||
const SettingsIcon = getAssetIDByName("SettingsIcon");
|
||||
const ChatIcon = getAssetIDByName("ChatIcon");
|
||||
|
||||
if (!YouBarNotificationsButton) return () => {};
|
||||
|
||||
return instead("type", YouBarNotificationsButton, (args: any[], OriginalRender: (...a: any[]) => any) => {
|
||||
try {
|
||||
const [, forceUpdate] = React.useReducer((x: number) => ~x, 0);
|
||||
internalForceUpdate = () => forceUpdate();
|
||||
const [, forceUpdate] = React.useReducer((x: number) => ~x, 0);
|
||||
|
||||
const res = OriginalRender(...args);
|
||||
if (!res?.props?.children) return res;
|
||||
updateYouBar = () => forceUpdate();
|
||||
|
||||
const IconButton = res.props.children.type;
|
||||
const originalProps = res.props.children.props;
|
||||
const res = OriginalRender(...args);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{storage.showDMButton && (
|
||||
<IconButton
|
||||
variant={originalProps?.variant || "tertiary"}
|
||||
size={originalProps?.size || "sm"}
|
||||
icon={ChatIcon}
|
||||
onPress={() => transitionModule?.transitionToGuild?.("@me")}
|
||||
/>
|
||||
)}
|
||||
{storage.showSettingsButton && (
|
||||
<IconButton
|
||||
variant={originalProps?.variant || "tertiary"}
|
||||
size={originalProps?.size || "sm"}
|
||||
icon={SettingsIcon}
|
||||
onPress={() => userSettingsAction?.openUserSettings?.()}
|
||||
/>
|
||||
)}
|
||||
{res}
|
||||
</React.Fragment>
|
||||
);
|
||||
} catch {
|
||||
// A shape mismatch here should mean "stock YouBar, no extra buttons" - not a crash.
|
||||
return OriginalRender(...args);
|
||||
}
|
||||
if (!res?.props?.children) return res;
|
||||
|
||||
const IconButton = res.props.children.type;
|
||||
const originalProps = res.props.children.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{storage.showDMButton && (
|
||||
<IconButton
|
||||
variant={originalProps?.variant || "tertiary"}
|
||||
size={originalProps?.size || "sm"}
|
||||
icon={ChatIcon}
|
||||
onPress={() => {
|
||||
transitionModule?.transitionToGuild?.("@me");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{storage.showSettingsButton && (
|
||||
<IconButton
|
||||
variant={originalProps?.variant || "tertiary"}
|
||||
size={originalProps?.size || "sm"}
|
||||
icon={SettingsIcon}
|
||||
onPress={() => {
|
||||
userSettingsAction?.openUserSettings?.();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{res}
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ export default function Settings() {
|
|||
return (
|
||||
<SettingsScaffold>
|
||||
<NoteBox>
|
||||
Fork of Purple-EyeZ's YouBar+, ported to Revenge's Vendetta-compat API with a fix
|
||||
for a startup race that could silently skip the button patch for an entire session.
|
||||
Fork of Purple-EyeZ's YouBar+, ported to Revenge's Vendetta-compat API. Like the
|
||||
original, this patches the YouBar the moment it loads - if you enable it while
|
||||
Discord's already running, you may need to restart once for the buttons to appear.
|
||||
</NoteBox>
|
||||
<FormSection title="Buttons">
|
||||
<FormSwitchRow
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
// The actual reason YouBar+'s buttons only ever showed up after a full reload: YouBar's own
|
||||
// button is wrapped in React.memo with the default shallow-prop comparator, and it only ever
|
||||
// receives one stable boolean prop. Once it mounts, React's own updateMemoComponent bailout means
|
||||
// it will *never* run its render function again - regardless of how often anything around it
|
||||
// re-renders - unless something upstream schedules real work that reaches this exact fiber. That
|
||||
// makes patching `.type` alone invisible until pure luck (an unrelated future re-render) happens
|
||||
// to touch it, which on a live device can be "eventually" or "never in this session" - matching
|
||||
// exactly what looked like "needs 2 reloads."
|
||||
//
|
||||
// Two earlier versions of this file tried to actively force that render pass:
|
||||
// - Walking React's DevTools fiber-root registry (__REACT_DEVTOOLS_GLOBAL_HOOK__). Confirmed via
|
||||
// Key Inspector on a real device that hook doesn't exist on this Discord build at all - that
|
||||
// whole approach was silently doing nothing.
|
||||
// - Calling setParams() on the current route via getRootNavigationRef(), to force a genuine
|
||||
// react-navigation state update. This ran unconditionally, synchronously, the moment the button
|
||||
// module first initializes - which on a cold boot can be very early, before navigation has
|
||||
// fully settled. After shipping it, buttons stopped appearing even after a full restart - a
|
||||
// real regression, not just "still needs a reload." Poking live navigation state as an automatic
|
||||
// side effect of applying a UI patch is a meaningfully bigger risk than anything else in this
|
||||
// fix, and it's the prime suspect, so it's been pulled entirely rather than debugged further
|
||||
// blind. (It also used the cached findByProps instead of this repo's raw* variants, so on a
|
||||
// build where that lookup wasn't registered yet, it would have permanently poisoned
|
||||
// getRootNavigationRef for the rest of the session - a second, independent reason to drop it.)
|
||||
//
|
||||
// What's left is the one part that's unconditionally safe: if `target` is a memo wrapper,
|
||||
// permanently override its `compare` so that whenever a future render pass *does* reach this fiber
|
||||
// for any reason, it can never bail out again. This doesn't make buttons appear instantly on a hot
|
||||
// toggle - that still needs a real trigger, which is worth another attempt, but only ever tested
|
||||
// manually first via Key Inspector's Eval tool where its effect can actually be observed before
|
||||
// it's wired back into anything that runs automatically.
|
||||
export function forceRerender(target: any): void {
|
||||
try {
|
||||
if (target && typeof target === "object" && "compare" in target) {
|
||||
target.compare = () => false;
|
||||
}
|
||||
} catch {
|
||||
// Best effort - a shape mismatch here just means no permanent bailout override.
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue