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+",
|
"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": [
|
"authors": [
|
||||||
{ "name": "everestmcarthur" },
|
{ "name": "everestmcarthur" },
|
||||||
{ "name": "Purple_Ξye™" }
|
{ "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 { React } from "@vendetta/metro/common";
|
||||||
import { instead } from "@vendetta/patcher";
|
import { instead } from "@vendetta/patcher";
|
||||||
import { storage } from "@vendetta/plugin";
|
import { storage } from "@vendetta/plugin";
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
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;
|
// Reset to Purple-EyeZ's original upstream logic, verbatim aside from the import path
|
||||||
let patchedComponent: any = null;
|
// (@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 {
|
export default function patchYouBarButtons(): () => void {
|
||||||
let unpatch: () => void = () => {};
|
const YouBarNotificationsButton = findByTypeName("YouBarNotificationsButton");
|
||||||
|
|
||||||
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 userSettingsAction = findByProps("openUserSettings");
|
const userSettingsAction = findByProps("openUserSettings");
|
||||||
const transitionModule = findByProps("transitionToGuild");
|
const transitionModule = findByProps("transitionToGuild");
|
||||||
const ChatIcon = getAssetIDByName("ChatIcon");
|
|
||||||
const SettingsIcon = getAssetIDByName("SettingsIcon");
|
const SettingsIcon = getAssetIDByName("SettingsIcon");
|
||||||
|
const ChatIcon = getAssetIDByName("ChatIcon");
|
||||||
|
|
||||||
|
if (!YouBarNotificationsButton) return () => {};
|
||||||
|
|
||||||
return instead("type", YouBarNotificationsButton, (args: any[], OriginalRender: (...a: any[]) => any) => {
|
return instead("type", YouBarNotificationsButton, (args: any[], OriginalRender: (...a: any[]) => any) => {
|
||||||
try {
|
const [, forceUpdate] = React.useReducer((x: number) => ~x, 0);
|
||||||
const [, forceUpdate] = React.useReducer((x: number) => ~x, 0);
|
|
||||||
internalForceUpdate = () => forceUpdate();
|
|
||||||
|
|
||||||
const res = OriginalRender(...args);
|
updateYouBar = () => forceUpdate();
|
||||||
if (!res?.props?.children) return res;
|
|
||||||
|
|
||||||
const IconButton = res.props.children.type;
|
const res = OriginalRender(...args);
|
||||||
const originalProps = res.props.children.props;
|
|
||||||
|
|
||||||
return (
|
if (!res?.props?.children) return res;
|
||||||
<React.Fragment>
|
|
||||||
{storage.showDMButton && (
|
const IconButton = res.props.children.type;
|
||||||
<IconButton
|
const originalProps = res.props.children.props;
|
||||||
variant={originalProps?.variant || "tertiary"}
|
|
||||||
size={originalProps?.size || "sm"}
|
return (
|
||||||
icon={ChatIcon}
|
<React.Fragment>
|
||||||
onPress={() => transitionModule?.transitionToGuild?.("@me")}
|
{storage.showDMButton && (
|
||||||
/>
|
<IconButton
|
||||||
)}
|
variant={originalProps?.variant || "tertiary"}
|
||||||
{storage.showSettingsButton && (
|
size={originalProps?.size || "sm"}
|
||||||
<IconButton
|
icon={ChatIcon}
|
||||||
variant={originalProps?.variant || "tertiary"}
|
onPress={() => {
|
||||||
size={originalProps?.size || "sm"}
|
transitionModule?.transitionToGuild?.("@me");
|
||||||
icon={SettingsIcon}
|
}}
|
||||||
onPress={() => userSettingsAction?.openUserSettings?.()}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
{res}
|
{storage.showSettingsButton && (
|
||||||
</React.Fragment>
|
<IconButton
|
||||||
);
|
variant={originalProps?.variant || "tertiary"}
|
||||||
} catch {
|
size={originalProps?.size || "sm"}
|
||||||
// A shape mismatch here should mean "stock YouBar, no extra buttons" - not a crash.
|
icon={SettingsIcon}
|
||||||
return OriginalRender(...args);
|
onPress={() => {
|
||||||
}
|
userSettingsAction?.openUserSettings?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{res}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ export default function Settings() {
|
||||||
return (
|
return (
|
||||||
<SettingsScaffold>
|
<SettingsScaffold>
|
||||||
<NoteBox>
|
<NoteBox>
|
||||||
Fork of Purple-EyeZ's YouBar+, ported to Revenge's Vendetta-compat API with a fix
|
Fork of Purple-EyeZ's YouBar+, ported to Revenge's Vendetta-compat API. Like the
|
||||||
for a startup race that could silently skip the button patch for an entire session.
|
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>
|
</NoteBox>
|
||||||
<FormSection title="Buttons">
|
<FormSection title="Buttons">
|
||||||
<FormSwitchRow
|
<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