pull chatbubbles plugin support from kettu

This commit is contained in:
kmmiio99o 2025-11-07 21:43:00 +01:00
parent ebfe35846f
commit c9223d44f3
6 changed files with 238 additions and 1 deletions

View file

@ -77,7 +77,7 @@ const config = {
constModules: { constModules: {
globals: { globals: {
"bunny-build-info": { "bunny-build-info": {
version: `"1.1.10"`, version: `"1.1.10-1"`,
}, },
}, },
}, },

View file

@ -0,0 +1,82 @@
import { ImageLoader } from "./";
import type { LoaderPayload } from "./types";
type InteropReturn = Promise<{ ret: string } | { err: string } | { cancelled: true; reason: string }>;
export const loaderPayload = window.__PYON_LOADER__ as LoaderPayload;
export function isModuleRegistered(module: string) {
return loaderPayload.loader.modules[module] !== undefined;
}
export function isFunctionRegistered(module: string, function_: string) {
return loaderPayload.loader.modules[module]?.functions[function_] !== undefined;
}
export async function callFunction(module: string, functionName: string, args: unknown[]) {
if (!isFunctionRegistered(module, functionName)) {
throw new Error(`Function ${module}.${functionName} is not registered`);
}
const promise: InteropReturn = ImageLoader.queryCache([
"__pyon_bridge",
JSON.stringify({ m: module, f: functionName, a: args }),
]);
const result = await promise;
if ("ret" in result) {
return JSON.parse(result.ret);
}
if ("cancelled" in result) {
throw new Error(`Function ${module}.${functionName} was cancelled: ${result.reason}`);
}
if ("err" in result) {
throw new Error(result.err);
}
}
export interface LoaderModule {
hasFunction(functionName: string): boolean;
callFunction(functionName: string, args: unknown[]): Promise<unknown>;
}
type ModuleProps<T extends LoaderModule> = {
name: string;
argumentProcessors?: Partial<{ [key in keyof T]: (args: any[]) => any[] }>;
};
export function getModule<T extends LoaderModule>({ name, argumentProcessors: processArgs }: ModuleProps<T>) {
const module = loaderPayload.loader.modules[name];
if (!module) {
throw new Error(`Module ${name} is not registered`);
}
const baseObject: Record<string, any> = {
hasFunction: (functionName: string) => isFunctionRegistered(name, functionName),
callFunction: (functionName: string, args: unknown[]) => callFunction(name, functionName, args),
};
for (const [functionName] of Object.entries(module.functions)) {
baseObject[functionName] = (...args: unknown[]) =>
callFunction(
name,
functionName,
// @ts-expect-error
processArgs && functionName in processArgs ? processArgs[functionName](args) : args,
);
}
return baseObject as T;
}
/**
* Determines if safe mode is currently enabled. The result remains consistent over time.
* This will not change until the app is restarted. For draft changes, use the InitConfigStore.
* @returns True if safe mode is enabled for the current instance, otherwise false.
*/
export function isSafeModeEnabled() {
return Boolean(loaderPayload.loader.initConfig.safeMode);
}

View file

@ -0,0 +1,16 @@
import { processColor } from "react-native";
import { getModule } from "./loader";
import type { LoaderModule } from "./loader";
interface BubbleModule extends LoaderModule {
hookBubbles(): Promise<void>;
unhookBubbles(): Promise<void>;
configure(avatarRadius?: number, bubbleRadius?: number, bubbleColor?: string): Promise<void>;
}
export default getModule<BubbleModule>({
name: "BubbleModule",
argumentProcessors: {
configure: args => [args[0], args[1], Number(processColor(args[2]))],
},
});

View file

@ -33,3 +33,6 @@ export const NativeThemeModule = getNativeModule(
export const BundleUpdaterManager = getNativeModule( export const BundleUpdaterManager = getNativeModule(
"BundleUpdaterManager" "BundleUpdaterManager"
)!; )!;
export const ImageLoader = getNativeModule(
"ImageLoader"
)!;

View file

@ -134,3 +134,42 @@ export namespace RNModules {
} }
} }
} }
export interface InitConfig {
baseUrl: string;
forceUpdate: boolean;
skipUpdate: boolean;
bundlePath: string | null;
safeMode: boolean;
}
interface LoaderConstants {
WINTRY_DIR: string;
DEFAULT_BASE_URL: string;
}
interface LoaderModule {
functions: {
[functionName: string]: number;
};
constants: {
[constantName: string]: any;
};
}
interface LoaderModules {
[moduleName: string]: LoaderModule;
}
export interface LoaderPayload {
bundle: {
revision: string;
};
loader: {
name: string;
version: string;
initConfig: InitConfig;
constants: LoaderConstants;
modules: LoaderModules;
};
}

View file

@ -264,3 +264,100 @@ export function* getCachedPolyfillModules(name: string) {
} }
} }
} }
export interface WaitForOptions {
count?: number;
}
export type ModuleFilter<T = any> = {
(exports: any): T | undefined;
key?: string;
}
export function waitFor<T = any>(
filter: ModuleFilter<T>,
callback: (exports: T, id: Metro.ModuleID) => void,
options: WaitForOptions = {}
): () => void {
const { count = 1 } = options;
let currentCount = 0;
const unsubscribers: Array<() => void> = [];
let isActive = true;
const cleanup = () => {
if (!isActive) return;
isActive = false;
unsubscribers.forEach(unsub => unsub());
unsubscribers.length = 0;
};
function checkModule(id: Metro.ModuleID): boolean {
if (!isActive) return true;
const exports = requireModule(id);
if (isBadExports(exports)) return false;
const result = filter(exports);
if (!result) return false;
callback(result, id);
if (++currentCount >= count) {
cleanup();
return true;
}
return false;
}
if (filter.key) {
const cache = getMetroCache().findIndex[filter.key];
if (cache) {
for (const id in cache) {
if (id[0] === "_") continue;
const numId = Number(id);
if (metroModules[numId]?.isInitialized) {
if (checkModule(numId)) return cleanup;
} else {
const unsub = subscribeModule(numId, () => {
checkModule(numId);
});
unsubscribers.push(unsub);
}
}
}
}
for (const id in metroModules) {
if (!isActive) break;
const numId = Number(id);
if (metroModules[numId]?.isInitialized && !metroModules[numId]?.hasError) {
if (checkModule(numId)) return cleanup;
}
}
if (isActive) {
for (const id in metroModules) {
const numId = Number(id);
if (!metroModules[numId]?.isInitialized) {
const unsub = subscribeModule(numId, () => {
checkModule(numId);
});
unsubscribers.push(unsub);
}
}
}
return cleanup;
}
export function waitForModule<T = any>(
filter: ModuleFilter<T>,
options: WaitForOptions = {}
): Promise<T> {
return new Promise((resolve) => {
waitFor(filter, (exports) => resolve(exports), options);
});
}