Files
grimoire/src/hooks/useTimeline.ts
Alejandro b2b398b9fb docs: add applesauce v5 upgrade plan (#39)
* docs: add applesauce v5 upgrade plan

Comprehensive migration plan covering:
- Package updates (add applesauce-common, update to v5)
- EventFactory import migration (applesauce-factory → applesauce-core)
- Unified event loader setup
- ActionHub → ActionRunner migration
- useObservableMemo → use$ hook migration
- New features: casting system, encrypted content caching
- Documentation and skills updates needed

* feat: upgrade applesauce libraries to v5

Major upgrade from applesauce v4 to v5 with breaking changes:

Package updates:
- applesauce-core: ^4.0.0 → ^5.0.0
- applesauce-actions: ^4.0.0 → ^5.0.0
- applesauce-loaders: ^4.0.0 → ^5.0.0
- applesauce-react: ^4.0.0 → ^5.0.0
- applesauce-relay: ^4.0.0 → ^5.0.0
- applesauce-signers: ^4.0.0 → ^5.0.0
- applesauce-accounts: ^4.0.0 → ^5.0.0
- Added new applesauce-common: ^5.0.0 package

API migrations:
- EventFactory: applesauce-factory → applesauce-core/event-factory
- ActionHub → ActionRunner with async function pattern (not generators)
- useObservableMemo → use$ hook across all components
- Helper imports: article, highlight, threading, zap, comment, lists
  moved from applesauce-core to applesauce-common
- parseCoordinate → parseReplaceableAddress
- Subscription options: retries → reconnect
- getEventPointerFromETag now returns null instead of throwing

New features:
- Unified event loader via createEventLoaderForStore
- Updated loaders.ts to use v5 unified loader pattern

Documentation:
- Updated CLAUDE.md with v5 patterns and migration notes
- Updated applesauce-core skill for v5 changes
- Created new applesauce-common skill

Test fixes:
- Updated publish-spellbook.test.ts for v5 ActionRunner pattern
- Updated publish-spell.test.ts with eventStore mock
- Updated relay-selection.test.ts with valid test events
- Updated loaders.test.ts with valid 64-char hex event IDs
- Added createEventLoaderForStore mock

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 14:54:21 +01:00

86 lines
2.3 KiB
TypeScript

import { useState, useEffect } from "react";
import type { NostrEvent, Filter } from "nostr-tools";
import { useEventStore, use$ } from "applesauce-react/hooks";
import { createTimelineLoader } from "@/services/loaders";
import pool from "@/services/relay-pool";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { useStableValue, useStableArray } from "./useStable";
interface UseTimelineOptions {
limit?: number;
}
interface UseTimelineReturn {
events: NostrEvent[];
loading: boolean;
error: Error | null;
}
/**
* Hook for subscribing to a timeline of events from relays
* Uses applesauce loaders for efficient event loading and caching
* @param id - Unique identifier for this timeline (for caching)
* @param filters - Nostr filter object
* @param relays - Array of relay URLs
* @param options - Additional options like limit
* @returns Object containing events array, loading state, and error
*/
export function useTimeline(
id: string,
filters: Filter | Filter[],
relays: string[],
options: UseTimelineOptions = { limit: 20 },
): UseTimelineReturn {
const { limit } = options;
const eventStore = useEventStore();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Stabilize filters and relays to prevent unnecessary re-renders
const stableFilters = useStableValue(filters);
const stableRelays = useStableArray(relays);
// Load events into store
useEffect(() => {
if (relays.length === 0) return;
const loader = createTimelineLoader(
pool,
relays.concat(AGGREGATOR_RELAYS),
filters,
{
eventStore,
limit,
},
);
setLoading(true);
setError(null);
const subscription = loader().subscribe({
error: (err: Error) => {
console.error("Timeline error:", err);
setError(err);
setLoading(false);
},
complete: () => {
setLoading(false);
},
});
return () => subscription.unsubscribe();
}, [id, stableRelays, limit, eventStore, stableFilters]);
// Watch store for matching events
const timeline = use$(() => {
return eventStore.timeline(filters, false);
}, [id]);
const hasItems = timeline ? timeline.length > 0 : false;
return {
events: timeline || [],
loading: hasItems ? false : loading,
error,
};
}