af0e028cde
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.
89 lines
2.8 KiB
TypeScript
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 };
|
|
}
|