Port YouBar+, fix startup race, add slot-based button config
This commit is contained in:
parent
12d8d9021d
commit
019589cf58
7 changed files with 238 additions and 0 deletions
|
|
@ -26,6 +26,7 @@ To install: open Discord with Revenge installed, go to **Settings → Plugins**,
|
|||
| FakeProfileThemesAndEffects | Revived | Profile theming and profile effects without Nitro. Fork of [shipwr3ckd's FPTE](https://github.com/shipwr3ckd/FPTE) - fixed a crash on every profile-editor open, confirmed via on-device diagnostics. | `https://rp.jarviscli.dev/fake-profile-themes-and-effects/install/` |
|
||||
| BetterTimestamps | Revived | Calendar-relative, fully relative, ISO 8601, or a custom moment.js timestamp format, for both messages and day dividers. | `https://rp.jarviscli.dev/better-timestamps/install/` |
|
||||
| CopyRoleColor | Revived | Long-press a role pill to copy its color as hex. | `https://rp.jarviscli.dev/copy-role-color/install/` |
|
||||
| YouBar+ | Revived | Up to 2 configurable buttons (None/Direct Messages/Settings) on the YouBar, in your chosen order. Fork of [Purple-EyeZ's YouBar+](https://github.com/Purple-EyeZ/RevengePlugins/tree/main/plugins/YouBarPlus) - fixed a startup race condition where the button patch could silently never apply for an entire session. | `https://rp.jarviscli.dev/you-bar-plus/install/` |
|
||||
|
||||
### Productivity
|
||||
|
||||
|
|
|
|||
9
plugins/you-bar-plus/manifest.json
Normal file
9
plugins/you-bar-plus/manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "YouBar+",
|
||||
"description": "Adds up to 2 configurable buttons (Direct Messages, Settings, or none) to the YouBar, in whatever order you pick.",
|
||||
"authors": [
|
||||
{ "name": "everestmcarthur" },
|
||||
{ "name": "Purple_Ξye™" }
|
||||
],
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
20
plugins/you-bar-plus/src/index.ts
Normal file
20
plugins/you-bar-plus/src/index.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { logger } from "@vendetta";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { applyPatches } from "@shared/lib/patcher";
|
||||
import patchYouBarButtons from "./patches/youBarButtons";
|
||||
import Settings from "./ui/Settings";
|
||||
|
||||
let unpatchAll: () => void = () => {};
|
||||
|
||||
export default {
|
||||
onLoad: () => {
|
||||
storage.slot1 ??= "dms";
|
||||
storage.slot2 ??= "settings";
|
||||
|
||||
unpatchAll = applyPatches("YouBar+", logger, {
|
||||
"YouBar buttons": patchYouBarButtons
|
||||
});
|
||||
},
|
||||
onUnload: () => unpatchAll(),
|
||||
settings: Settings
|
||||
};
|
||||
95
plugins/you-bar-plus/src/patches/youBarButtons.tsx
Normal file
95
plugins/you-bar-plus/src/patches/youBarButtons.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
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";
|
||||
|
||||
export type YouBarButtonAction = "none" | "dms" | "settings";
|
||||
|
||||
export const YOU_BAR_BUTTON_CHOICES: YouBarButtonAction[] = ["none", "dms", "settings"];
|
||||
|
||||
export const YOU_BAR_BUTTON_LABELS: Record<YouBarButtonAction, string> = {
|
||||
none: "None",
|
||||
dms: "Direct Messages",
|
||||
settings: "Settings"
|
||||
};
|
||||
|
||||
/** Lets the Settings screen force the patched YouBar to re-render right after a slot changes. */
|
||||
export let updateYouBar = () => {};
|
||||
|
||||
/**
|
||||
* `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 -
|
||||
* `waitFor` keeps retrying until the component shows up (or this patch gets unloaded first,
|
||||
* which cancels the pending wait).
|
||||
*/
|
||||
export default function patchYouBarButtons(): () => void {
|
||||
let unpatch: () => void = () => {};
|
||||
|
||||
const handle = waitFor(
|
||||
() => findByTypeName("YouBarNotificationsButton"),
|
||||
(YouBarNotificationsButton) => {
|
||||
unpatch = applyButtonPatch(YouBarNotificationsButton);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
handle.cancel();
|
||||
unpatch();
|
||||
};
|
||||
}
|
||||
|
||||
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 transitionModule = findByProps("transitionToGuild");
|
||||
const icons: Partial<Record<YouBarButtonAction, number | undefined>> = {
|
||||
dms: getAssetIDByName("ChatIcon"),
|
||||
settings: getAssetIDByName("SettingsIcon")
|
||||
};
|
||||
|
||||
const runAction = (action: YouBarButtonAction) => {
|
||||
if (action === "dms") transitionModule?.transitionToGuild?.("@me");
|
||||
else if (action === "settings") userSettingsAction?.openUserSettings?.();
|
||||
};
|
||||
|
||||
return instead("type", YouBarNotificationsButton, (args: any[], OriginalRender: (...a: any[]) => any) => {
|
||||
try {
|
||||
const [, forceUpdate] = React.useReducer((x: number) => ~x, 0);
|
||||
updateYouBar = () => forceUpdate();
|
||||
|
||||
const res = OriginalRender(...args);
|
||||
if (!res?.props?.children) return res;
|
||||
|
||||
const IconButton = res.props.children.type;
|
||||
const originalProps = res.props.children.props;
|
||||
// Two independent slots, rendered left to right - each can be off, DMs, or Settings.
|
||||
const slots: YouBarButtonAction[] = [storage.slot1, storage.slot2];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{slots.map((action, i) =>
|
||||
action === "none" ? null : (
|
||||
<IconButton
|
||||
key={`you-bar-plus-${i}`}
|
||||
variant={originalProps?.variant || "tertiary"}
|
||||
size={originalProps?.size || "sm"}
|
||||
icon={icons[action]}
|
||||
onPress={() => runAction(action)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{res}
|
||||
</React.Fragment>
|
||||
);
|
||||
} catch {
|
||||
// A shape mismatch here should mean "stock YouBar, no extra buttons" - not a crash.
|
||||
return OriginalRender(...args);
|
||||
}
|
||||
});
|
||||
}
|
||||
51
plugins/you-bar-plus/src/ui/Settings.tsx
Normal file
51
plugins/you-bar-plus/src/ui/Settings.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { React } from "@vendetta/metro/common";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { Forms } from "@vendetta/ui/components";
|
||||
import SettingsScaffold from "@shared/ui/SettingsScaffold";
|
||||
import NoteBox from "@shared/ui/NoteBox";
|
||||
import { updateYouBar, YOU_BAR_BUTTON_CHOICES, YOU_BAR_BUTTON_LABELS, type YouBarButtonAction } from "../patches/youBarButtons";
|
||||
|
||||
const { FormSection, FormRow } = Forms;
|
||||
|
||||
function nextChoice(current: YouBarButtonAction): YouBarButtonAction {
|
||||
const idx = YOU_BAR_BUTTON_CHOICES.indexOf(current);
|
||||
return YOU_BAR_BUTTON_CHOICES[(idx + 1) % YOU_BAR_BUTTON_CHOICES.length];
|
||||
}
|
||||
|
||||
function SlotRow({ label, slotKey }: { label: string; slotKey: "slot1" | "slot2" }) {
|
||||
const value: YouBarButtonAction = storage[slotKey];
|
||||
|
||||
return (
|
||||
<FormRow
|
||||
label={label}
|
||||
subLabel={YOU_BAR_BUTTON_LABELS[value]}
|
||||
onPress={() => {
|
||||
storage[slotKey] = nextChoice(value);
|
||||
updateYouBar();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
useProxy(storage);
|
||||
|
||||
// Also force one last re-render on the way out, in case a tap didn't reach a mounted YouBar.
|
||||
React.useEffect(() => () => updateYouBar(), []);
|
||||
|
||||
return (
|
||||
<SettingsScaffold>
|
||||
<NoteBox>
|
||||
YouBar+ adds up to 2 buttons to the YouBar - the row with your avatar and status near the top
|
||||
of the app. Tap a slot below to cycle it through None, Direct Messages, and Settings; the first
|
||||
slot renders on the left. 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.
|
||||
</NoteBox>
|
||||
<FormSection title="Buttons">
|
||||
<SlotRow label="First button" slotKey="slot1" />
|
||||
<SlotRow label="Second button" slotKey="slot2" />
|
||||
</FormSection>
|
||||
</SettingsScaffold>
|
||||
);
|
||||
}
|
||||
45
shared/lib/waitFor.ts
Normal file
45
shared/lib/waitFor.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
export interface WaitForHandle {
|
||||
/** Stops polling. Safe to call even after `find` has already resolved. */
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls `find` every `intervalMs` (default 100) until it returns a truthy value, then calls
|
||||
* `onFound` with it and stops polling.
|
||||
*
|
||||
* Needed because a plugin's onLoad can run before every Metro module/component it depends on has
|
||||
* actually been required - on a cold app start that's a real race, not an edge case. A one-shot
|
||||
* `findByX(...)` lookup at that exact moment can come back undefined and, without a retry, never
|
||||
* try again for the rest of the session. `waitFor` checks immediately (so there's no added delay
|
||||
* once the target is already registered) and keeps retrying otherwise.
|
||||
*
|
||||
* Returns a handle whose `cancel()` stops the poll - call it from a plugin's unpatch/onUnload so a
|
||||
* pending lookup doesn't keep firing (or apply a patch) after the plugin's been turned off.
|
||||
*/
|
||||
export function waitFor<T>(find: () => T | null | undefined, onFound: (value: T) => void, intervalMs = 100): WaitForHandle {
|
||||
let cancelled = false;
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
const immediate = find();
|
||||
if (immediate) {
|
||||
onFound(immediate);
|
||||
return { cancel() {} };
|
||||
}
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (cancelled) return;
|
||||
const value = find();
|
||||
if (value) {
|
||||
cancelled = true;
|
||||
clearInterval(timer);
|
||||
onFound(value);
|
||||
}
|
||||
}, intervalMs);
|
||||
|
||||
return {
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
if (timer) clearInterval(timer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -198,6 +198,23 @@
|
|||
"commands": [],
|
||||
"limitations": "Only the confirmed profile-editor crash was root-caused and fixed with real on-device data. The plugin's other patches (Nitro upsell card, purchase/profile data fetching) weren't individually re-audited - crash isolation means a problem there disables just that patch rather than the whole plugin, but hasn't been proven bug-free the way the color-resolution fix has."
|
||||
},
|
||||
"you-bar-plus": {
|
||||
"category": "Moderation & Appearance",
|
||||
"status": "revived",
|
||||
"accent": "#5865F2",
|
||||
"tagline": "Configurable buttons on the YouBar - up to 2 at a time, in your chosen order.",
|
||||
"note": "Fork of Purple-EyeZ's YouBar+. Fixed a startup race condition: the original did a single one-shot component lookup during onLoad and silently gave up for the rest of the session if it ran before the target had registered in Metro yet (a real risk on a cold app start) - restarting Discord just got lucky on a re-roll. This port polls for the component instead of looking it up once. Also replaced the original's two independent on/off toggles with a real slot-based system: 2 ordered slots, each set to None, Direct Messages, or Settings.",
|
||||
"howItWorks": "The YouBar is the row near your avatar/status at the top of the app. This plugin patches its notification button area to also render up to 2 extra icon buttons, each pulled from one of 2 configurable slots. Each slot cycles between None, Direct Messages (jumps to your DMs), and Settings (opens user settings) - tap a slot in the plugin's settings to cycle it. Slot order controls left-to-right button order, and each slot is independent, so you can run just one button, both, or neither. Both slots default to Direct Messages then Settings, matching the original plugin's default look.",
|
||||
"features": [
|
||||
"Up to 2 buttons on the YouBar, each independently configurable",
|
||||
"Choices per slot: None, Direct Messages, Settings",
|
||||
"Slot order controls left-to-right button order",
|
||||
"Settings changes apply immediately, no restart needed",
|
||||
"Startup race condition fixed - polls for the target component instead of a single one-shot lookup"
|
||||
],
|
||||
"commands": [],
|
||||
"limitations": "Only wires up to actions with a confirmed, still-working lookup (jump to DMs, open Settings) - no additional slot choices were added beyond what could be verified against a real findByProps target."
|
||||
},
|
||||
"rose-utils": {
|
||||
"category": "Utility",
|
||||
"status": "revived",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue