diff --git a/plugins/better-timestamps/src/ui/Settings.tsx b/plugins/better-timestamps/src/ui/Settings.tsx index 87d36b5..aee8330 100644 --- a/plugins/better-timestamps/src/ui/Settings.tsx +++ b/plugins/better-timestamps/src/ui/Settings.tsx @@ -3,6 +3,7 @@ import { storage } from "@vendetta/plugin"; import { useProxy } from "@vendetta/storage"; import { Forms } from "@vendetta/ui/components"; import SettingsScaffold from "@shared/ui/SettingsScaffold"; +import { resolveSemanticColorSafe } from "@shared/lib/color"; import renderTimestamp, { TimestampMode } from "../lib/renderTimestamp"; const { Text } = ReactNative; @@ -31,7 +32,7 @@ export default function Settings() { ✓ : undefined} + trailing={selected ? ✓ : undefined} onPress={() => { storage.selected = mode.key; }} /> {mode.key === "custom" && selected && ( diff --git a/plugins/key-inspector/src/ui/Settings.tsx b/plugins/key-inspector/src/ui/Settings.tsx index cb6c99e..3c1791b 100644 --- a/plugins/key-inspector/src/ui/Settings.tsx +++ b/plugins/key-inspector/src/ui/Settings.tsx @@ -6,6 +6,7 @@ import SettingsScaffold from "@shared/ui/SettingsScaffold"; import NoteBox from "@shared/ui/NoteBox"; import PrimaryButton from "@shared/ui/PrimaryButton"; import { rawFindByProps, rawFindByName, rawFindByTypeName, rawFindByStoreName } from "@shared/lib/rawFind"; +import { resolveSemanticColorSafe } from "@shared/lib/color"; import { runAllChecks, formatReport } from "../lib/checks"; import { dumpVendettaApiTree } from "../lib/apiTree"; import { runEval } from "../lib/evalTool"; @@ -15,6 +16,10 @@ import { captureComponentRenders } from "../lib/renderLogger"; const { View, Text } = ReactNative; const { FormSection, FormInput } = Forms; +// Every raw Text below used to have no explicit color at all - illegible black-on-black on +// Discord's dark theme, since RN's Text defaults to black with no theming of its own. +const textColor = () => resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1"); + function FullScanSection() { const [summary, setSummary] = React.useState(null); @@ -32,7 +37,7 @@ function FullScanSection() { }} /> {summary && ( - {summary} + {summary} )} @@ -77,11 +82,11 @@ function EvalSection() { onPress={run} /> {result != null && ( - + {result.length > 2000 ? `${result.slice(0, 2000)}\n... (truncated, full result copied)` : result} )} - + Runs as a plain (non-async) function body - a bare expression or `return ...` works, but not the async/await keywords (Hermes doesn't support those in dynamically-eval'd code). For timed/async checks, return a Promise built with a @@ -142,11 +147,11 @@ function FluxLoggerSection() { onPress={capture} /> {result != null && ( - + {result.length > 2000 ? `${result.slice(0, 2000)}\n... (truncated, full report copied)` : result} )} - + Tap start, then go do the thing you're trying to understand (open a screen, send a message, tap a button) before the window closes. Reports every action type that dispatched, sorted by count - the fastest way to find which event to hook into @@ -198,11 +203,11 @@ function RenderLoggerSection() { onPress={capture} /> {result != null && ( - + {result.length > 2000 ? `${result.slice(0, 2000)}\n... (truncated, full result copied)` : result} )} - + Patches the component to log its actual props on every render, up to the call cap or the time limit. Only works for React.memo/forwardRef-wrapped components (most of what findByTypeName/findByName return) - a bare function component has no @@ -275,7 +280,7 @@ function RawSearchSection() { onPress={() => storeQuery.trim() && run(`rawFindByStoreName("${storeQuery.trim()}")`, () => rawFindByStoreName(storeQuery.trim()))} /> - + Revenge's own findByProps/findByName/findByTypeName/findByStoreName cache a negative result forever the first time a search comes up empty, and never rescan - so if a module hasn't been required yet when you search, it can look "missing" @@ -360,8 +365,8 @@ function FiberCaptureSection() { - {status} - + {status} + Reads React's own internal fiber pointer directly off a native view instance - works without React DevTools (confirmed absent on this build), since it's the same internal linkage React itself relies on to route touch events, not an @@ -387,7 +392,7 @@ function ApiTreeSection() { showToast(`Copied ${lineCount} lines - every key path under window.vendetta`, undefined); }} /> - + Every key path under the stable plugin API (window.vendetta), 3 levels deep - this is everything Revenge itself exposes to plugins, not Discord's raw internals (those are searchable individually below, since dumping all of them would be enormous). @@ -492,7 +497,7 @@ function ManualSearchSection() { - + "ALL matches" is worth checking even when the first-match search succeeds - more than one module matching the same name/props is a common source of "found the wrong one" bugs (patching a re-export or an unrelated duplicate instead of the diff --git a/plugins/pronoun-db/src/ui/PronounSection.tsx b/plugins/pronoun-db/src/ui/PronounSection.tsx index 5f1cfdd..3e26261 100644 --- a/plugins/pronoun-db/src/ui/PronounSection.tsx +++ b/plugins/pronoun-db/src/ui/PronounSection.tsx @@ -1,5 +1,6 @@ import { React, ReactNative } from "@vendetta/metro/common"; import { findByName } from "@vendetta/metro"; +import { resolveSemanticColorSafe } from "@shared/lib/color"; import { fetchPronouns, getCachedPronouns } from "../lib/pronounStore"; const { Text } = ReactNative; @@ -12,9 +13,13 @@ export default function PronounSection({ userId }: { userId: string }) { if (!pronouns || !UserProfileSection) return null; + // Had no explicit color at all - illegible black-on-black on Discord's dark theme, since RN's + // Text defaults to black with no theming of its own. + const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1"); + return ( - {pronouns} + {pronouns} ); } diff --git a/plugins/rose-utils/src/modules/MessageLogger/ui/LogViewerPage.tsx b/plugins/rose-utils/src/modules/MessageLogger/ui/LogViewerPage.tsx index 92ca482..cf260d7 100644 --- a/plugins/rose-utils/src/modules/MessageLogger/ui/LogViewerPage.tsx +++ b/plugins/rose-utils/src/modules/MessageLogger/ui/LogViewerPage.tsx @@ -3,6 +3,7 @@ import { React, ReactNative, clipboard } from "@vendetta/metro/common"; import { Forms } from "@vendetta/ui/components"; import { showToast } from "@vendetta/ui/toasts"; import { getAssetIDByName } from "@vendetta/ui/assets"; +import { resolveSemanticColorSafe } from "@shared/lib/color"; import { getLog, removeLogEntry } from "../lib/store"; import type { LoggedMessage } from "../lib/types"; @@ -20,6 +21,13 @@ const { FormInput } = Forms; const PAGE_SIZE = 100; +// Every raw Text below used to have no explicit color at all - illegible black-on-black on +// Discord's dark theme, since RN's Text defaults to black with no theming of its own. Same +// resolveSemanticColorSafe fallback-chain pattern used everywhere else in this repo. +function textColor(): string { + return resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1"); +} + function formatTime(iso: string | undefined): string { if (!iso) return "unknown time"; try { @@ -45,34 +53,36 @@ function entryMatches(entry: LoggedMessage, query: string): boolean { } function EntryDetail({ entry, onClose, onDeleted }: { entry: LoggedMessage; onClose: () => void; onDeleted: () => void }) { + const color = textColor(); + return ( - + {entry.kind.toUpperCase()} · {formatTime(entry.messageTimestamp)} · logged {formatTime(entry.loggedAt)} - + {entry.authorDisplayName || entry.authorUsername || entry.authorId || "Unknown author"} {entry.authorIsBot ? " [BOT]" : ""} {entry.kind === "edited" ? ( <> - Before: - {entry.content || "(empty)"} - After: - {entry.newContent || "(empty)"} + Before: + {entry.content || "(empty)"} + After: + {entry.newContent || "(empty)"} ) : ( - {entry.content || "(empty)"} + {entry.content || "(empty)"} )} {entry.attachments.length > 0 && ( - + Attachments ({entry.attachments.length}): {entry.attachments.map((a, i) => ( - + {a.filename}{a.url ? ` - ${a.url}` : ""} ))} @@ -81,11 +91,11 @@ function EntryDetail({ entry, onClose, onDeleted }: { entry: LoggedMessage; onCl {entry.embeds.length > 0 && ( - + Embeds ({entry.embeds.length}): {entry.embeds.map((e, i) => ( - + {e.title || e.url || "(untitled embed)"} ))} @@ -93,12 +103,12 @@ function EntryDetail({ entry, onClose, onDeleted }: { entry: LoggedMessage; onCl )} {entry.reactions.length > 0 && ( - + Reactions: {entry.reactions.map((r) => `${r.emoji} x${r.count}`).join(" ")} )} - + Message ID: {entry.id}{"\n"}Channel ID: {entry.channelId}{entry.guildId ? `\nServer ID: ${entry.guildId}` : ""} {entry.referencedMessageId ? `\nReplying to: ${entry.referencedMessageId}` : ""} @@ -110,7 +120,7 @@ function EntryDetail({ entry, onClose, onDeleted }: { entry: LoggedMessage; onCl showToast("Copied entry as JSON", getAssetIDByName("ic_copy_24px")); }} > - Copy JSON + Copy JSON { @@ -122,7 +132,7 @@ function EntryDetail({ entry, onClose, onDeleted }: { entry: LoggedMessage; onCl Delete entry - Back + Back @@ -135,6 +145,7 @@ function LogList() { const [selected, setSelected] = React.useState(null); const [visibleCount, setVisibleCount] = React.useState(PAGE_SIZE); const [, forceUpdate] = React.useReducer((n: number) => n + 1, 0); + const color = textColor(); const all = getLog(); const filtered = React.useMemo(() => { @@ -172,13 +183,13 @@ function LogList() { {(["all", "deleted", "edited"] as const).map((k) => ( setKindFilter(k)}> - + {k === "all" ? "All" : k === "deleted" ? "Deleted" : "Edited"} ))} - + {filtered.length} of {all.length} total @@ -190,20 +201,20 @@ function LogList() { onPress={() => setSelected(entry)} style={{ paddingHorizontal: 16, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: "rgba(128,128,128,0.15)" }} > - + {entry.kind === "deleted" ? "🗑" : "✎"} {entry.authorDisplayName || entry.authorUsername || entry.authorId || "Unknown"} · {formatTime(entry.messageTimestamp)} - + {(entry.kind === "deleted" ? entry.content : entry.newContent) || "(no text content)"} ))} {filtered.length === 0 && ( - No log entries match. + No log entries match. )} {visibleCount < filtered.length && ( setVisibleCount((n) => n + PAGE_SIZE)} style={{ padding: 16, alignItems: "center" }}> - Load more ({filtered.length - visibleCount} remaining) + Load more ({filtered.length - visibleCount} remaining) )} diff --git a/plugins/rose-utils/src/ui/Settings.tsx b/plugins/rose-utils/src/ui/Settings.tsx index 59c1c33..da34d90 100644 --- a/plugins/rose-utils/src/ui/Settings.tsx +++ b/plugins/rose-utils/src/ui/Settings.tsx @@ -1,14 +1,21 @@ import { React, ReactNative, clipboard } from "@vendetta/metro/common"; +import { useProxy } from "@vendetta/storage"; import { Forms } from "@vendetta/ui/components"; import { showToast } from "@vendetta/ui/toasts"; import SettingsScaffold from "@shared/ui/SettingsScaffold"; import NoteBox from "@shared/ui/NoteBox"; +import { resolveSemanticColorSafe } from "@shared/lib/color"; import modules from "../modules"; -import { ModuleCategory, type AnyModule, type ModuleSetting } from "../lib/Module"; +import { ModuleCategory, vstorage, type AnyModule, type ModuleSetting } from "../lib/Module"; const { View, Text, TouchableOpacity } = ReactNative; const { FormSection, FormSwitchRow, FormRow, FormInput } = Forms; +// Raw Text below this point (the category header and bulk-action links) had no explicit color at +// all - illegible black-on-black on Discord's dark theme, since RN's Text defaults to black with no +// theming of its own. Same resolveSemanticColorSafe fallback-chain pattern used everywhere else. +const textColor = () => resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1"); + function resolveSubLabel(setting: ModuleSetting, value: any): string | undefined { return typeof setting.subLabel === "function" ? setting.subLabel(value) : setting.subLabel; } @@ -98,6 +105,13 @@ function ModuleSettingRow({ module, settingKey }: { module: AnyModule; settingKe function ModuleSection({ module }: { module: AnyModule }) { module.useRefresh(); + // Belt and suspenders alongside the custom refresh() pub-sub above: module.storage.enabled is + // read for the switch's `value` prop right below, and useProxy is the same storage-reactivity + // mechanism every other plugin's settings screen in this repo already relies on for a toggle to + // visually update immediately - this repo's own custom listeners system covers module.errors + // too (plain in-memory state, not part of storage, so useProxy alone can't see it), which is + // why that mechanism exists at all rather than being replaceable by useProxy outright. + useProxy(module.storage); const errorEntries = Object.entries(module.errors); @@ -151,16 +165,16 @@ function CategoryHeader({ }} > - + {collapsed ? "▸" : "▾"} {category.toUpperCase()} · {enabledCount}/{mods.length} setModulesEnabled(mods, true)}> - All + All setModulesEnabled(mods, false)} style={{ marginLeft: 16 }}> - None + None @@ -210,12 +224,12 @@ export default function Settings() { setModulesEnabled(allVisible, true)}> - + Enable {searching ? "matching" : "all"} setModulesEnabled(allVisible, false)} style={{ marginLeft: 16 }}> - + Disable {searching ? "matching" : "all"} diff --git a/plugins/server-drawer/src/ui/NoteBox.tsx b/plugins/server-drawer/src/ui/NoteBox.tsx index 20cc42e..d8407bb 100644 --- a/plugins/server-drawer/src/ui/NoteBox.tsx +++ b/plugins/server-drawer/src/ui/NoteBox.tsx @@ -1,9 +1,16 @@ import { React, ReactNative } from "@vendetta/metro/common"; +import { resolveSemanticColorSafe } from "../lib/color"; const { View, Text } = ReactNative; -/** Small bordered callout for explanatory text in a settings screen - theme-agnostic by design. */ +/** + * Small bordered callout for explanatory text in a settings screen. Text color used to be unset + * entirely - React Native's Text defaults to black with no explicit color, illegible on Discord's + * dark theme (the default for most users). + */ export default function NoteBox({ children, style }: { children: any; style?: any }) { + const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT", "TEXT_MUTED"], "#96989d"); + return ( - {children} + {children} ); } diff --git a/plugins/view-raw/src/RawPage.tsx b/plugins/view-raw/src/RawPage.tsx index 5501126..c3bed8e 100644 --- a/plugins/view-raw/src/RawPage.tsx +++ b/plugins/view-raw/src/RawPage.tsx @@ -2,6 +2,7 @@ import { ReactNative, clipboard, React } from "@vendetta/metro/common"; import { showToast } from "@vendetta/ui/toasts"; import { getAssetIDByName } from "@vendetta/ui/assets"; import { Button } from "@vendetta/ui/components"; +import { resolveSemanticColorSafe } from "@shared/lib/color"; import { cleanMessage } from "./cleanMessage"; import JsonView from "./JsonView"; @@ -10,6 +11,10 @@ const { View, ScrollView, TextInput, Text } = ReactNative; export default function RawPage({ message }: { message: any }) { const [query, setQuery] = React.useState(""); const stringMessage = React.useMemo(() => JSON.stringify(cleanMessage(message), null, 4), [message.id]); + // The search box previously hardcoded white text (illegible on a light theme) and the two + // section labels below had no color at all (illegible black-on-black on a dark theme, RN's + // Text default with no theming of its own). + const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1"); return ( @@ -51,18 +56,18 @@ export default function RawPage({ message }: { message: any }) { paddingHorizontal: 10, paddingVertical: 8, marginBottom: 10, - color: "#fff" + color: textColor }} /> {!!message.content && ( - MESSAGE CONTENT - {message.content} + MESSAGE CONTENT + {message.content} )} - RAW DATA + RAW DATA diff --git a/shared/ui/ColorInput.tsx b/shared/ui/ColorInput.tsx index 5d385e2..b1c7e3e 100644 --- a/shared/ui/ColorInput.tsx +++ b/shared/ui/ColorInput.tsx @@ -1,5 +1,5 @@ import { React, ReactNative } from "@vendetta/metro/common"; -import { isValidHex } from "../lib/color"; +import { isValidHex, resolveSemanticColorSafe } from "../lib/color"; const { View, Text, TextInput, TouchableOpacity } = ReactNative; @@ -24,6 +24,9 @@ interface ColorInputProps { export default function ColorInput({ title, value, placeholder, onChange }: ColorInputProps) { const valid = !value || isValidHex(value); const swatchColor = isValidHex(value) ? value : (isValidHex(placeholder) ? placeholder : "#5865F2"); + // Same missing-color bug as NoteBox - Text with no explicit color defaults to black, illegible + // on Discord's dark theme. + const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1"); return ( @@ -39,7 +42,7 @@ export default function ColorInput({ title, value, placeholder, onChange }: Colo borderColor: "rgba(128,128,128,0.35)" }} /> - {title} + {title} diff --git a/shared/ui/NoteBox.tsx b/shared/ui/NoteBox.tsx index 20cc42e..349c926 100644 --- a/shared/ui/NoteBox.tsx +++ b/shared/ui/NoteBox.tsx @@ -1,9 +1,17 @@ import { React, ReactNative } from "@vendetta/metro/common"; +import { resolveSemanticColorSafe } from "../lib/color"; const { View, Text } = ReactNative; -/** Small bordered callout for explanatory text in a settings screen - theme-agnostic by design. */ +/** + * Small bordered callout for explanatory text in a settings screen. Text color used to be + * unset entirely - React Native's Text defaults to black with no explicit color, which is + * illegible on Discord's dark theme (the default for most users). Explicit theme-aware color now, + * same resolveSemanticColorSafe fallback-chain pattern used everywhere else in this repo. + */ export default function NoteBox({ children, style }: { children: any; style?: any }) { + const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT", "TEXT_MUTED"], "#96989d"); + return ( - {children} + {children} ); }