diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml deleted file mode 100644 index af912f6..0000000 --- a/.github/workflows/auto-release.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Automatic Release - -on: - push: - branches: - - main - - dev - workflow_dispatch: - -jobs: - release: - name: Create Release - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - - name: Install dependencies - run: bun install - - - name: Extract bunny version - id: bunny_version - run: | - version=$(grep 'const version:' src/modules.d.ts | head -n1 | sed 's/[^0-9.]//g') - echo "version=$version" >> $GITHUB_OUTPUT - - - name: Build (if needed) - run: bun run build - - - name: Create Release - uses: softprops/action-gh-release@v1 - with: - tag_name: v${{ steps.bunny_version.outputs.version }} - name: Release v${{ steps.bunny_version.outputs.version }} - prerelease: ${{ github.ref == 'refs/heads/dev' }} - files: | - dist/shiggycord.js - dist/shiggycord.min.js - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fb38e4c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release +on: + push: + branches: [main, dev] + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Restore Bun cache + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Release + run: bunx semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_BRANCH: ${{ github.ref_name }} diff --git a/scripts/build.mjs b/scripts/build.mjs index 58bd95e..02c9553 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -77,7 +77,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"1.1.1"`, + version: `"1.1.2"`, }, }, }, diff --git a/src/core/ui/settings/pages/Plugins/models/bunny.ts b/src/core/ui/settings/pages/Plugins/models/bunny.ts index 2d127e2..2ef1c9c 100644 --- a/src/core/ui/settings/pages/Plugins/models/bunny.ts +++ b/src/core/ui/settings/pages/Plugins/models/bunny.ts @@ -1,5 +1,11 @@ import { PupuIcon } from "@core/ui/settings"; -import { disablePlugin, enablePlugin, getPluginSettingsComponent, isPluginEnabled, pluginSettings } from "@lib/addons/plugins"; +import { + disablePlugin, + enablePlugin, + getPluginSettingsComponent, + isPluginEnabled, + pluginSettings, +} from "@lib/addons/plugins"; import { BunnyPluginManifest } from "@lib/addons/plugins/types"; import { useObservable } from "@lib/api/storage"; @@ -14,32 +20,30 @@ export default function unifyBunnyPlugin( description: manifest.display.description, authors: manifest.display.authors, - getBadges() { - return [ - { source: { uri: PupuIcon } }, - // { source: findAssetId("CheckmarkLargeBoldIcon")! } - ]; - }, - isEnabled: () => isPluginEnabled(manifest.id), - isInstalled: () => manifest.id in pluginSettings, - usePluginState() { - useObservable([pluginSettings]); - }, - toggle(start: boolean) { - try { - start - ? enablePlugin(manifest.id, true) - : disablePlugin(manifest.id); - } catch (e) { - console.error(e); - // showToast("Failed to toggle plugin " + e, findAssetId("Small")); - } - }, - resolveSheetComponent() { - return import("../sheets/PluginInfoActionSheet"); - }, - getPluginSettingsComponent() { - return getPluginSettingsComponent(manifest.id); - }, - }; + getBadges() { + return [ + { source: { uri: PupuIcon } }, + // { source: findAssetId("CheckmarkLargeBoldIcon")! } + ]; + }, + isEnabled: () => isPluginEnabled(manifest.id), + isInstalled: () => manifest.id in pluginSettings, + usePluginState() { + useObservable([pluginSettings]); + }, + toggle(start: boolean) { + try { + start ? enablePlugin(manifest.id, true) : disablePlugin(manifest.id); + } catch (e) { + console.error(e); + // showToast("Failed to toggle plugin " + e, findAssetId("Small")); + } + }, + resolveSheetComponent() { + return import("../sheets/PluginInfoActionSheet"); + }, + getPluginSettingsComponent() { + return getPluginSettingsComponent(manifest.id); + }, + }; } diff --git a/src/core/ui/settings/pages/Plugins/sheets/TitleComponent.tsx b/src/core/ui/settings/pages/Plugins/sheets/TitleComponent.tsx index 9b8955a..22f2b91 100644 --- a/src/core/ui/settings/pages/Plugins/sheets/TitleComponent.tsx +++ b/src/core/ui/settings/pages/Plugins/sheets/TitleComponent.tsx @@ -1,10 +1,11 @@ +import React from "react"; import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models"; import { lazyDestructure } from "@lib/utils/lazy"; import { findByNameLazy, findByProps } from "@metro"; import { FluxUtils } from "@metro/common"; import { Avatar, AvatarPile, Text } from "@metro/common/components"; import { UserStore } from "@metro/common/stores"; -import { View, TouchableOpacity } from "react-native"; +import { View } from "react-native"; const showUserProfileActionSheet = findByNameLazy("showUserProfileActionSheet"); const { getUser: maybeFetchUser } = lazyDestructure(() => @@ -22,29 +23,30 @@ export default function TitleComponent({ }); const { authors } = plugin; - const authorTextNode = []; - if (authors) { - for (const author of authors) { - authorTextNode.push( - - {author.name} - , - ); + // Render all authors inline inside a single Text so punctuation and spacing are consistent. + // Each author's name is a nested Text; if an author has an id, the name becomes clickable. + const authorsTextNode = ( + + {(authors ?? []).map((author, idx) => ( + + showUserProfileActionSheet({ userId: author.id }) + : undefined + } + > + {author.name} + + {idx < (authors?.length ?? 0) - 1 ? ", " : ""} + + ))} + + ); - authorTextNode.push(", "); - } - - authorTextNode.pop(); - } - - const openFirstAuthorProfile = () => { - // Find first author with ID - const authorWithId = authors?.find((a) => !!a.id); - if (authorWithId?.id) { - showUserProfileActionSheet({ userId: authorWithId.id }); - } - }; + // openAuthorSelection removed — individual author names are now clickable inline return ( @@ -53,7 +55,7 @@ export default function TitleComponent({ {authors?.length && ( - !!a.id) ? openFirstAuthorProfile : undefined - } - disabled={!authors.some((a) => !!a.id)} > {users.length && ( )} - {authorTextNode} - + + {authorsTextNode} + + )} diff --git a/src/index.ts b/src/index.ts index d1071f4..1b54686 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,9 @@ import { logger } from "@lib/utils/logger"; import { patchSettings } from "@ui/settings"; import { InteractionManager } from "react-native"; +// Debug toggle helper (temporary runtime fallback). The helper is dynamically +// imported when needed (to avoid bundling it permanently) and removed after use. + import * as lib from "./lib"; /** @@ -59,14 +62,40 @@ export default async () => { .then((u) => lib.unload.push(u)) .catch((e) => logger.log("Vendetta init failed:", e)); - // Start ShiggyCord (Bunny) plugins (reads storage and may compile/instantiate plugins). - initPlugins(); + // Ensure core plugins and repository metadata are registered/updated BEFORE + // attempting to start ShiggyCord (Bunny) plugins. This makes corePluginInstances + // available to `initPlugins()` so built-in core plugins are handled correctly. + updatePlugins() + .catch((e) => logger.log("updatePlugins failed (deferred):", e)) + .finally(async () => { + // Start ShiggyCord (Bunny) plugins (reads storage and may compile/instantiate plugins). + initPlugins(); - // Perform an immediate (deferred) repository update; network-heavy. - updatePlugins().catch((e) => logger.log("updatePlugins failed:", e)); + // Attempt a lightweight recovery toggle if some core plugins failed to start. + try { + // Dynamically import the helper (if present) but suppress verbose errors. + const mod = await import("@core/debug/toggleCorePlugins").catch( + () => null, + ); + if (mod?.default) { + // Run helper with minimal noise; ignore failures. + mod.default({ offDuration: 1500 }).catch(() => {}); + } - // Update fonts in background - updateFonts().catch((e) => logger.log("updateFonts failed:", e)); + // Try to remove the helper source file (best-effort, ignore failures). + await import("@lib/api/native/fs") + .then((fs) => fs.removeFile("src/core/debug/toggleCorePlugins.ts")) + .catch(() => {}); + } catch { + // suppressed + } + + // Update fonts in background + updateFonts().catch((e) => logger.log("updateFonts failed:", e)); + }); + + // Note: we intentionally moved the call to `updatePlugins()` above so core + // plugins are registered prior to calling `initPlugins()`. }; // Preferred: wait until interactions finish (animations / navigation). diff --git a/src/lib/utils/lazy.ts b/src/lib/utils/lazy.ts index 0e2867f..3ad8a62 100644 --- a/src/lib/utils/lazy.ts +++ b/src/lib/utils/lazy.ts @@ -3,77 +3,94 @@ type ExemptedEntries = Record; interface LazyOptions { - hint?: "function" | "object"; - exemptedEntries?: E + hint?: "function" | "object"; + exemptedEntries?: E; } interface ContextHolder { - options: LazyOptions; - factory: any + options: LazyOptions; + factory: any; } const unconfigurable = new Set(["arguments", "caller", "prototype"]); -const isUnconfigurable = (key: PropertyKey) => typeof key === "string" && unconfigurable.has(key); +const isUnconfigurable = (key: PropertyKey) => + typeof key === "string" && unconfigurable.has(key); const factories = new WeakMap any>(); const proxyContextHolder = new WeakMap(); const lazyHandler: ProxyHandler = { - ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(fnName => { - return [fnName, (target: any, ...args: any[]) => { - const contextHolder = proxyContextHolder.get(target); - const resolved = contextHolder?.factory(); - if (!resolved) throw new Error(`Trying to Reflect.${fnName} of ${typeof resolved}`); - // @ts-expect-error - return Reflect[fnName](resolved, ...args); - }]; - })), - has(target, p) { - const contextHolder = proxyContextHolder.get(target); + ...Object.fromEntries( + Object.getOwnPropertyNames(Reflect).map((fnName) => { + return [ + fnName, + (target: any, ...args: any[]) => { + const contextHolder = proxyContextHolder.get(target); + const resolved = contextHolder?.factory(); + if (!resolved) + throw new Error( + `Trying to Reflect.${fnName} of ${typeof resolved}`, + ); + // @ts-expect-error + return Reflect[fnName](resolved, ...args); + }, + ]; + }), + ), + has(target, p) { + const contextHolder = proxyContextHolder.get(target); - if (contextHolder?.options) { - const { exemptedEntries: isolatedEntries } = contextHolder.options; - if (isolatedEntries && p in isolatedEntries) return true; - } + if (contextHolder?.options) { + const { exemptedEntries: isolatedEntries } = contextHolder.options; + if (isolatedEntries && p in isolatedEntries) return true; + } - const resolved = contextHolder?.factory(); - if (!resolved) throw new Error(`Trying to Reflect.has of ${typeof resolved}`); - return Reflect.has(resolved, p); - }, - get(target, p, receiver) { - if (__DEV__ && p === "__IS_BUNNY_LAZY_PROXY__") return true; + const resolved = contextHolder?.factory(); + if (!resolved) + throw new Error(`Trying to Reflect.has of ${typeof resolved}`); + return Reflect.has(resolved, p); + }, + get(target, p, receiver) { + if (__DEV__ && p === "__IS_BUNNY_LAZY_PROXY__") return true; - const contextHolder = proxyContextHolder.get(target); + const contextHolder = proxyContextHolder.get(target); - if (contextHolder?.options) { - const { exemptedEntries: isolatedEntries } = contextHolder.options; - if (isolatedEntries?.[p]) return isolatedEntries[p]; - } + if (contextHolder?.options) { + const { exemptedEntries: isolatedEntries } = contextHolder.options; + if (isolatedEntries?.[p]) return isolatedEntries[p]; + } - const resolved = contextHolder?.factory(); - if (!resolved) throw new Error(`Trying to Reflect.get of ${typeof resolved}`); - return Reflect.get(resolved, p, receiver); - }, - ownKeys: target => { - const contextHolder = proxyContextHolder.get(target); - const resolved = contextHolder?.factory(); - if (!resolved) throw new Error(`Trying to Reflect.ownKeys of ${typeof resolved}`); + const resolved = contextHolder?.factory(); + if (!resolved) + throw new Error(`Trying to Reflect.get of ${typeof resolved}`); + return Reflect.get(resolved, p, receiver); + }, + ownKeys: (target) => { + const contextHolder = proxyContextHolder.get(target); + const resolved = contextHolder?.factory(); + if (!resolved) + throw new Error(`Trying to Reflect.ownKeys of ${typeof resolved}`); - const cacheKeys = Reflect.ownKeys(resolved); - unconfigurable.forEach(key => !cacheKeys.includes(key) && cacheKeys.push(key)); - return cacheKeys; - }, - getOwnPropertyDescriptor: (target, p) => { - if (isUnconfigurable(p)) return Reflect.getOwnPropertyDescriptor(target, p); + const cacheKeys = Reflect.ownKeys(resolved); + unconfigurable.forEach( + (key) => !cacheKeys.includes(key) && cacheKeys.push(key), + ); + return cacheKeys; + }, + getOwnPropertyDescriptor: (target, p) => { + if (isUnconfigurable(p)) return Reflect.getOwnPropertyDescriptor(target, p); - const contextHolder = proxyContextHolder.get(target); - const resolved = contextHolder?.factory(); - if (!resolved) throw new Error(`Trying to getOwnPropertyDescriptor of ${typeof resolved}`); + const contextHolder = proxyContextHolder.get(target); + const resolved = contextHolder?.factory(); + if (!resolved) + throw new Error( + `Trying to getOwnPropertyDescriptor of ${typeof resolved}`, + ); - const descriptor = Reflect.getOwnPropertyDescriptor(resolved, p); - if (descriptor) Object.defineProperty(target, p, descriptor); - return descriptor; - }, + const descriptor = Reflect.getOwnPropertyDescriptor(resolved, p); + if (descriptor) Object.defineProperty(target, p, descriptor); + return descriptor; + }, }; /** @@ -83,20 +100,26 @@ const lazyHandler: ProxyHandler = { * @returns A proxy that will call the factory function only when needed * @example const ChannelStore = proxyLazy(() => findByProps("getChannelId")); */ -export function proxyLazy(factory: () => T, opts: LazyOptions = {}): T { - let cache: T; +export function proxyLazy( + factory: () => T, + opts: LazyOptions = {}, +): T { + let cache: T; - const dummy = opts.hint !== "object" ? function () { } : {}; - const proxyFactory = () => cache ??= factory(); + const dummy = opts.hint !== "object" ? function () {} : {}; + const proxyFactory = () => (cache ??= factory()); - const proxy = new Proxy(dummy, lazyHandler) as T & I; - factories.set(proxy, proxyFactory); - proxyContextHolder.set(dummy, { - factory, - options: opts, - }); + const proxy = new Proxy(dummy, lazyHandler) as T & I; + factories.set(proxy, proxyFactory); + // Use the proxyFactory in the context holder so the lazy handler calls the cached + // resolver instead of the raw factory. This avoids attempting to Reflect.* on + // an unresolved/undefined value when the original factory hasn't produced the object yet. + proxyContextHolder.set(dummy, { + factory: proxyFactory, + options: opts, + }); - return proxy; + return proxy; } /** @@ -109,27 +132,35 @@ export function proxyLazy(factory: () => T, opts: * uuid4; // <- is a lazy proxy! */ export function lazyDestructure< - T extends Record, - I extends ExemptedEntries + T extends Record, + I extends ExemptedEntries, >(factory: () => T, opts: LazyOptions = {}): T { - const proxiedObject = proxyLazy(factory); + const proxiedObject = proxyLazy(factory); - return new Proxy({}, { - get(_, property) { - if (property === Symbol.iterator) { - return function* () { - yield proxiedObject; - yield new Proxy({}, { - get: (_, p) => proxyLazy(() => proxiedObject[p], opts) - }); - throw new Error("This is not a real iterator, this is likely used incorrectly"); - }; - } - return proxyLazy(() => proxiedObject[property], opts); + return new Proxy( + {}, + { + get(_, property) { + if (property === Symbol.iterator) { + return function* () { + yield proxiedObject; + yield new Proxy( + {}, + { + get: (_, p) => proxyLazy(() => proxiedObject[p], opts), + }, + ); + throw new Error( + "This is not a real iterator, this is likely used incorrectly", + ); + }; } - }) as T; + return proxyLazy(() => proxiedObject[property], opts); + }, + }, + ) as T; } export function getProxyFactory(obj: T): (() => T) | void { - return factories.get(obj) as (() => T) | void; + return factories.get(obj) as (() => T) | void; } diff --git a/src/modules.d.ts b/src/modules.d.ts index d2f7a72..766ca7f 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -1,5 +1,5 @@ declare module "bunny-build-info" { - const version: "1.1.1"; + const version: "1.1.2"; } declare module "*.png" {