Files
eranos/src/hooks/useResolveTabFilter.ts
T
Lemon af0e028cde Revert saved feeds and profile tabs to TabFilter format
Restore the original TabFilter-based storage for saved feeds and profile
tabs. The spell-based storage (kind:777 events embedded in SavedFeed and
ProfileTab) was causing data loss: existing saved feeds and profile tabs
were silently dropped because the Zod schema rejected the old format.

Restored from main:
- profileTabsEvent.ts: TabFilter/TabVarDef types, var tag parsing,
  resolvePointer/resolveFilter for variable substitution
- useResolveTabFilter.ts: hook for resolving tab variables
- useProfileFeed.ts: useTabFeed for custom profile tab feeds
- useSavedFeeds.ts: 3-arg addSavedFeed(label, filter, vars) API
- FeedEditModal.tsx / ProfileTabEditModal.tsx: produce TabFilters
- schemas.ts: SavedFeedSchema with filter/vars fields

Updated consumers:
- Feed.tsx SavedFeedContent: uses useResolveTabFilter + useStreamPosts
- ProfilePage ProfileSavedFeedContent: uses useResolveTabFilter + useTabFeed
- ContentSettings SavedFeedsSection: TabFilter-based CRUD
- SearchPage: builds TabFilters for saving, spell tags only for sharing
- SpellRunPage: converts spell to TabFilter when saving to home feed
- GetFeedTool: resolves saved feed TabFilters directly

Spells (kind:777) remain fully functional for their intended use cases:
standalone shareable feeds, SpellRunPage, Discover tab, sidebar items,
and the AI create_spell tool.
2026-04-14 11:30:05 -07:00

89 lines
2.8 KiB
TypeScript

/**
* useResolveTabFilter
*
* Resolves variable placeholders in a tab filter by:
* 1. Setting `$me` to the owner's pubkey
* 2. Fetching events referenced by `var` tags
* 3. Extracting tag values to populate variables
* 4. Substituting all variables into the filter
*/
import { useMemo } from 'react';
import { useNostr } from '@nostrify/react';
import { useQueries } from '@tanstack/react-query';
import { resolvePointer, resolveFilter, type TabFilter, type TabVarDef } from '@/lib/profileTabsEvent';
import type { NostrFilter } from '@nostrify/nostrify';
interface ResolvedTabFilter {
filter: NostrFilter | null;
isLoading: boolean;
}
/**
* Resolve a tab filter with variable substitution.
*
* @param tabFilter - The filter with potential `$variable` placeholders
* @param vars - Variable definitions from the kind 16769 event
* @param ownerPubkey - The profile owner's pubkey (used as `$me`)
*/
export function useResolveTabFilter(
tabFilter: TabFilter,
vars: TabVarDef[],
ownerPubkey: string,
): ResolvedTabFilter {
const { nostr } = useNostr();
const runtimeVars = useMemo(() => ({ '$me': ownerPubkey }), [ownerPubkey]);
// Build queries for each var definition
const varQueries = useQueries({
queries: vars.map((v) => {
const pointer = resolvePointer(v.pointer, runtimeVars);
const queryFilter: NostrFilter | null = pointer
? pointer.type === 'e'
? { ids: [pointer.id], limit: 1 }
: { kinds: [pointer.kind], authors: [pointer.pubkey], '#d': [pointer.dTag], limit: 1 }
: null;
return {
queryKey: ['tab-var', v.name, v.pointer, ownerPubkey],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
if (!queryFilter) return [];
const events = await nostr.query(
[queryFilter],
{ signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) },
);
if (events.length === 0) return [];
// Extract all values of the specified tag
return events[0].tags
.filter(([name]) => name === v.tagName)
.map(([, value]) => value)
.filter(Boolean) as string[];
},
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
enabled: !!pointer,
};
}),
});
const isLoading = varQueries.some((q) => q.isLoading);
const resolvedFilter = useMemo<NostrFilter | null>(() => {
if (isLoading) return null;
// Build resolved vars map
const resolvedVars: Record<string, string[]> = {};
vars.forEach((v, i) => {
const data = varQueries[i]?.data;
if (data) {
resolvedVars[v.name] = data;
}
});
return resolveFilter(tabFilter, resolvedVars, runtimeVars);
}, [isLoading, vars, varQueries, tabFilter, runtimeVars]);
return { filter: resolvedFilter, isLoading };
}