Files
eranos/src/hooks/useMembersOnlyFilter.ts
T
lemon bd1b1bd056 Tighten flat community primitives
Extract isAuthorizedAward helper as the single source of truth for
membership award validation, used by both resolveMembership and
useMyCommunities. Simplify resolveCommunityModeration by dropping
the dead banned-reporter guard from pass 1 (impossible under strict
rank ordering). Flip useMembersOnlyFilter default to opt-in to match
the spec's MAY wording, and reword the NIP to match.
2026-05-07 22:54:40 -07:00

96 lines
3.0 KiB
TypeScript

import { useCallback, useSyncExternalStore } from 'react';
/**
* LocalStorage key for the "members only" filter toggle.
* Shared across all community surfaces so the preference is global.
*/
const STORAGE_KEY = 'community:members-only';
/**
* Controls whether community views filter content down to posts authored by
* validated members, or show everything scoped to the community.
*
* Defaults to `false` (show everything). The flat-communities spec treats
* members-only as a MAY feature (see NIP.md §Community-Scoped Content) —
* the protocol makes no recommendation, so the default is the broader view
* and users opt in via the shield-icon toggle.
*
* Implementation: a module-level singleton store (Set of subscribers +
* cached boolean). Every component mounting `useMembersOnlyFilter` shares
* the same state instance, so toggling the shield in one subtree
* immediately rerenders every other consumer in the same tab. Changes
* are also persisted to localStorage and synchronised with other tabs
* via the browser's `storage` event.
*/
/** Read the persisted boolean, defaulting to `false` when absent or malformed. */
function readFromStorage(): boolean {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw === null) return false;
return JSON.parse(raw) === true;
} catch {
return false;
}
}
// ── Module-level singleton store ────────────────────────────────────────────
// Module initialisation accesses `localStorage` which is unavailable in some
// SSR-ish environments. Guard so the module can still be imported.
let cached: boolean = typeof localStorage !== 'undefined' ? readFromStorage() : false;
const subscribers = new Set<() => void>();
function notify() {
for (const cb of subscribers) cb();
}
function setMembersOnly(next: boolean) {
if (cached === next) return;
cached = next;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
} catch {
// Storage quota / private mode — swallow; state still updates in-memory.
}
notify();
}
/** Subscribe the singleton to cross-tab `storage` events once per module load. */
if (typeof window !== 'undefined') {
window.addEventListener('storage', (e) => {
if (e.key !== STORAGE_KEY) return;
const next = readFromStorage();
if (next !== cached) {
cached = next;
notify();
}
});
}
function subscribe(cb: () => void) {
subscribers.add(cb);
return () => {
subscribers.delete(cb);
};
}
function getSnapshot() {
return cached;
}
/**
* React hook that reads and writes the members-only filter preference.
*
* All instances share the same underlying state — toggling via one
* call immediately rerenders every component consuming this hook.
*/
export function useMembersOnlyFilter() {
const membersOnly = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
const toggle = useCallback(() => {
setMembersOnly(!cached);
}, []);
return { membersOnly, setMembersOnly, toggle };
}