chore: separate vd plugins from bunny

This commit is contained in:
amsyarasyiq 2024-07-20 11:23:00 +08:00
parent 06a852639f
commit a8d2ddd0df
12 changed files with 279 additions and 98 deletions

View file

@ -1,6 +1,6 @@
import { Strings } from "@core/i18n";
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
import { ApplicationCommand, ApplicationCommandOptionType } from "@lib/api/commands/types";
import { BunnyPlugin, sourceStore as pluginStorage } from "@lib/managers/plugins";
import { messageUtil } from "@metro/common";
export default () => <ApplicationCommand>{
@ -15,7 +15,7 @@ export default () => <ApplicationCommand>{
}
],
execute([ephemeral], ctx) {
const plugins = Object.values(pluginStorage).filter(Boolean) as unknown as BunnyPlugin[];
const plugins = Object.values(VdPluginManager.plugins).filter(Boolean) as unknown as VendettaPlugin[];
plugins.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
const enabled = plugins.filter(p => p.enabled).map(p => p.manifest.name);

View file

@ -1,9 +1,9 @@
import { Strings } from "@core/i18n";
import { VdPluginManager } from "@core/vendetta/plugins";
import { findAssetId } from "@lib/api/assets";
import { isThemeSupported } from "@lib/api/native/loader";
import { after } from "@lib/api/patcher";
import { useProxy } from "@lib/api/storage";
import { installPlugin, removePlugin,sourceStore } from "@lib/managers/plugins";
import { installTheme, removeTheme, themes } from "@lib/managers/themes";
import { DISCORD_SERVER_ID, HTTP_REGEX_MULTI, PLUGINS_CHANNEL_ID, THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
@ -21,11 +21,11 @@ const forumReactions = findByPropsLazy("MostCommonForumPostReaction");
const postMap = {
Plugin: {
storage: sourceStore,
storage: VdPluginManager.plugins,
urlsFilter: (url: string) => url.startsWith(VD_PROXY_PREFIX),
installOrRemove: (url: string) => {
const isInstalled = postMap.Plugin.storage[url];
return isInstalled ? removePlugin(url) : installPlugin(url);
return isInstalled ? VdPluginManager.removePlugin(url) : VdPluginManager.installPlugin(url);
}
},
Theme: {
@ -63,7 +63,7 @@ function useExtractThreadContent(thread: any, _firstMessage = null, actionSheet
function useInstaller(thread: any, firstMessage = null, actionSheet = false): [true] | [false, PostType, boolean, boolean, () => Promise<void>] {
const [postType, url] = useExtractThreadContent(thread, firstMessage, actionSheet) ?? [];
useProxy(sourceStore);
useProxy(VdPluginManager.plugins);
useProxy(themes);
const [isInstalling, setIsInstalling] = React.useState(false);

View file

@ -1,8 +1,8 @@
import { formatString, Strings } from "@core/i18n";
import { VdPluginManager } from "@core/vendetta/plugins";
import { findAssetId } from "@lib/api/assets";
import { isThemeSupported } from "@lib/api/native/loader";
import { after, instead } from "@lib/api/patcher";
import { installPlugin } from "@lib/managers/plugins";
import { installTheme } from "@lib/managers/themes";
import { THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
@ -28,7 +28,7 @@ function typeFromUrl(url: string) {
}
function installWithToast(type: "plugin" | "theme", url: string) {
(type === "plugin" ? installPlugin : installTheme)(url)
(type === "plugin" ? VdPluginManager.installPlugin.bind(VdPluginManager) : installTheme)(url)
.then(() => {
showToast(Strings.SUCCESSFULLY_INSTALLED, findAssetId("Check"));
})

View file

@ -1,11 +1,11 @@
import { CardWrapper } from "@core/ui/components/AddonCard";
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
import { findAssetId } from "@lib/api/assets";
import { useProxy } from "@lib/api/storage";
import { BunnyPlugin, getSettingsComponent, startPlugin, stopPlugin } from "@lib/managers/plugins";
import { showSheet } from "@lib/ui/sheets";
import { lazyDestructure } from "@lib/utils/lazy";
import { findByProps } from "@metro";
import { NavigationNative, tokens } from "@metro/common";
import { NavigationNative } from "@metro/common";
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
import { createContext, memo, useContext } from "react";
import { Image, View } from "react-native";
@ -13,7 +13,7 @@ import { Image, View } from "react-native";
import { usePluginCardStyles } from "./usePluginCardStyles";
const { useToken } = lazyDestructure(() => findByProps("useToken"));
const PluginContext = createContext<BunnyPlugin>(null!);
const PluginContext = createContext<VendettaPlugin>(null!);
const usePlugin = () => useContext(PluginContext);
function Authors() {
@ -65,25 +65,25 @@ function Title() {
// TODO: Wrap in a Card-ish component with red bg
// TODO: Allow glacing at the error's stack
function Status() {
const plugin = usePlugin();
const styles = usePluginCardStyles();
const INTERACTIVE_NORMAL = useToken(tokens.colors.INTERACTIVE_NORMAL);
// function Status() {
// const plugin = usePlugin();
// const styles = usePluginCardStyles();
// const INTERACTIVE_NORMAL = useToken(tokens.colors.INTERACTIVE_NORMAL);
if (!plugin.error) return null;
// if (!plugin.error) return null;
return <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<View style={styles.smallIcon}>
<Image
tintColor={INTERACTIVE_NORMAL}
source={findAssetId("WarningIcon")}
/>
</View>
<Text variant="text-sm/semibold">
There was an error while attempting to start this plugin.
</Text>
</View>;
}
// return <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
// <View style={styles.smallIcon}>
// <Image
// tintColor={INTERACTIVE_NORMAL}
// source={findAssetId("WarningIcon")}
// />
// </View>
// <Text variant="text-sm/semibold">
// There was an error while attempting to start this plugin.
// </Text>
// </View>;
// }
const Actions = memo(() => {
const plugin = usePlugin();
@ -94,10 +94,10 @@ const Actions = memo(() => {
size="sm"
variant="secondary"
icon={findAssetId("WrenchIcon")}
disabled={!getSettingsComponent(plugin.id)}
disabled={!VdPluginManager.getSettings(plugin.id)}
onPress={() => navigation.push("VendettaCustomPage", {
title: plugin.manifest.name,
render: getSettingsComponent(plugin.id),
render: VdPluginManager.getSettings(plugin.id),
})}
/>
<IconButton
@ -113,14 +113,14 @@ const Actions = memo(() => {
</View>;
});
export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
export default function PluginCard({ item: plugin }: CardWrapper<VendettaPlugin>) {
useProxy(plugin);
return (
<PluginContext.Provider value={plugin}>
<Card>
<Stack spacing={16}>
<Status />
{/* <Status /> */}
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<View style={{ flexShrink: 1 }}>
<Title />
@ -132,8 +132,8 @@ export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
<TableSwitch
value={plugin.enabled}
onValueChange={(v: boolean) => {
if (v) startPlugin(plugin.id);
else stopPlugin(plugin.id);
if (v) VdPluginManager.startPlugin(plugin.id);
else VdPluginManager.stopPlugin(plugin.id);
}}
/>
</Stack>

View file

@ -1,8 +1,8 @@
import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import PluginCard from "@core/ui/settings/pages/Plugins/PluginCard";
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
import { useProxy } from "@lib/api/storage";
import { BunnyPlugin, installPlugin, preferredSourceStore, sourceStore } from "@lib/managers/plugins";
import { settings } from "@lib/settings";
import { Author } from "@lib/utils/types";
@ -10,16 +10,15 @@ export default function Plugins() {
useProxy(settings);
return (
<AddonPage<BunnyPlugin>
<AddonPage<VendettaPlugin>
title={Strings.PLUGINS}
searchKeywords={[
"manifest.name",
"manifest.description",
p => p.manifest.authors?.map((a: Author) => a.name).join()
]}
items={preferredSourceStore}
resolveItem={source => sourceStore[source]}
fetchFunction={installPlugin}
items={VdPluginManager.plugins}
fetchFunction={VdPluginManager.installPlugin.bind(VdPluginManager)}
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}
card={PluginCard}
/>

View file

@ -1,7 +1,7 @@
import { formatString, Strings } from "@core/i18n";
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
import { findAssetId } from "@lib/api/assets";
import { purgeStorage, useProxy } from "@lib/api/storage";
import { BunnyPlugin, fetchAndStorePlugin, getSettingsComponent, removePlugin, startPlugin, stopPlugin } from "@lib/managers/plugins";
import { showConfirmationAlert } from "@lib/ui/alerts";
import { hideSheet } from "@lib/ui/sheets";
import { showToast } from "@lib/ui/toasts";
@ -11,14 +11,14 @@ import { ActionSheet, ActionSheetRow, Button, TableRow, Text } from "@metro/comm
import { ScrollView, View } from "react-native";
interface InfoProps {
plugin: BunnyPlugin;
plugin: VendettaPlugin;
navigation: any;
}
export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps) {
useProxy(plugin);
const Settings = getSettingsComponent(plugin.id);
const Settings = VdPluginManager.getSettings(plugin.id);
return <ActionSheet>
<ScrollView>
@ -47,16 +47,16 @@ export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps)
label={Strings.REFETCH}
icon={<TableRow.Icon source={findAssetId("RetryIcon")} />}
onPress={async () => {
if (plugin.enabled) stopPlugin(plugin.id, false);
if (plugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
try {
await fetchAndStorePlugin(plugin.id);
await VdPluginManager.fetchPlugin(plugin.id);
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, findAssetId("toast_image_saved"));
} catch {
showToast(Strings.PLUGIN_REFETCH_FAILED, findAssetId("Small"));
}
if (plugin.enabled) await startPlugin(plugin.id);
if (plugin.enabled) await VdPluginManager.startPlugin(plugin.id);
hideSheet("PluginInfoActionSheet");
}}
/>
@ -89,10 +89,10 @@ export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps)
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
onConfirm: async () => {
if (plugin.enabled) stopPlugin(plugin.id, false);
if (plugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
try {
await fetchAndStorePlugin(plugin.id);
await VdPluginManager.fetchPlugin(plugin.id);
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, findAssetId("toast_image_saved"));
} catch {
showToast(Strings.PLUGIN_REFETCH_FAILED, findAssetId("Small"));
@ -111,7 +111,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps)
findAssetId(message[1])
);
if (plugin.enabled) await startPlugin(plugin.id);
if (plugin.enabled) await VdPluginManager.startPlugin(plugin.id);
hideSheet("PluginInfoActionSheet");
}
})}
@ -127,7 +127,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps)
confirmColor: ButtonColors.RED,
onConfirm: () => {
try {
removePlugin(plugin.id);
VdPluginManager.removePlugin(plugin.id);
} catch (e) {
showToast(String(e), findAssetId("Small"));
}

View file

@ -5,10 +5,10 @@ import patcher from "@lib/api/patcher";
import * as storage from "@lib/api/storage";
import { createStorage } from "@lib/api/storage";
import * as debug from "@lib/debug";
import * as plugins from "@lib/managers/plugins";
import * as themes from "@lib/managers/themes";
import { loaderConfig, settings } from "@lib/settings";
import * as utils from "@lib/utils";
import { cyrb64Hash } from "@lib/utils/cyrb64";
import { DiscordLogger } from "@lib/utils/logger";
import * as metro from "@metro";
import * as common from "@metro/common";
@ -23,7 +23,9 @@ import { omit } from "es-toolkit";
import { createElement, useEffect } from "react";
import { View } from "react-native";
export async function createVdPluginObject(plugin: plugins.BunnyPlugin) {
import { VdPluginManager, VendettaPlugin } from "./plugins";
export async function createVdPluginObject(plugin: VendettaPlugin) {
return {
...window.vendetta,
plugin: {
@ -171,13 +173,13 @@ export const initVendettaObject = (): any => {
rawColors: color.rawColors
},
plugins: {
plugins: plugins.sourceStore,
fetchPlugin: (source: string) => plugins.fetchAndStorePlugin(source),
installPlugin: (source: string, enabled = true) => plugins.installPlugin(source, enabled),
startPlugin: (id: string) => plugins.startPlugin(plugins.sourceStore[id]!.id),
stopPlugin: (id: string, disable = true) => plugins.stopPlugin(plugins.sourceStore[id]!.id, disable),
removePlugin: (id: string) => plugins.removePlugin(plugins.sourceStore[id]!.id),
getSettings: (id: string) => plugins.getSettingsComponent(plugins.sourceStore[id]!.id)
plugins: VdPluginManager.plugins,
fetchPlugin: (source: string) => VdPluginManager.fetchPlugin(source),
installPlugin: (source: string, enabled = true) => VdPluginManager.installPlugin(source, enabled),
startPlugin: (id: string) => VdPluginManager.startPlugin(id),
stopPlugin: (id: string, disable = true) => VdPluginManager.stopPlugin(id, disable),
removePlugin: (id: string) => VdPluginManager.removePlugin(id),
getSettings: (id: string) => VdPluginManager.getSettings(id)
},
themes: {
themes: themes.themes,
@ -229,32 +231,3 @@ export const initVendettaObject = (): any => {
return () => api.unload();
};
// cyrb53 (c) 2018 bryc (github.com/bryc). License: Public domain. Attribution appreciated.
// A fast and simple 64-bit (or 53-bit) string hash function with decent collision resistance.
// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
const cyrb64 = (str: string, seed = 0) => {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
// For a single 53-bit numeric return value we could return
// 4294967296 * (2097151 & h2) + (h1 >>> 0);
// but we instead return the full 64-bit value:
return [h2 >>> 0, h1 >>> 0];
};
// An improved, *insecure* 64-bit hash that's short, fast, and has no dependencies.
// Output is always 14 characters.
const cyrb64Hash = (str: string, seed = 0) => {
const [h2, h1] = cyrb64(str, seed);
return h2.toString(36).padStart(7, "0") + h1.toString(36).padStart(7, "0");
};

View file

@ -0,0 +1,184 @@
import { allSettled } from "@core/polyfills/allSettled";
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
import { settings } from "@lib/settings";
import { safeFetch } from "@lib/utils";
import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { DiscordLogger, logger } from "@lib/utils/logger";
import { Author } from "@lib/utils/types";
type EvaledPlugin = {
onLoad?(): void;
onUnload(): void;
settings: JSX.Element;
};
// See https://github.com/vendetta-mod/polymanifest
interface PluginManifest {
name: string;
description: string;
authors: Author[];
main: string;
hash: string;
// Vendor-specific field, contains our own data
vendetta?: {
icon?: string;
};
}
export interface VendettaPlugin {
id: string;
manifest: PluginManifest;
enabled: boolean;
update: boolean;
js: string;
}
const plugins = wrapSync(createStorage<Record<string, VendettaPlugin>>(createMMKVBackend("VENDETTA_PLUGINS")));
const pluginInstance: Record<string, EvaledPlugin> = {};
export const VdPluginManager = {
plugins,
async pluginFetch(url: string) {
if (url.startsWith(VD_PROXY_PREFIX)) {
url = url.replace(VD_PROXY_PREFIX, BUNNY_PROXY_PREFIX);
}
return await safeFetch(url, { cache: "no-store" });
},
async fetchPlugin(id: string) {
if (!id.endsWith("/")) id += "/";
const existingPlugin = plugins[id];
let pluginManifest: PluginManifest;
try {
pluginManifest = await (await this.pluginFetch(id + "manifest.json")).json();
} catch {
throw new Error(`Failed to fetch manifest for ${id}`);
}
let pluginJs: string | undefined;
if (existingPlugin?.manifest.hash !== pluginManifest.hash) {
try {
// by polymanifest spec, plugins should always specify their main file, but just in case
pluginJs = await (await this.pluginFetch(id + (pluginManifest.main || "index.js"))).text();
} catch { } // Empty catch, checked below
}
if (!pluginJs && !existingPlugin) throw new Error(`Failed to fetch JS for ${id}`);
plugins[id] = {
id: id,
manifest: pluginManifest,
enabled: existingPlugin?.enabled ?? false,
update: existingPlugin?.update ?? true,
js: pluginJs ?? existingPlugin.js,
};
},
async installPlugin(id: string, enabled = true) {
if (!id.endsWith("/")) id += "/";
if (typeof id !== "string" || id in plugins) throw new Error("Plugin already installed");
await this.fetchPlugin(id);
if (enabled) await this.startPlugin(id);
},
/**
* @internal
*/
async evalPlugin(plugin: VendettaPlugin) {
const vendettaForPlugins = {
...window.vendetta,
plugin: {
id: plugin.id,
manifest: plugin.manifest,
// Wrapping this with wrapSync is NOT an option.
storage: await createStorage<Record<string, any>>(createMMKVBackend(plugin.id)),
},
logger: new DiscordLogger(`Bunny » ${plugin.manifest.name}`),
};
const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}`;
const raw = (0, eval)(pluginString)(vendettaForPlugins);
const ret = typeof raw === "function" ? raw() : raw;
return ret?.default ?? ret ?? {};
},
async startPlugin(id: string) {
if (!id.endsWith("/")) id += "/";
const plugin = plugins[id];
if (!plugin) throw new Error("Attempted to start non-existent plugin");
try {
if (!settings.safeMode?.enabled) {
const pluginRet: EvaledPlugin = await this.evalPlugin(plugin);
pluginInstance[id] = pluginRet;
pluginRet.onLoad?.();
}
plugin.enabled = true;
} catch (e) {
logger.error(`Plugin ${plugin.id} errored whilst loading, and will be unloaded`, e);
try {
pluginInstance[plugin.id]?.onUnload?.();
} catch (e2) {
logger.error(`Plugin ${plugin.id} errored whilst unloading`, e2);
}
delete pluginInstance[id];
plugin.enabled = false;
}
},
stopPlugin(id: string, disable = true) {
if (!id.endsWith("/")) id += "/";
const plugin = plugins[id];
const pluginRet = pluginInstance[id];
if (!plugin) throw new Error("Attempted to stop non-existent plugin");
if (!settings.safeMode?.enabled) {
try {
pluginRet?.onUnload?.();
} catch (e) {
logger.error(`Plugin ${plugin.id} errored whilst unloading`, e);
}
delete pluginInstance[id];
}
disable && (plugin.enabled = false);
},
async removePlugin(id: string) {
if (!id.endsWith("/")) id += "/";
const plugin = plugins[id];
if (plugin.enabled) this.stopPlugin(id);
delete plugins[id];
await purgeStorage(id);
},
/**
* @internal
*/
async initPlugins() {
await awaitStorage(settings, plugins);
const allIds = Object.keys(plugins);
if (!settings.safeMode?.enabled) {
// Loop over any plugin that is enabled, update it if allowed, then start it.
await allSettled(allIds.filter(pl => plugins[pl].enabled).map(async pl => (plugins[pl].update && await this.fetchPlugin(pl).catch((e: Error) => logger.error(e.message)), await this.startPlugin(pl))));
// Wait for the above to finish, then update all disabled plugins that are allowed to.
allIds.filter(pl => !plugins[pl].enabled && plugins[pl].update).forEach(pl => this.fetchPlugin(pl));
}
return () => this.stopAllPlugins();
},
stopAllPlugins() {
return Object.keys(pluginInstance).forEach(p => this.stopPlugin(p, false));
},
getSettings: (id: string) => pluginInstance[id]?.settings
};

View file

@ -1,8 +1,9 @@
import initFixes from "@core/fixes";
import { initFetchI18nStrings } from "@core/i18n";
import { initCorePlugins } from "@core/plugins";
import { initVendettaObject } from "@core/polyfills/vendettaObject";
import initSettings from "@core/ui/settings";
import { initVendettaObject } from "@core/vendetta/api";
import { VdPluginManager } from "@core/vendetta/plugins";
import { patchCommands } from "@lib/api/commands";
import { injectFluxInterceptor } from "@lib/api/flux";
import { removeFile, writeFile } from "@lib/api/native/fs";
@ -10,7 +11,6 @@ import { isPyonLoader, isThemeSupported } from "@lib/api/native/loader";
import { FileManager } from "@lib/api/native/modules";
import { patchLogHook } from "@lib/debug";
import { updateFonts } from "@lib/managers/fonts";
import { initPlugins } from "@lib/managers/plugins";
import { initThemes, patchChatBackground } from "@lib/managers/themes";
import { logger } from "@lib/utils/logger";
import initSafeMode from "@ui/safeMode";
@ -56,10 +56,10 @@ export default async () => {
// Assign window object
window.bunny = lib;
// Once done, load plugins
initPlugins()
// Once done, load Vendetta plugins
VdPluginManager.initPlugins()
.then(u => lib.unload.push(u))
.catch(() => alert("Failed to initialize Bunny plugins"));
.catch(() => alert("Failed to initialize Vendetta plugins"));
// Update the fonts
updateFonts();

View file

@ -1,5 +1,5 @@
import { allSettled } from "@core/polyfills/allSettled";
import { createVdPluginObject } from "@core/polyfills/vendettaObject";
import { createVdPluginObject } from "@core/vendetta/api";
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
import { settings } from "@lib/settings";
import { safeFetch } from "@lib/utils";

View file

@ -3,7 +3,6 @@ import { findAssetId } from "@lib/api/assets";
import { lazyDestructure } from "@lib/utils/lazy";
import { toasts } from "@metro/common";
import { findByProps } from "@metro/utils";
import { Platform } from "react-native";
const { uuid4 } = lazyDestructure(() => findByProps("uuid4"));
@ -17,8 +16,5 @@ export const showToast = (content: string, asset?: number) => toasts.open({
});
showToast.showCopyToClipboard = (message = Strings.COPIED_TO_CLIPBOARD) => {
// On Android, only show toast for 12 and lower
if (Platform.OS !== "android" || Platform.Version <= 32) {
showToast(message, findAssetId("toast_copy_link"));
}
showToast(message, findAssetId("toast_copy_link"));
};

29
src/lib/utils/cyrb64.ts Normal file
View file

@ -0,0 +1,29 @@
// cyrb53 (c) 2018 bryc (github.com/bryc). License: Public domain. Attribution appreciated.
// A fast and simple 64-bit (or 53-bit) string hash function with decent collision resistance.
// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
export function cyrb64(str: string, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
// For a single 53-bit numeric return value we could return
// 4294967296 * (2097151 & h2) + (h1 >>> 0);
// but we instead return the full 64-bit value:
return [h2 >>> 0, h1 >>> 0];
}
// An improved, *insecure* 64-bit hash that's short, fast, and has no dependencies.
// Output is always 14 characters.
export function cyrb64Hash(str: string, seed = 0) {
const [h2, h1] = cyrb64(str, seed);
return h2.toString(36).padStart(7, "0") + h1.toString(36).padStart(7, "0");
}