From a862852e536324f0c0c09cdc59936ed3e9a4f122 Mon Sep 17 00:00:00 2001 From: plebeius Date: Tue, 10 Mar 2026 12:47:38 +0800 Subject: [PATCH] fix(subplebbit): resolve aliased board metadata in subplebbit selectors --- .../__tests__/use-stable-subplebbit.test.tsx | 90 +++++++++++++++++++ src/hooks/use-stable-subplebbit.ts | 22 ++++- 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 src/hooks/__tests__/use-stable-subplebbit.test.tsx diff --git a/src/hooks/__tests__/use-stable-subplebbit.test.tsx b/src/hooks/__tests__/use-stable-subplebbit.test.tsx new file mode 100644 index 00000000..924e7984 --- /dev/null +++ b/src/hooks/__tests__/use-stable-subplebbit.test.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { createElement } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useStableSubplebbit, useSubplebbitField } from '../use-stable-subplebbit'; + +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; +const act = (React as { act?: (cb: () => void | Promise) => void | Promise }).act as (cb: () => void | Promise) => void | Promise; + +const testState = vi.hoisted(() => ({ + subplebbits: {} as Record, +})); + +vi.mock('@bitsocialnet/bitsocial-react-hooks/dist/stores/subplebbits', () => ({ + default: (selector: (state: { subplebbits: typeof testState.subplebbits }) => unknown) => + selector({ + subplebbits: testState.subplebbits, + }), +})); + +let latestValue: unknown; +let container: HTMLDivElement; +let root: Root; +let renderCount = 0; + +const HookHarness = ({ useValue }: { useValue: () => unknown }) => { + latestValue = useValue(); + return null; +}; + +const renderHookValue = (useValue: () => unknown) => { + act(() => { + root.render(createElement(HookHarness, { key: renderCount++, useValue })); + }); + + return latestValue; +}; + +describe('use-stable-subplebbit', () => { + beforeEach(() => { + latestValue = undefined; + renderCount = 0; + testState.subplebbits = {}; + + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + }); + + it('resolves alias board addresses when the store key uses a different suffix', () => { + testState.subplebbits = { + 'international-sfw.bso': { + address: 'international-sfw.bso', + roles: { + 'plebeius.eth': { role: 'owner' }, + }, + title: '/int/ - International', + }, + }; + + expect(renderHookValue(() => useSubplebbitField('international-sfw.eth', (subplebbit) => subplebbit?.roles))).toEqual({ + 'plebeius.eth': { role: 'owner' }, + }); + + expect(renderHookValue(() => useStableSubplebbit('international-sfw.eth'))).toMatchObject({ + address: 'international-sfw.bso', + title: '/int/ - International', + }); + }); + + it('prefers an exact key match when both exact and alias variants are present', () => { + testState.subplebbits = { + 'business.eth': { + address: 'business.eth', + title: '/biz/ - Exact', + }, + 'business.bso': { + address: 'business.bso', + title: '/biz/ - Alias', + }, + }; + + expect(renderHookValue(() => useSubplebbitField('business.eth', (subplebbit) => subplebbit?.title))).toBe('/biz/ - Exact'); + }); +}); diff --git a/src/hooks/use-stable-subplebbit.ts b/src/hooks/use-stable-subplebbit.ts index 85c5e538..af1c0142 100644 --- a/src/hooks/use-stable-subplebbit.ts +++ b/src/hooks/use-stable-subplebbit.ts @@ -1,4 +1,22 @@ import useSubplebbitsStore from '@bitsocialnet/bitsocial-react-hooks/dist/stores/subplebbits'; +import { normalizeBoardAddress } from './use-directories'; + +const getSubplebbitByAddress = (subplebbits: Record | undefined, subplebbitAddress: string | undefined) => { + if (!subplebbits || !subplebbitAddress) { + return undefined; + } + + const exactMatch = subplebbits[subplebbitAddress]; + if (exactMatch) { + return exactMatch; + } + + const normalizedAddress = normalizeBoardAddress(subplebbitAddress); + return Object.entries(subplebbits).find(([key, subplebbit]) => { + const candidateAddress = typeof subplebbit?.address === 'string' ? subplebbit.address : key; + return normalizeBoardAddress(candidateAddress) === normalizedAddress; + })?.[1]; +}; /** * Shallow compare two objects by keys and values. @@ -45,7 +63,7 @@ const isSubplebbitEqual = (prev: any, next: any): boolean => { */ export const useStableSubplebbit = (subplebbitAddress: string | undefined) => { // Use selector with custom equality to ignore transient state - const subplebbit = useSubplebbitsStore((state) => (subplebbitAddress ? state.subplebbits[subplebbitAddress] : undefined), isSubplebbitEqual); + const subplebbit = useSubplebbitsStore((state) => getSubplebbitByAddress(state.subplebbits, subplebbitAddress), isSubplebbitEqual); return subplebbit; }; @@ -61,7 +79,7 @@ export const useStableSubplebbit = (subplebbitAddress: string | undefined) => { export const useSubplebbitField = (subplebbitAddress: string | undefined, selector: (subplebbit: any) => T): T | undefined => { const field = useSubplebbitsStore( (state) => { - const subplebbit = subplebbitAddress ? state.subplebbits[subplebbitAddress] : undefined; + const subplebbit = getSubplebbitByAddress(state.subplebbits, subplebbitAddress); return subplebbit ? selector(subplebbit) : undefined; }, (prev, next) => prev === next,