feat: native bridge (#4)

* feat(lib/modules/native): types for calls

* feat(lib): proper caching for `assets` and `modules` lib

* fix(lib/modules/metro/runtime): add flag once `importedAll` or `importedDefault` exports cached

* feat(lib/modules/native): add `fs`

* feat(plugins/settings): add Loader info in Revenge page

* feat: use native bridge for alerting errors

* chore(lib/modules/native): remove useless `String()` call on error

* feat(lib/modules/native/fs): rename `deleteFile` to `rm`

* feat(lib/plugins): save plugin states

* chore(preinit): correct arg name for native method
This commit is contained in:
Palm 2025-09-16 01:06:04 +07:00 • committed by GitHub
commit f89d523c0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 317 additions and 171 deletions

View file

@ -1,51 +1,50 @@
// TODO(lib/assets): do not depend on Discord's ClientInfoModule for versioning, use native interop
import { ClientInfoModule } from '@revenge-mod/discord/native'
import { getStorage } from '@revenge-mod/storage'
import {
callBridgeMethod,
callBridgeMethodSync,
} from '@revenge-mod/modules/native'
import { debounce } from '@revenge-mod/utils/callback'
import { mergeDeep } from '@revenge-mod/utils/object'
import type { Metro } from '@revenge-mod/modules/types'
import type { Asset } from './types'
const Version = 1
const Key = `${Version}.${ClientInfoModule.getConstants().Build}`
const ExpectedCacheVersion = 2
export const Uncached: Cache = {
data: {},
version: ExpectedCacheVersion,
}
// In-memory cache
export const cache: Cache = {}
export let cache: Cache =
callBridgeMethodSync('revenge.caches.assets.read', []) ?? Uncached
const CacheStorage = getStorage<Cache>(`revenge/assets.${Key}`, {
default: cache,
directory: 'cache',
})
// TODO(lib/assets/caches): This loads way too late and requires native interop to load earlier
export const cached = CacheStorage.get().then(cache_ => {
const cached = cache_ !== cache
// If we are not using the default value, merge it into the in-memory cache, then point the storage cache to the in-memory cache.
if (cached) {
Object.assign(cache, cache_)
CacheStorage.cache = cache
}
return cached
})
if (cache.version !== ExpectedCacheVersion) {
// TODO: Alert to user to update build
cache = Uncached
}
export interface Cache {
[key: Asset['name']]: {
[key: Asset['type']]: Metro.ModuleID
data: {
[key: Asset['name']]: {
[key: Asset['type']]: Metro.ModuleID
}
}
version: number
}
const save = debounce(() => {
CacheStorage.set({})
callBridgeMethod('revenge.caches.assets.write', [cache.data])
}, 1000)
export function cacheAsset(asset: Asset, moduleId: Metro.ModuleID) {
// Merge directly into the cache, only debouncing actual writes
mergeDeep(cache, {
[asset.name]: {
[asset.type]: moduleId,
},
})
const reg = (cache.data[asset.name] ??= {})
reg[asset.type] = moduleId
save()
}
declare module '@revenge-mod/modules/native' {
interface Methods {
'revenge.caches.assets.read': [[], Cache | null]
'revenge.caches.assets.write': [[data: Cache['data']], void]
}
}

View file

@ -48,7 +48,7 @@ export function* getCustomAssets(): Generator<CustomAsset> {
* Yields all registered packager assets, including ones with same name but different types.
*/
export function* getPackagerAssets(): Generator<PackagerAsset> {
for (const reg of Object.values(cache))
for (const reg of Object.values(cache.data))
for (const moduleId of Object.values(reg))
yield AssetsRegistry.getAssetByID(metroRequire(moduleId))
}
@ -77,7 +77,7 @@ export function getAssetByName(
export function getAssetsByName(
name: string,
): Record<Asset['type'], Asset> | undefined {
const reg = cache[name]
const reg = cache.data[name]
if (!reg) return
return Object.entries(reg).reduce(
@ -106,7 +106,7 @@ export function getAssetIdByName(
name: string,
type?: Asset['type'],
): AssetId | undefined {
const reg = cache[name]
const reg = cache.data[name]
if (!reg) return
if (type !== undefined) {
@ -127,7 +127,7 @@ export function getAssetIdByName(
* @returns The asset ID.
*/
export function registerAsset(asset: RegisterableAsset): AssetId {
if (cache[asset.name]?.[asset.type] !== undefined)
if (cache.data[asset.name]?.[asset.type] !== undefined)
throw new Error(
`Asset with name ${asset.name} and type ${asset.type} already exists!`,
)

View file

@ -15,7 +15,7 @@ import {
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
import { proxify } from '@revenge-mod/utils/proxy'
import { aOverrides } from './_internal'
import { cacheAsset, cached } from './caches'
import { cache, cacheAsset, Uncached } from './caches'
import type { Metro } from '@revenge-mod/modules/types'
import type { ReactNative } from '@revenge-mod/react/types'
import type { Asset, PackagerAsset } from './types'
@ -52,24 +52,20 @@ const unsubAR = waitForModules(
if (getModuleDependencies(id)!.length) {
unsubAR()
// TODO(lib/assets/caches): Ideally we should not wait for the cache promise here, see the promise impl for more info.
cached.then(cached => {
if (!cached) {
// Resolve reference once and keep in closure
const metroRequire = __r
if (cache === Uncached) {
// Resolve reference once and keep in closure
const metroRequire = __r
// More fragile way, but also more performant:
// There is exactly one asset before the reexported asset registry :/
const firstAssetModuleId = id - 1
for (const mId of mUninitialized) {
if (mId < firstAssetModuleId) continue
// More fragile way, but also more performant:
// There is exactly one asset before the reexported asset registry :/
const firstAssetModuleId = id - 1
for (const mId of mUninitialized) {
if (mId < firstAssetModuleId) continue
const deps = getModuleDependencies(mId)!
if (deps.length === 1 && deps[0] === id)
metroRequire(mId)
}
const deps = getModuleDependencies(mId)!
if (deps.length === 1 && deps[0] === id) metroRequire(mId)
}
})
}
// We already patched the original asset registry, so we don't need to patch again.
return

View file

@ -9,7 +9,7 @@ import {
} from '@revenge-mod/modules/finders/filters'
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
import { asap, noop } from '@revenge-mod/utils/callback'
import { cached, cacheFilterResultForId } from '#modules/src/caches'
import { cache, cacheFilterResultForId, Uncached } from '#modules/src/caches'
import { FilterResultFlags } from '#modules/src/finders/_internal'
import { FilterFlag } from '#modules/src/finders/filters/constants'
import type {
@ -140,13 +140,11 @@ waitForModules(withStore(), (store, id) => {
Stores[name] = store
})
cached.then(cached => {
if (!cached)
asap(() => {
const lookup = lookupModules(withStore(), {
uninitialized: true,
})
while (lookup.next().done);
if (cache === Uncached)
asap(() => {
const lookup = lookupModules(withStore(), {
uninitialized: true,
})
})
while (lookup.next().done);
})

View file

@ -19,7 +19,10 @@
"default": "./src/metro/utils.ts"
},
"./native": {
"default": "./src/native.ts"
"default": "./src/native/index.ts"
},
"./native/fs": {
"default": "./src/native/fs.ts"
},
"./types": {
"types": "./src/types.ts"

View file

@ -1,59 +1,61 @@
// TODO(lib/modules/caches): do not depend on Discord's ClientInfoModule for versioning, use native interop
import { ClientInfoModule } from '@revenge-mod/discord/native'
import { getStorage } from '@revenge-mod/storage'
import {
callBridgeMethod,
callBridgeMethodSync,
} from '@revenge-mod/modules/native'
import { debounce } from '@revenge-mod/utils/callback'
import type { Metro } from '@revenge-mod/modules/types'
import type { FilterResultFlag } from './finders/_internal'
import type { Filter } from './finders/filters'
const Version = 2
const Key = `${Version}.${ClientInfoModule.getConstants().Build}`
const ExpectedCacheVersion = 3
export const Uncached: Cache = {
blacklist: [],
finds: {},
version: ExpectedCacheVersion,
}
// In-memory cache
const cache: Cache = { b: [], f: {} }
export let cache: Cache =
callBridgeMethodSync('revenge.caches.modules.read', []) ?? Uncached
const CacheStorage = getStorage<Cache>(`revenge/modules.${Key}`, {
default: cache,
directory: 'cache',
})
if (cache.version !== ExpectedCacheVersion) {
// TODO: Alert to user to update build
cache = Uncached
}
// TODO(lib/modules/caches): This loads way too late and requires native interop to load earlier
export const cached = CacheStorage.get().then(cache_ => {
const cached = cache_ !== cache
// If we are not using the default value, merge it into the in-memory cache, then point the storage cache to the in-memory cache.
if (cached) {
Object.assign(cache, cache_)
CacheStorage.cache = cache
}
return cached
})
export type Blacklist = Metro.ModuleID[]
export type Finds = Record<
Filter['key'],
Record<Metro.ModuleID, FilterResultFlag> | null
>
export interface Cache {
b: Metro.ModuleID[]
f: Record<
Filter['key'],
Record<Metro.ModuleID, FilterResultFlag> | null | undefined
>
blacklist: Blacklist
finds: Finds
version: number
}
const save = debounce(() => {
CacheStorage.set({})
callBridgeMethod('revenge.caches.modules.write', [
cache.blacklist,
cache.finds,
])
}, 1000)
export function cacheBlacklistedModule(id: Metro.ModuleID): boolean {
cache.b.push(id)
cache.blacklist.push(id)
save()
return true
}
export function cacheFilterResultForId(
key: Filter['key'],
key: keyof Finds,
id: Metro.ModuleID,
flag: FilterResultFlag,
): FilterResultFlag {
const reg = (cache.f[key] ??= {})
const reg = (cache.finds[key] ??= {})
reg[id] = flag
save()
@ -61,12 +63,22 @@ export function cacheFilterResultForId(
return flag
}
export function cacheFilterNotFound(key: Filter['key']) {
cache.f[key] = null
export function cacheFilterNotFound(key: keyof Finds) {
cache.finds[key] = null
}
export const getCachedFilterRegistry = (
key: Filter['key'],
): Cache['f'][Filter['key']] | undefined => cache.f[key]
export const getFilterMatches = (
key: keyof Finds,
): Finds[keyof Finds] | undefined => cache.finds[key]
export const getCachedBlacklistedModules = () => cache.b
export const getBlacklistedModules = () => cache.blacklist
declare module '@revenge-mod/modules/native' {
interface Methods {
'revenge.caches.modules.read': [[], Cache | null]
'revenge.caches.modules.write': [
[blacklist: Blacklist, finds: Finds],
void,
]
}
}

View file

@ -1,7 +1,7 @@
import { getCurrentStack } from '@revenge-mod/utils/error'
import { cacheFilterResultForId } from '../caches'
import { metroRequire } from '../metro/custom'
import { mInitialized } from '../metro/patches'
import { metroRequire } from '../metro/runtime'
import { isModuleExportBad } from '../metro/utils'
import { FilterFlag } from './filters'
import type { If } from '@revenge-mod/utils/types'

View file

@ -1,13 +1,13 @@
import { getCurrentStack } from '@revenge-mod/utils/error'
import { proxify } from '@revenge-mod/utils/proxy'
import { cacheFilterNotFound, getCachedFilterRegistry } from '../caches'
import { metroRequire } from '../metro/custom'
import { cacheFilterNotFound, getFilterMatches } from '../caches'
import {
mImportedPaths,
mInitialized,
mList,
mUninitialized,
} from '../metro/patches'
import { metroRequire } from '../metro/runtime'
import {
getInitializedModuleExports,
isModuleInitialized,
@ -163,7 +163,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
if (options?.cached ?? true) {
const notInit = !(options?.initialize ?? true)
const reg = getCachedFilterRegistry(filter.key)
const reg = getFilterMatches(filter.key)
// Return early if previous lookup was a full lookup and no modules were found
if (reg === null) return
@ -307,7 +307,7 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
if (options?.cached ?? true) {
const notInit = !(options?.initialize ?? true)
const reg = getCachedFilterRegistry(filter.key)
const reg = getFilterMatches(filter.key)
// Return early if previous lookup was a full lookup and no modules were found
if (reg === null) return NotFoundResult

View file

@ -1,5 +1,5 @@
import { noop } from '@revenge-mod/utils/callback'
import { getCachedFilterRegistry } from '../caches'
import { getFilterMatches } from '../caches'
import { mInitialized } from '../metro/patches'
import {
onAnyModuleInitialized,
@ -97,13 +97,13 @@ export function waitForModules(
options?: WaitForModulesOptions,
): WaitForModulesUnsubscribeFunction {
if (options?.cached) {
const reg = getCachedFilterRegistry(filter.key)
const reg = getFilterMatches(filter.key)
if (reg === null) return noop
if (reg) {
if (__BUILD_FLAG_DEBUG_MODULE_WAITS__)
nativeLoggingHook(
`\u001b[32mUsing cached results for wait \u001b[33m${filter.key}\u001b[0m`,
`\u001b[32mUsing cached results for wait: \u001b[33m${filter.key}\u001b[0m`,
1,
)

View file

@ -1,14 +1,10 @@
import {
cacheBlacklistedModule,
cached,
getCachedBlacklistedModules,
} from '../caches'
import { cache, cacheBlacklistedModule, Uncached } from '../caches'
import {
global,
metroImportAll,
metroImportDefault,
metroRequire,
} from './custom'
} from './runtime'
import { onModuleInitialized } from './subscriptions'
import {
executeImportedPathSubscriptions,
@ -144,11 +140,8 @@ function handleFactoryCall(
/// MODULE PATCHES AND BLACKLISTS
// Restore blacklists
cached.then(cached => {
if (cached)
for (const id of getCachedBlacklistedModules())
mUninitialized.delete(id)
})
if (cache !== Uncached)
for (const id of cache.blacklist) mUninitialized.delete(id)
const ImportTrackerModuleId = 2

View file

@ -1,6 +1,8 @@
/**
* A more performant implementation of Metro's core functions.
* A minimal implementation of Metro's runtime with little overhead.
* Making initialization faster and use less resources.
*
* Also avoids cloning exports, allowing for patches to be applied directly without checking for clones.
*/
import { mList } from './patches'
@ -58,6 +60,9 @@ export const metroImportDefault: Metro.RequireFn = moduleId => {
if (mod.flags & HasImportedDefault) return mod.importedDefault
const exports = metroRequire(moduleId)
mod.flags |= HasImportedDefault
return (mod.importedDefault = exports?.__esModule
? exports.default
: exports)
@ -68,7 +73,11 @@ export const metroImportAll: Metro.RequireFn = moduleId => {
if (mod.flags & HasImportedAll) return mod.importedAll
const exports = metroRequire(moduleId)
// Our implementation doesn't match Metro's because we modify the exports directly instead of cloning
// But this is why ours is superior, it allows patching the exports without needing to do it more than a single time
if (!exports?.__esModule) exports.default = exports
mod.flags |= HasImportedAll
return (mod.importedAll = exports)
}

View file

@ -1,6 +1,6 @@
import { isProxy } from '@revenge-mod/utils/proxy'
import { Initialized } from './custom'
import { mDeps, mList } from './patches'
import { Initialized } from './runtime'
import type { Metro } from '../types'
/**

View file

@ -0,0 +1,45 @@
import {
callBridgeMethod,
callBridgeMethodSync,
} from '@revenge-mod/modules/native'
export function readFile(path: string) {
return callBridgeMethod('revenge.fs.read', [path])
}
export function writeFile(path: string, data: string) {
return callBridgeMethod('revenge.fs.write', [path, data])
}
export function exists(path: string) {
return callBridgeMethod('revenge.fs.exists', [path])
}
export function rm(path: string) {
return callBridgeMethod('revenge.fs.delete', [path])
}
export function readFileSync(path: string) {
return callBridgeMethodSync('revenge.fs.read', [path])
}
export function writeFileSync(path: string, data: string) {
return callBridgeMethodSync('revenge.fs.write', [path, data])
}
export function rmSync(path: string) {
return callBridgeMethodSync('revenge.fs.exists', [path])
}
export function deleteFileSync(path: string) {
return callBridgeMethodSync('revenge.fs.delete', [path])
}
declare module '@revenge-mod/modules/native' {
export interface Methods {
'revenge.fs.read': [[path: string], string]
'revenge.fs.write': [[path: string, data: string], void]
'revenge.fs.exists': [[path: string], boolean]
'revenge.fs.delete': [[path: string], boolean]
}
}

View file

@ -45,16 +45,22 @@ function makePayload(name: string, args: any[]): object {
* @param args The arguments to pass to the native method.
* @returns A promise that resolves with the result of the native method call.
*/
export async function callMethod<T>(name: string, args: any[]): Promise<T> {
const result = await BridgePromise.readAsDataURL(makePayload(name, args))
export async function callBridgeMethod<N extends MethodName>(
name: N,
args: MethodArgs<N>,
): Promise<MethodResult<N>> {
try {
const result = await BridgePromise.readAsDataURL(
makePayload(name, args),
)
if ('error' in result)
throw new Error(`Call failed: ${result.error as string}`)
if ('result' in result) return result.result as T
if ('error' in result) throw result.error
if ('result' in result) return result.result as MethodResult<N>
throw new Error(
'Call failed: The module did not return a valid result. The native hook must have failed.',
)
throw 'The module did not return a valid result. The native hook must have failed.'
} catch (error) {
throw new Error(`Call failed: ${error}`)
}
}
/**
@ -66,17 +72,46 @@ export async function callMethod<T>(name: string, args: any[]): Promise<T> {
* @param args The arguments to pass to the native method.
* @returns The result of the native method call.
*/
export function callMethodSync<T>(name: string, args: any[]): T {
export function callBridgeMethodSync<N extends MethodName>(
name: N,
args: MethodArgs<N>,
): MethodResult<N> {
try {
const result = Bridge.getBBox(0, makePayload(name, args))
if ('error' in result) throw result.error
if ('result' in result) return result.result as T
if ('result' in result) return result.result as MethodResult<N>
throw 'The module did not return a valid result. The native hook must have failed.'
} catch (error) {
throw new Error(
`Call failed: ${error instanceof Error ? error.message : String(error)}`,
)
throw new Error(`Call failed: ${error}`)
}
}
/**
* Get the bridge information.
*/
export function getBridgeInfo(): BridgeInfo | null {
try {
return callBridgeMethodSync('revenge.info', [])
} catch (e) {
nativeLoggingHook(
`\u001b[31mFailed to get native bridge info: ${e}\u001b[0m`,
2,
)
return null
}
}
export interface BridgeInfo {
name: string
version: number
}
export type MethodName = Extract<keyof Methods, string>
export type MethodArgs<T extends MethodName> = Methods[T][0]
export type MethodResult<T extends MethodName> = Methods[T][1]
export interface Methods {
'revenge.info': [[], BridgeInfo]
}

View file

@ -1,8 +1,16 @@
import { TypedEventEmitter } from '@revenge-mod/discord/common/utils'
import {
callBridgeMethod,
callBridgeMethodSync,
} from '@revenge-mod/modules/native'
import { getErrorStack } from '@revenge-mod/utils/error'
import { sleepReject } from '@revenge-mod/utils/promise'
import { pUnscopedApi as uapi } from '../apis'
import { PluginFlags as Flag, PluginStatus as Status } from '../constants'
import {
PluginFlags as Flag,
PersistentPluginFlags,
PluginStatus as Status,
} from '../constants'
import {
addPluginApiDecorator,
decoratePluginApi,
@ -83,13 +91,30 @@ export const pEmitter = new TypedEventEmitter<{
export const pList = new Map<PluginManifest['id'], AnyPlugin>()
export const pMetadata = new WeakMap<AnyPlugin, InternalPluginMeta>()
const { flags: PersistedFlags }: PersistedPluginStates = callBridgeMethodSync(
'revenge.plugins.states.read',
[],
) ?? {
flags: {},
}
pEmitter.on('flagUpdate', plugin => {
PersistedFlags[plugin.manifest.id] = plugin.flags & PersistentPluginFlags
callBridgeMethod('revenge.plugins.states.write', [PersistedFlags])
})
/**
* Registers a new plugin with the system.
*
* @param manifest The manifest of the plugin.
* @param options The options for the plugin.
* @param defflags The default flags for the plugin.
* @param iflags The internal flags for the plugin.
*/
export function registerPlugin<O extends PluginApiExtensionsOptions>(
manifest: PluginManifest,
options: PluginOptions<O>,
flags: number,
defflags: number,
iflags: number,
) {
// TODO(plugins): verify plugin manifest
@ -127,7 +152,7 @@ export function registerPlugin<O extends PluginApiExtensionsOptions>(
dependents: [],
handleError: e => handlePluginError(e, plugin),
options,
flags,
flags: PersistedFlags[manifest.id] ?? defflags,
}
pMetadata.set(plugin, meta)
@ -299,7 +324,6 @@ export async function disablePlugin(plugin: AnyPlugin) {
if (plugin.status && !(plugin.status & Status.Stopping))
await stopPlugin(plugin)
// TODO(plugins): write to storage
plugin.flags &= ~Flag.Enabled
pEmitter.emit('disabled', plugin)
}
@ -317,9 +341,7 @@ export async function enablePlugin(plugin: AnyPlugin) {
}),
)
// TODO(plugins): write to storage
plugin.flags |= Flag.Enabled
pEmitter.emit('enabled', plugin)
}
@ -557,3 +579,18 @@ async function cleanupPlugin(plugin: AnyPlugin, meta: InternalPluginMeta) {
await Promise.all(proms)
}
declare module '@revenge-mod/modules/native' {
interface Methods {
'revenge.plugins.states.read': [[], PersistedPluginStates | null]
'revenge.plugins.states.write': [[statuses: PersistedPluginFlags], void]
}
}
interface PersistedPluginStates {
flags: PersistedPluginFlags
}
interface PersistedPluginFlags {
[id: PluginManifest['id']]: number
}

View file

@ -3,6 +3,7 @@ import * as PluginApiModulesFindersFilters from '@revenge-mod/modules/finders/fi
import * as PluginApiModulesMetroSubscriptions from '@revenge-mod/modules/metro/subscriptions'
import * as PluginApiModulesMetroUtils from '@revenge-mod/modules/metro/utils'
import * as PluginApiModulesNative_ from '@revenge-mod/modules/native'
import * as PluginApiModulesNativeFileSystem from '@revenge-mod/modules/native/fs'
import { spreadDescriptors } from '.'
export interface PluginApiModules {
@ -11,7 +12,9 @@ export interface PluginApiModules {
native: PluginApiModulesNative
}
export type PluginApiModulesNative = typeof PluginApiModulesNative_
export type PluginApiModulesNative = typeof PluginApiModulesNative_ & {
fs: typeof PluginApiModulesNativeFileSystem
}
export type PluginApiModulesMetro =
// biome-ignore format: Don't
@ -30,5 +33,7 @@ export const modules: PluginApiModules = {
PluginApiModulesMetroUtils,
spreadDescriptors(PluginApiModulesMetroSubscriptions, {}),
),
native: PluginApiModulesNative_,
native: spreadDescriptors(PluginApiModulesNative_, {
fs: PluginApiModulesNativeFileSystem,
}),
}

View file

@ -21,6 +21,11 @@ export const PluginFlags = {
EnabledLate: 1 << 3,
}
/**
* A bitmask of {@link PluginFlags} that are persisted to storage.
*/
export const PersistentPluginFlags = PluginFlags.Enabled
/**
* The plugin status.
*/

View file

@ -3,7 +3,7 @@ import '@revenge-mod/react/init'
import '@revenge-mod/storage/init'
import { onRunApplication } from '@revenge-mod/react/native'
import { onError } from './preinit'
import { onError } from '~preinit'
const unsub = onRunApplication(() => {
unsub()

View file

@ -15,6 +15,7 @@ export const Setting = {
ReactVersion: 'ReactVersion',
ReactNativeVersion: 'ReactNativeVersion',
HermesVersion: 'HermesVersion',
LoaderVersion: 'LoaderVersion',
} as const
export const RouteNames = {

View file

@ -0,0 +1,19 @@
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
import { getBridgeInfo } from '@revenge-mod/modules/native'
import { Setting } from '../constants'
import { CopyableSetting } from './shared'
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
const bridgeInfo = getBridgeInfo()
const LoaderVersionSetting: SettingsItem = CopyableSetting(
{
parent: Setting.Revenge,
IconComponent: () => <TableRowAssetIcon name="SendMessageIcon" />,
title: () => 'Loader',
usePredicate: () => Boolean(bridgeInfo),
},
() => `${bridgeInfo!.name} (${bridgeInfo!.version})`,
)
export default LoaderVersionSetting

View file

@ -4,6 +4,7 @@ import {
} from '@revenge-mod/discord/modules/settings'
import { Setting } from './constants'
import HermesVersionSetting from './definitions/HermesVersionSetting'
import LoaderVersionSetting from './definitions/LoaderVersionSetting'
import ReactNativeVersionSetting from './definitions/ReactNativeVersionSetting'
import ReactVersionSetting from './definitions/ReactVersionSetting'
import ReloadSetting from './definitions/ReloadSetting'
@ -25,6 +26,7 @@ registerSettingsItems({
[Setting.ReactVersion]: ReactVersionSetting,
[Setting.ReactNativeVersion]: ReactNativeVersionSetting,
[Setting.HermesVersion]: HermesVersionSetting,
[Setting.LoaderVersion]: LoaderVersionSetting,
})
registerSettingsSection('REVENGE', {

View file

@ -1,4 +1,3 @@
import { Design } from '@revenge-mod/discord/design'
import { SettingListRenderer } from '@revenge-mod/discord/modules/settings/renderer'
import { Setting } from '../constants'
@ -10,24 +9,11 @@ export default function RevengeSettingScreen() {
label: 'Revenge',
settings: [
Setting.RevengeVersion,
Setting.LoaderVersion,
Setting.RevengeDiscord,
Setting.RevengeSourceRepository,
Setting.RevengeLicense,
],
subLabel: (
<>
<Design.Text variant="text-xs/medium">
You are using the next version of Revenge!
</Design.Text>
<Design.Text
color="text-danger"
variant="text-xs/semibold"
>
This version is experimental and may be
unstable.
</Design.Text>
</>
),
},
{
label: 'Versions',

View file

@ -5,8 +5,9 @@ import {
onModuleFirstRequired,
onModuleInitialized,
} from '@revenge-mod/modules/metro/subscriptions'
import { callBridgeMethod } from '@revenge-mod/modules/native'
import { getErrorStack } from '@revenge-mod/utils/error'
import { BuildEnvironment, FullVersion } from './constants'
import { BuildEnvironment, FullVersion } from '~constants'
const IndexModuleId = 0
@ -49,19 +50,19 @@ onModuleFirstRequired(IndexModuleId, function onIndexRequired() {
}
})
export function onError(e: unknown) {
// TODO(init): Move to use native provided alert function, which will accept a string stack trace
// Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further
const { ClientInfoModule, DeviceModule } =
require('@revenge-mod/discord/native') as typeof import('@revenge-mod/discord/native')
export function onError(error: unknown) {
const stack = getErrorStack(error) ?? String(error)
const Client = ClientInfoModule.getConstants()
const Device = DeviceModule.getConstants()
callBridgeMethod('revenge.alertError', [
stack,
`${FullVersion} (${BuildEnvironment})`,
])
alert(
`Failed to load Revenge (${FullVersion} (${BuildEnvironment}))\n` +
`Discord: ${Client.Version} (${Client.Build})\n` +
`Device: ${Device.deviceManufacturer} ${Device.deviceModel}\n\n` +
getErrorStack(e),
)
nativeLoggingHook(`\u001b[31m${stack}\u001b[0m`, 2)
}
declare module '@revenge-mod/modules/native' {
export interface Methods {
'revenge.alertError': [[error: string, version: string], void]
}
}