From 51e34bbdb5598511132df88a3adecdbcd0302712 Mon Sep 17 00:00:00 2001 From: Ally Founder Date: Sun, 2 Aug 2026 19:05:06 -0400 Subject: [PATCH] Fix BetterTimestamps/RoleColorEverywhere: RowManager lookup raced Metro's own module init --- .../src/patches/rowManager.ts | 68 +++++++++++-------- .../role-color-everywhere/src/patches/rows.ts | 33 ++++++--- 2 files changed, 66 insertions(+), 35 deletions(-) diff --git a/plugins/better-timestamps/src/patches/rowManager.ts b/plugins/better-timestamps/src/patches/rowManager.ts index 827eff1..d28bc60 100644 --- a/plugins/better-timestamps/src/patches/rowManager.ts +++ b/plugins/better-timestamps/src/patches/rowManager.ts @@ -1,39 +1,53 @@ -import { findByName } from "@vendetta/metro"; import { moment } from "@vendetta/metro/common"; import { after, before } from "@vendetta/patcher"; import { storage } from "@vendetta/plugin"; +import { waitFor } from "@shared/lib/waitFor"; +import { rawFindByName } from "@shared/lib/rawFind"; import renderTimestamp from "../lib/renderTimestamp"; -const RowManager = findByName("RowManager", false); - +// RowManager used to be looked up eagerly at module-import time with the cached findByName - a +// plugin's top-level code runs as soon as Discord requires its bundle, which can be before Discord's +// own code has required RowManager itself, and Revenge's findByName permanently caches a negative +// result. Confirmed live via Key Inspector's Eval console: a raw, uncached scan found +// RowManager.prototype.generate present and correct once the module had actually initialized - the +// class itself was never the problem, just the timing of a one-shot lookup racing it. export default function patchRowManager(): () => void { - if (!RowManager?.prototype?.generate) return () => {}; - const patches: (() => void)[] = []; - patches.push(before("generate", RowManager.prototype, ([row]: any[]) => { - try { - if (row.rowType === 1) { - if (storage.separateMessages) row.isFirst = true; - row.message.__customTimestamp = renderTimestamp(row.message.timestamp); - } else if (row.rowType === "day") { - row.text = renderTimestamp(moment(row.text, "LL")); - } - } catch { - // Leave this row's timestamp untouched. - } - })); + const handle = waitFor( + () => { + const RowManager = rawFindByName("RowManager"); + return RowManager?.prototype?.generate ? RowManager : undefined; + }, + (RowManager) => { + patches.push(before("generate", RowManager.prototype, ([row]: any[]) => { + try { + if (row.rowType === 1) { + if (storage.separateMessages) row.isFirst = true; + row.message.__customTimestamp = renderTimestamp(row.message.timestamp); + } else if (row.rowType === "day") { + row.text = renderTimestamp(moment(row.text, "LL")); + } + } catch { + // Leave this row's timestamp untouched. + } + })); - patches.push(after("generate", RowManager.prototype, ([row]: any[], result: any) => { - try { - if (row.rowType !== 1) return; - if (row.message?.__customTimestamp && result?.message?.state === "SENT" && result.message.timestamp) { - result.message.timestamp = row.message.__customTimestamp; - } - } catch { - // Leave the default timestamp. + patches.push(after("generate", RowManager.prototype, ([row]: any[], result: any) => { + try { + if (row.rowType !== 1) return; + if (row.message?.__customTimestamp && result?.message?.state === "SENT" && result.message.timestamp) { + result.message.timestamp = row.message.__customTimestamp; + } + } catch { + // Leave the default timestamp. + } + })); } - })); + ); - return () => patches.forEach((p) => p()); + return () => { + handle.cancel(); + patches.forEach((p) => p()); + }; } diff --git a/plugins/role-color-everywhere/src/patches/rows.ts b/plugins/role-color-everywhere/src/patches/rows.ts index 9bd6149..117f22b 100644 --- a/plugins/role-color-everywhere/src/patches/rows.ts +++ b/plugins/role-color-everywhere/src/patches/rows.ts @@ -1,12 +1,13 @@ -import { findByName, findByStoreName } from "@vendetta/metro"; +import { findByStoreName } from "@vendetta/metro"; import { ReactNative } from "@vendetta/metro/common"; import { after, before } from "@vendetta/patcher"; import { storage } from "@vendetta/plugin"; +import { waitFor } from "@shared/lib/waitFor"; +import { rawFindByName } from "@shared/lib/rawFind"; import { defaultTextColor, interpolateColor } from "../lib/color"; const GuildMemberStore = findByStoreName("GuildMemberStore"); const ChannelStore = findByStoreName("ChannelStore"); -const RowManager = findByName("RowManager", false); function patchComponents(component: any, funcs: any[], args: any[], tree?: any[]): any { if (!component) return component; @@ -118,11 +119,27 @@ export default function patchRows(): () => void { }); } - if (RowManager?.prototype?.generate) { - return after("generate", RowManager.prototype, (_: any, row: any) => { - try { handleRow(row); } catch { /* skip this row */ } - }); - } + // RowManager used to be looked up eagerly at module-import time with the cached findByName - a + // plugin's top-level code can run before Discord's own code has required RowManager itself, and + // Revenge's findByName permanently caches a negative result. Confirmed live via Key Inspector's + // Eval console: a raw, uncached scan found RowManager.prototype.generate present once the module + // had actually initialized - waitFor + a raw lookup retries until that happens instead of giving + // up on the first (possibly premature) miss. + const patches: (() => void)[] = []; + const handle = waitFor( + () => { + const RowManager = rawFindByName("RowManager"); + return RowManager?.prototype?.generate ? RowManager : undefined; + }, + (RowManager) => { + patches.push(after("generate", RowManager.prototype, (_: any, row: any) => { + try { handleRow(row); } catch { /* skip this row */ } + })); + } + ); - return () => {}; + return () => { + handle.cancel(); + patches.forEach((p) => p()); + }; }