From 740d3a182af677b939b54dba01446e2317a85e5a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Jan 2026 13:43:06 +0000 Subject: [PATCH] feat: upgrade applesauce libraries to v5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/skills/applesauce-common/SKILL.md | 290 ++++++++++++++++ .claude/skills/applesauce-core/SKILL.md | 23 +- CLAUDE.md | 43 ++- eslint.config.js | 42 +-- package-lock.json | 320 +++++++++--------- package.json | 17 +- postcss.config.js | 2 +- src/actions/delete-event.ts | 2 +- src/actions/publish-spell.test.ts | 6 + src/actions/publish-spell.ts | 2 +- src/actions/publish-spellbook.test.ts | 41 ++- src/actions/publish-spellbook.ts | 24 +- src/components/DynamicWindowTitle.tsx | 2 +- src/components/ProfileViewer.tsx | 6 +- src/components/nostr/ChatView.tsx | 9 +- src/components/nostr/CompactEventRow.tsx | 3 +- src/components/nostr/SpellDialog.tsx | 4 +- .../nostr/compact/ZapCompactPreview.tsx | 2 +- .../nostr/kinds/ArticleDetailRenderer.tsx | 2 +- .../nostr/kinds/ArticleRenderer.tsx | 2 +- .../nostr/kinds/CommunityNIPRenderer.tsx | 2 +- .../nostr/kinds/GenericRelayListRenderer.tsx | 2 +- .../nostr/kinds/HighlightDetailRenderer.tsx | 2 +- .../nostr/kinds/HighlightRenderer.tsx | 4 +- .../nostr/kinds/Kind1111Renderer.tsx | 2 +- src/components/nostr/kinds/NoteRenderer.tsx | 2 +- .../nostr/kinds/ReactionRenderer.tsx | 6 +- .../nostr/kinds/ZapReceiptRenderer.tsx | 2 +- src/components/nostr/relay-pool.tsx | 4 +- src/components/nostr/user-menu.tsx | 4 +- src/hooks/useAccountSync.ts | 4 +- src/hooks/useLiveTimeline.ts | 7 +- src/hooks/useNostrEvent.ts | 4 +- src/hooks/useReqTimeline.ts | 3 +- src/hooks/useReqTimelineEnhanced.ts | 3 +- src/hooks/useTimeline.ts | 4 +- src/lib/event-title.ts | 2 +- src/lib/nostr-utils.ts | 4 +- src/services/hub.ts | 8 +- src/services/loaders.test.ts | 29 +- src/services/loaders.ts | 35 +- src/services/relay-selection.test.ts | 246 ++++++-------- tailwind.config.js | 98 +++--- vercel.json | 4 +- 44 files changed, 830 insertions(+), 493 deletions(-) create mode 100644 .claude/skills/applesauce-common/SKILL.md diff --git a/.claude/skills/applesauce-common/SKILL.md b/.claude/skills/applesauce-common/SKILL.md new file mode 100644 index 0000000..6ab2124 --- /dev/null +++ b/.claude/skills/applesauce-common/SKILL.md @@ -0,0 +1,290 @@ +--- +name: applesauce-common +description: This skill should be used when working with applesauce-common library for social/NIP-specific helpers, casting system, blueprints, and operations. New in applesauce v5 - contains helpers that moved from applesauce-core. +--- + +# applesauce-common Skill (v5) + +This skill provides comprehensive knowledge for working with applesauce-common, a new package in applesauce v5 that contains social/NIP-specific utilities, the casting system, blueprints, and operations. + +**Note**: applesauce-common was introduced in v5. Many helpers that were previously in `applesauce-core/helpers` have moved here. + +## When to Use This Skill + +Use this skill when: +- Working with article, highlight, threading, zap, or reaction helpers +- Using the casting system for typed event access +- Creating events with blueprints +- Modifying events with operations +- Working with NIP-specific social features + +## Package Structure + +``` +applesauce-common/ +├── helpers/ # Social/NIP-specific helpers +│ ├── article.js # NIP-23 article helpers +│ ├── highlight.js # NIP-84 highlight helpers +│ ├── threading.js # NIP-10 thread helpers +│ ├── comment.js # NIP-22 comment helpers +│ ├── zap.js # NIP-57 zap helpers +│ ├── reaction.js # NIP-25 reaction helpers +│ ├── lists.js # NIP-51 list helpers +│ └── ... +├── casts/ # Typed event classes +│ ├── Note.js +│ ├── User.js +│ ├── Profile.js +│ ├── Article.js +│ └── ... +├── blueprints/ # Event creation blueprints +└── operations/ # Event modification operations +``` + +## Helpers (Migrated from applesauce-core) + +### Article Helpers (NIP-23) + +```typescript +import { + getArticleTitle, + getArticleSummary, + getArticleImage, + getArticlePublished +} from 'applesauce-common/helpers/article'; + +// All helpers cache internally - no useMemo needed +const title = getArticleTitle(event); +const summary = getArticleSummary(event); +const image = getArticleImage(event); +const publishedAt = getArticlePublished(event); +``` + +### Highlight Helpers (NIP-84) + +```typescript +import { + getHighlightText, + getHighlightSourceUrl, + getHighlightSourceEventPointer, + getHighlightSourceAddressPointer, + getHighlightContext, + getHighlightComment +} from 'applesauce-common/helpers/highlight'; + +const text = getHighlightText(event); +const sourceUrl = getHighlightSourceUrl(event); +const eventPointer = getHighlightSourceEventPointer(event); +const addressPointer = getHighlightSourceAddressPointer(event); +const context = getHighlightContext(event); +const comment = getHighlightComment(event); +``` + +### Threading Helpers (NIP-10) + +```typescript +import { getNip10References } from 'applesauce-common/helpers/threading'; + +// Parse NIP-10 thread structure +const refs = getNip10References(event); + +if (refs.root) { + console.log('Root event:', refs.root.e); + console.log('Root address:', refs.root.a); +} + +if (refs.reply) { + console.log('Reply to:', refs.reply.e); +} +``` + +### Comment Helpers (NIP-22) + +```typescript +import { getCommentReplyPointer } from 'applesauce-common/helpers/comment'; + +const pointer = getCommentReplyPointer(event); +if (pointer) { + // Handle reply target +} +``` + +### Zap Helpers (NIP-57) + +```typescript +import { + getZapAmount, + getZapSender, + getZapRecipient, + getZapComment +} from 'applesauce-common/helpers/zap'; + +const amount = getZapAmount(event); // In millisats +const sender = getZapSender(event); // Pubkey +const recipient = getZapRecipient(event); +const comment = getZapComment(event); +``` + +### List Helpers (NIP-51) + +```typescript +import { getRelaysFromList } from 'applesauce-common/helpers/lists'; + +const relays = getRelaysFromList(event); +``` + +## Casting System + +The casting system transforms raw Nostr events into typed classes with both synchronous properties and reactive observables. + +### Basic Usage + +```typescript +import { castEvent, Note, User, Profile } from 'applesauce-common/casts'; + +// Cast an event to a typed class +const note = castEvent(event, Note, eventStore); + +// Access synchronous properties +console.log(note.id); +console.log(note.createdAt); +console.log(note.isReply); + +// Subscribe to reactive observables +note.author.profile$.subscribe(profile => { + console.log('Author name:', profile?.name); +}); +``` + +### Available Casts + +- **Note** - Kind 1 short text notes +- **User** - User with profile and social graph +- **Profile** - Kind 0 profile metadata +- **Article** - Kind 30023 long-form articles +- **Reaction** - Kind 7 reactions +- **Zap** - Kind 9735 zap receipts +- **Comment** - NIP-22 comments +- **Share** - Reposts/quotes +- **Bookmarks** - NIP-51 bookmarks +- **Mutes** - NIP-51 mute lists + +### With React + +```typescript +import { use$ } from 'applesauce-react/hooks'; +import { castEvent, Note } from 'applesauce-common/casts'; + +function NoteComponent({ event }) { + const note = castEvent(event, Note, eventStore); + + // Subscribe to author's profile + const profile = use$(note.author.profile$); + + // Subscribe to replies + const replies = use$(note.replies$); + + return ( +
+ {profile?.name} +

{note.content}

+ {replies?.length} replies +
+ ); +} +``` + +## Blueprints + +Blueprints provide templates for creating events. + +```typescript +import { EventFactory } from 'applesauce-core/event-factory'; +import { NoteBlueprint } from 'applesauce-common/blueprints'; + +const factory = new EventFactory({ signer }); + +// Create a note using blueprint +const draft = await factory.build(NoteBlueprint({ + content: 'Hello Nostr!', + tags: [['t', 'nostr']] +})); + +const event = await factory.sign(draft); +``` + +## Operations + +Operations modify existing events. + +```typescript +import { addTag, removeTag } from 'applesauce-common/operations'; + +// Add a tag to an event +const modified = addTag(event, ['t', 'bitcoin']); + +// Remove a tag +const updated = removeTag(event, 'client'); +``` + +## Migration from v4 + +### Helper Import Changes + +```typescript +// ❌ Old (v4) +import { getArticleTitle } from 'applesauce-core/helpers'; +import { getNip10References } from 'applesauce-core/helpers/threading'; +import { getZapAmount } from 'applesauce-core/helpers/zap'; + +// ✅ New (v5) +import { getArticleTitle } from 'applesauce-common/helpers/article'; +import { getNip10References } from 'applesauce-common/helpers/threading'; +import { getZapAmount } from 'applesauce-common/helpers/zap'; +``` + +### Helpers that stayed in applesauce-core + +These protocol-level helpers remain in `applesauce-core/helpers`: +- `getTagValue`, `hasNameValueTag` +- `getProfileContent` +- `parseCoordinate`, `getEventPointerFromETag`, `getAddressPointerFromATag` +- `isFilterEqual`, `matchFilter`, `mergeFilters` +- `getSeenRelays`, `mergeRelaySets` +- `getInboxes`, `getOutboxes` +- `normalizeURL` + +## Best Practices + +### Helper Caching + +All helpers in applesauce-common cache internally using symbols: + +```typescript +// ❌ Don't memoize helper calls +const title = useMemo(() => getArticleTitle(event), [event]); + +// ✅ Call helpers directly +const title = getArticleTitle(event); +``` + +### Casting vs Helpers + +Use **helpers** when you need specific fields: +```typescript +const title = getArticleTitle(event); +const amount = getZapAmount(event); +``` + +Use **casts** when you need reactive data or multiple related properties: +```typescript +const note = castEvent(event, Note, eventStore); +const profile$ = note.author.profile$; +const replies$ = note.replies$; +``` + +## Related Skills + +- **applesauce-core** - Protocol-level helpers and event store +- **applesauce-signers** - Event signing abstractions +- **nostr** - Nostr protocol fundamentals diff --git a/.claude/skills/applesauce-core/SKILL.md b/.claude/skills/applesauce-core/SKILL.md index 8f6cfeb..5ce32fe 100644 --- a/.claude/skills/applesauce-core/SKILL.md +++ b/.claude/skills/applesauce-core/SKILL.md @@ -3,9 +3,15 @@ name: applesauce-core description: This skill should be used when working with applesauce-core library for Nostr client development, including event stores, queries, observables, and client utilities. Provides comprehensive knowledge of applesauce patterns for building reactive Nostr applications. --- -# applesauce-core Skill +# applesauce-core Skill (v5) -This skill provides comprehensive knowledge and patterns for working with applesauce-core, a library that provides reactive utilities and patterns for building Nostr clients. +This skill provides comprehensive knowledge and patterns for working with applesauce-core v5, a library that provides reactive utilities and patterns for building Nostr clients. + +**Note**: applesauce v5 introduced package reorganization: +- Protocol-level code stays in `applesauce-core` +- Social/NIP-specific helpers moved to `applesauce-common` +- `EventFactory` moved from `applesauce-factory` to `applesauce-core/event-factory` +- `ActionHub` renamed to `ActionRunner` in `applesauce-actions` ## When to Use This Skill @@ -396,6 +402,8 @@ function getTagValues(event, tagName) { ### Article Helpers (NIP-23) +**Note**: Article helpers moved to `applesauce-common` in v5. + ```javascript import { getArticleTitle, @@ -403,7 +411,7 @@ import { getArticleImage, getArticlePublished, isValidArticle -} from 'applesauce-core/helpers'; +} from 'applesauce-common/helpers/article'; // All cached automatically const title = getArticleTitle(event); @@ -419,6 +427,8 @@ if (isValidArticle(event)) { ### Highlight Helpers (NIP-84) +**Note**: Highlight helpers moved to `applesauce-common` in v5. + ```javascript import { getHighlightText, @@ -428,7 +438,7 @@ import { getHighlightContext, getHighlightComment, getHighlightAttributions -} from 'applesauce-core/helpers'; +} from 'applesauce-common/helpers/highlight'; // All cached - no useMemo needed const text = getHighlightText(event); @@ -514,8 +524,10 @@ if (eventPointer) { ### Threading Helpers (NIP-10) +**Note**: Threading helpers moved to `applesauce-common` in v5. + ```javascript -import { getNip10References } from 'applesauce-core/helpers'; +import { getNip10References } from 'applesauce-common/helpers/threading'; // Parse NIP-10 thread structure (cached) const refs = getNip10References(event); @@ -907,6 +919,7 @@ function createInfiniteScroll(timeline, pageSize = 50) { ## Related Skills - **nostr-tools** - Lower-level Nostr operations +- **applesauce-common** - Social/NIP-specific helpers (article, highlight, threading, zap, etc.) - **applesauce-signers** - Event signing abstractions - **svelte** - Building reactive UIs - **nostr** - Nostr protocol fundamentals diff --git a/CLAUDE.md b/CLAUDE.md index 1f6f13e..507ddfc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,17 @@ Grimoire is a Nostr protocol explorer and developer tool. It's a tiling window m **Critical**: Don't create new EventStore, RelayPool, or RelayLiveness instances - use the singletons in `src/services/` +**Event Loading** (`src/services/loaders.ts`): +- Unified loader auto-fetches missing events when queried via `eventStore.event()` or `eventStore.replaceable()` +- Custom `eventLoader()` with smart relay hint merging for explicit loading with context +- `addressLoader` and `profileLoader` for replaceable events with batching +- `createTimelineLoader` for paginated feeds + +**Action System** (`src/services/hub.ts`): +- `ActionRunner` (v5) executes actions with signing and publishing +- Actions are async functions: `async ({ factory, sign, publish }) => { ... }` +- Use `await publish(event)` to publish (not generators/yield) + ### Window System Windows are rendered in a recursive binary split layout (via `react-mosaic-component`): @@ -72,6 +83,18 @@ Applesauce uses RxJS observables for reactive data flow: Use hooks like `useProfile()`, `useNostrEvent()`, `useTimeline()` - they handle subscriptions. +**The `use$` Hook** (applesauce v5): +```typescript +import { use$ } from "applesauce-react/hooks"; + +// Direct observable (for BehaviorSubjects - never undefined) +const account = use$(accounts.active$); + +// Factory with deps (for dynamic observables) +const event = use$(() => eventStore.event(eventId), [eventId]); +const timeline = use$(() => eventStore.timeline(filters), [filters]); +``` + ### Applesauce Helpers & Caching **Critical Performance Insight**: Applesauce helpers cache computed values internally using symbols. **You don't need `useMemo` when calling applesauce helpers.** @@ -88,16 +111,24 @@ const text = getHighlightText(event); **How it works**: Helpers use `getOrComputeCachedValue(event, symbol, compute)` to cache results on the event object. The first call computes and caches, subsequent calls return the cached value instantly. -**Available Helpers** (from `applesauce-core/helpers`): +**Available Helpers** (split between packages in applesauce v5): + +*From `applesauce-core/helpers` (protocol-level):* - **Tags**: `getTagValue(event, name)` - get single tag value (searches hidden tags first) -- **Article**: `getArticleTitle`, `getArticleSummary`, `getArticleImage`, `getArticlePublished` -- **Highlight**: `getHighlightText`, `getHighlightSourceUrl`, `getHighlightSourceEventPointer`, `getHighlightSourceAddressPointer`, `getHighlightContext`, `getHighlightComment` - **Profile**: `getProfileContent(event)`, `getDisplayName(metadata, fallback)` - **Pointers**: `parseCoordinate(aTag)`, `getEventPointerFromETag`, `getAddressPointerFromATag`, `getProfilePointerFromPTag` -- **Reactions**: `getReactionEventPointer(event)`, `getReactionAddressPointer(event)` -- **Threading**: `getNip10References(event)` - parses NIP-10 thread tags - **Filters**: `isFilterEqual(a, b)`, `matchFilter(filter, event)`, `mergeFilters(...filters)` -- **And 50+ more** - see `node_modules/applesauce-core/dist/helpers/index.d.ts` +- **Relays**: `getSeenRelays`, `mergeRelaySets`, `getInboxes`, `getOutboxes` +- **URL**: `normalizeURL` + +*From `applesauce-common/helpers` (social/NIP-specific):* +- **Article**: `getArticleTitle`, `getArticleSummary`, `getArticleImage`, `getArticlePublished` +- **Highlight**: `getHighlightText`, `getHighlightSourceUrl`, `getHighlightSourceEventPointer`, `getHighlightSourceAddressPointer`, `getHighlightContext`, `getHighlightComment` +- **Threading**: `getNip10References(event)` - parses NIP-10 thread tags +- **Comment**: `getCommentReplyPointer(event)` - parses NIP-22 comment replies +- **Zap**: `getZapAmount`, `getZapSender`, `getZapRecipient`, `getZapComment` +- **Reactions**: `getReactionEventPointer(event)`, `getReactionAddressPointer(event)` +- **Lists**: `getRelaysFromList` **Custom Grimoire Helpers** (not in applesauce): - `getTagValues(event, name)` - plural version to get array of tag values (src/lib/nostr-utils.ts) diff --git a/eslint.config.js b/eslint.config.js index 866cec9..51e8f85 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,41 +1,41 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import prettier from 'eslint-plugin-prettier' -import prettierConfig from 'eslint-config-prettier' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import prettier from "eslint-plugin-prettier"; +import prettierConfig from "eslint-config-prettier"; export default tseslint.config( - { ignores: ['dist', 'node_modules', '.claude'] }, + { ignores: ["dist", "node_modules", ".claude"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - 'prettier': prettier, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + prettier: prettier, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': [ - 'warn', + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", }, ], - 'prettier/prettier': 'error', + "prettier/prettier": "error", }, }, prettierConfig, -) +); diff --git a/package-lock.json b/package-lock.json index 26cfa02..ac97e9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,14 +24,15 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@types/qrcode": "^1.5.6", - "applesauce-accounts": "^4.1.0", - "applesauce-actions": "^4.0.0", - "applesauce-content": "^4.0.0", - "applesauce-core": "latest", - "applesauce-loaders": "^4.2.0", - "applesauce-react": "^4.0.0", - "applesauce-relay": "latest", - "applesauce-signers": "^4.1.0", + "applesauce-accounts": "^5.0.0", + "applesauce-actions": "^5.0.0", + "applesauce-common": "^5.0.0", + "applesauce-content": "^5.0.0", + "applesauce-core": "^5.0.0", + "applesauce-loaders": "^5.0.0", + "applesauce-react": "^5.0.0", + "applesauce-relay": "^5.0.0", + "applesauce-signers": "^5.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -673,43 +674,26 @@ } }, "node_modules/@cashu/cashu-ts": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-2.8.1.tgz", - "integrity": "sha512-4HO3LC3VqiMs0K7ccQdfSs3l1wJNL0VuE8ZQ6zAfMsoeKRwswA1eC5BaGFrEDv7PcPqjliE/RBRw3+1Hz/SmsA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-3.2.2.tgz", + "integrity": "sha512-FD3EBYQiDRhZFwCMXuhAGRAb279WpGEWePzRQk58GJSZy16OdcP3hrYmj7L9wWdETy2fQU0wn+bCuw2NAB6szQ==", "license": "MIT", "dependencies": { - "@noble/curves": "^1.9.5", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0" + "@noble/curves": "^2.0.1", + "@noble/hashes": "^2.0.1", + "@scure/bip32": "^2.0.1" }, "engines": { "node": ">=22.4.0" } }, - "node_modules/@cashu/cashu-ts/node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "node_modules/@cashu/cashu-ts/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@cashu/cashu-ts/node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1642,24 +1626,27 @@ } }, "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.3.2" + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", "license": "MIT", "engines": { - "node": ">= 16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -4076,59 +4063,35 @@ } }, "node_modules/@scure/bip32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", - "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", + "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", "license": "MIT", "dependencies": { - "@noble/curves": "~1.1.0", - "@noble/hashes": "~1.3.1", - "@scure/base": "~1.1.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", - "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.3.1" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", - "license": "MIT", - "engines": { - "node": ">= 16" + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", "license": "MIT", "engines": { - "node": ">= 16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" @@ -4974,16 +4937,14 @@ } }, "node_modules/applesauce-accounts": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/applesauce-accounts/-/applesauce-accounts-4.1.0.tgz", - "integrity": "sha512-4vRUpkJL8RpVBiboxDb5fUPkeqv+rTIlw3Tog79K1paLHJUcUokcdzzdZLEmS/C531n0AamO2Qvr1XiBFqR5xg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/applesauce-accounts/-/applesauce-accounts-5.0.0.tgz", + "integrity": "sha512-2WR9bRpU2ONvaMTn2NINNYjsBUGeJJVuEDkgafacOwfz+TEKWJ0A9ibtfA9JfMer2GAFs/jt5TsLjzWhHY9zcg==", "license": "MIT", "dependencies": { - "@noble/hashes": "^1.7.1", - "applesauce-core": "^4.1.0", - "applesauce-signers": "^4.1.0", + "applesauce-core": "^5.0.0", + "applesauce-signers": "^5.0.0", "nanoid": "^5.1.5", - "nostr-tools": "~2.17", "rxjs": "^7.8.1" }, "funding": { @@ -4992,14 +4953,30 @@ } }, "node_modules/applesauce-actions": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/applesauce-actions/-/applesauce-actions-4.0.0.tgz", - "integrity": "sha512-oYAjrazKGDINeVwypNDnV9eNSv7ZDTjNeV3azo5jeUU1haEQ0t+zwVWzGxk9/VutT1yWQHFsCZBInYZIegfLhQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/applesauce-actions/-/applesauce-actions-5.0.0.tgz", + "integrity": "sha512-Lw9x3P3+p9udmA9BvAssJDasDr+eIXq22SBwS3D6kt+3TOnBmJqONR3ru6K3j5S5MflYsiiy66b4TcATrBOXgQ==", "license": "MIT", "dependencies": { - "applesauce-core": "^4.0.0", - "applesauce-factory": "^4.0.0", - "nostr-tools": "~2.17", + "applesauce-common": "^5.0.0", + "applesauce-core": "^5.0.0", + "rxjs": "^7.8.1" + }, + "funding": { + "type": "lightning", + "url": "lightning:nostrudel@geyser.fund" + } + }, + "node_modules/applesauce-common": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/applesauce-common/-/applesauce-common-5.0.0.tgz", + "integrity": "sha512-97ezzvy13yulozQDfjioW5MMnejPRr2ZrCqzJDbZm28elslS0x/s6rFh+NoHQFvStYuxgHOa3BIcv/E2qNhPqg==", + "license": "MIT", + "dependencies": { + "@scure/base": "^1.2.4", + "applesauce-core": "^5.0.0", + "hash-sum": "^2.0.0", + "light-bolt11-decoder": "^3.2.0", "rxjs": "^7.8.1" }, "funding": { @@ -5008,18 +4985,18 @@ } }, "node_modules/applesauce-content": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/applesauce-content/-/applesauce-content-4.0.0.tgz", - "integrity": "sha512-2ZrhM/UCQkcZcAldXJX+KfWAPAtkoTXH5BwPhYpaMw0UgHjWX8mYiy/801PtLBr2gWkKd/Dw1obdNDcPUO3idw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/applesauce-content/-/applesauce-content-5.0.0.tgz", + "integrity": "sha512-k8D+jl6XKUhAgnfDv0loeisCCsW8gGVFqLT4MQQKkQaR77vJM/zJ8O+ulq3lZraWOboTjCRzxVxpniButqW3ZA==", "license": "MIT", "dependencies": { - "@cashu/cashu-ts": "^2.7.2", + "@cashu/cashu-ts": "^3.1.1", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3", - "applesauce-core": "^4.0.0", + "applesauce-common": "^5.0.0", + "applesauce-core": "^5.0.0", "mdast-util-find-and-replace": "^3.0.2", - "nostr-tools": "~2.17", "remark": "^15.0.1", "remark-parse": "^11.0.0", "unified": "^11.0.5", @@ -5031,19 +5008,16 @@ } }, "node_modules/applesauce-core": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/applesauce-core/-/applesauce-core-4.4.2.tgz", - "integrity": "sha512-zuZB74Pp28UGM4e8DWbN1atR95xL7ODENvjkaGGnvAjIKvfdgMznU7m9gLxr/Hu+IHOmVbbd4YxwNmKBzCWhHQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/applesauce-core/-/applesauce-core-5.0.0.tgz", + "integrity": "sha512-l41CGztEakxAl8nk3KhaHrf7LLIe7+6tfDF1gC5mLnfSpV9raEsUUlmhE+t0AzQPmoEmyUPiWzc32KeP/u85YA==", "license": "MIT", "dependencies": { - "@noble/hashes": "^1.7.1", - "@scure/base": "^1.2.4", "debug": "^4.4.0", "fast-deep-equal": "^3.1.3", "hash-sum": "^2.0.0", - "light-bolt11-decoder": "^3.2.0", "nanoid": "^5.0.9", - "nostr-tools": "~2.17", + "nostr-tools": "~2.19", "rxjs": "^7.8.1" }, "funding": { @@ -5051,31 +5025,14 @@ "url": "lightning:nostrudel@geyser.fund" } }, - "node_modules/applesauce-factory": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/applesauce-factory/-/applesauce-factory-4.0.0.tgz", - "integrity": "sha512-Sqsg+bC7CkRXMxXLkO6YGoKxy/Aqtia9YenasS5qjPOQFmyFMwKRxaHCu6vX6KdpNSABusw0b9Tnn4gTh6CxLw==", - "license": "MIT", - "dependencies": { - "applesauce-content": "^4.0.0", - "applesauce-core": "^4.0.0", - "nanoid": "^5.0.9", - "nostr-tools": "^2.13" - }, - "funding": { - "type": "lightning", - "url": "lightning:nostrudel@geyser.fund" - } - }, "node_modules/applesauce-loaders": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/applesauce-loaders/-/applesauce-loaders-4.2.0.tgz", - "integrity": "sha512-FA5JH3qTcxylciN9SfWPF9DjNyCX6ZLj8iBQo6K+sNQfFBLVqcHjSXDT+whJyJ/T/Obk2yF3HxB2hqFzv8nKzA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/applesauce-loaders/-/applesauce-loaders-5.0.0.tgz", + "integrity": "sha512-iu06vscZyaA+tA5cndMrKBsmYk1wLucwP5Gb0n0GmAAKeG58SPIIR8lEJhfKoVGlDoV65jiurVml7X5e9TNq0Q==", "license": "MIT", "dependencies": { - "applesauce-core": "^4.2.0", + "applesauce-core": "^5.0.0", "nanoid": "^5.0.9", - "nostr-tools": "~2.17", "rxjs": "^7.8.1" }, "funding": { @@ -5084,49 +5041,37 @@ } }, "node_modules/applesauce-react": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/applesauce-react/-/applesauce-react-4.0.0.tgz", - "integrity": "sha512-eVDUf3GL1j4bsL1Y8GsC/2sywajLu1oJioCNajUsm68hf5+zIR0rLHWaA4y0o5Rcctf/O4UbYkFztj1XHcuHgg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/applesauce-react/-/applesauce-react-5.0.1.tgz", + "integrity": "sha512-MbcDpFQID+v/tF2dbLwWelmiFH8RbwjHJMDsZwm99kyieGo2CEJGkqzv2nP9xGyA5o8eJyrcbrZWsE/2QqOZVQ==", "license": "MIT", "dependencies": { - "applesauce-accounts": "^4.0.0", - "applesauce-actions": "^4.0.0", - "applesauce-content": "^4.0.0", - "applesauce-core": "^4.0.0", - "applesauce-factory": "^4.0.0", + "applesauce-accounts": "^5.0.0", + "applesauce-actions": "^5.0.0", + "applesauce-content": "^5.0.0", + "applesauce-core": "^5.0.0", "hash-sum": "^2.0.0", - "nostr-tools": "~2.17", "observable-hooks": "^4.2.4", - "react": "^18.3.1", "rxjs": "^7.8.1" }, "funding": { "type": "lightning", "url": "lightning:nostrudel@geyser.fund" - } - }, - "node_modules/applesauce-react/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" }, - "engines": { - "node": ">=0.10.0" + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/applesauce-relay": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/applesauce-relay/-/applesauce-relay-4.4.2.tgz", - "integrity": "sha512-JAmUvIQ0jFrBWHU5SxAFx1xEG9D8xL7aiinSNX05qcULVMcFv1t9llLJivRLNERpbfS4AGYfy2tipYzc/YtyIQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/applesauce-relay/-/applesauce-relay-5.0.0.tgz", + "integrity": "sha512-XcL3ymwwENGabRPTddATuujXlP6IyDMnwV9vL/TaS0OL+WycykB5wGTG9u+FRSvndVzcoD0FFKVVqW4vL9y8hw==", "license": "MIT", "dependencies": { "@noble/hashes": "^1.7.1", - "applesauce-core": "^4.4.0", + "applesauce-core": "^5.0.0", "nanoid": "^5.0.9", - "nostr-tools": "~2.17", + "nostr-tools": "~2.19", "rxjs": "^7.8.1" }, "funding": { @@ -5135,18 +5080,15 @@ } }, "node_modules/applesauce-signers": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/applesauce-signers/-/applesauce-signers-4.2.0.tgz", - "integrity": "sha512-celexNd+aLt6/vhf72XXw2oAk8ohjna+aWEg/Z2liqPwP+kbVjnqq4Z1RXvt79QQbTIQbXYGWqervXWLE8HmHg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/applesauce-signers/-/applesauce-signers-5.0.0.tgz", + "integrity": "sha512-wybzjnK584iTH5SeAgQHVk/CwYgFFahV1T/3oU3wR6v6TecFnDe7cVhBToYMk2F8ANQVcrDYEebyeq844+al7w==", "license": "MIT", "dependencies": { - "@noble/hashes": "^1.7.1", "@noble/secp256k1": "^1.7.1", - "@scure/base": "^1.2.4", - "applesauce-core": "^4.2.0", + "applesauce-core": "^5.0.0", "debug": "^4.4.0", "nanoid": "^5.0.9", - "nostr-tools": "~2.17", "rxjs": "^7.8.2" }, "funding": { @@ -8481,9 +8423,9 @@ } }, "node_modules/nostr-tools": { - "version": "2.17.4", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.17.4.tgz", - "integrity": "sha512-LGqpKufnmR93tOjFi4JZv1BTTVIAVfZAaAa+1gMqVfI0wNz2DnCB6UDXmjVTRrjQHMw2ykbk0EZLPzV5UeCIJw==", + "version": "2.19.4", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.19.4.tgz", + "integrity": "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg==", "license": "Unlicense", "dependencies": { "@noble/ciphers": "^0.5.1", @@ -8503,6 +8445,30 @@ } } }, + "node_modules/nostr-tools/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/nostr-tools/node_modules/@noble/hashes": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", @@ -8527,6 +8493,32 @@ ], "license": "MIT" }, + "node_modules/nostr-tools/node_modules/@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/nostr-tools/node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/nostr-wasm": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", diff --git a/package.json b/package.json index bc23b5a..7eff3b4 100644 --- a/package.json +++ b/package.json @@ -32,14 +32,15 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@types/qrcode": "^1.5.6", - "applesauce-accounts": "^4.1.0", - "applesauce-actions": "^4.0.0", - "applesauce-content": "^4.0.0", - "applesauce-core": "latest", - "applesauce-loaders": "^4.2.0", - "applesauce-react": "^4.0.0", - "applesauce-relay": "latest", - "applesauce-signers": "^4.1.0", + "applesauce-accounts": "^5.0.0", + "applesauce-actions": "^5.0.0", + "applesauce-common": "^5.0.0", + "applesauce-content": "^5.0.0", + "applesauce-core": "^5.0.0", + "applesauce-loaders": "^5.0.0", + "applesauce-react": "^5.0.0", + "applesauce-relay": "^5.0.0", + "applesauce-signers": "^5.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/postcss.config.js b/postcss.config.js index 2e7af2b..2aa7205 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/src/actions/delete-event.ts b/src/actions/delete-event.ts index bb2e2ab..7b19cbd 100644 --- a/src/actions/delete-event.ts +++ b/src/actions/delete-event.ts @@ -1,6 +1,6 @@ import accountManager from "@/services/accounts"; import pool from "@/services/relay-pool"; -import { EventFactory } from "applesauce-factory"; +import { EventFactory } from "applesauce-core/event-factory"; import { relayListCache } from "@/services/relay-list-cache"; import { AGGREGATOR_RELAYS } from "@/services/loaders"; import { mergeRelaySets } from "applesauce-core/helpers"; diff --git a/src/actions/publish-spell.test.ts b/src/actions/publish-spell.test.ts index 49a5e0c..078f3ce 100644 --- a/src/actions/publish-spell.test.ts +++ b/src/actions/publish-spell.test.ts @@ -31,6 +31,12 @@ vi.mock("@/services/relay-list-cache", () => ({ }, })); +vi.mock("@/services/event-store", () => ({ + default: { + add: vi.fn(), + }, +})); + describe("PublishSpellAction", () => { let action: PublishSpellAction; diff --git a/src/actions/publish-spell.ts b/src/actions/publish-spell.ts index 3c60257..0a56152 100644 --- a/src/actions/publish-spell.ts +++ b/src/actions/publish-spell.ts @@ -3,7 +3,7 @@ import accountManager from "@/services/accounts"; import pool from "@/services/relay-pool"; import { encodeSpell } from "@/lib/spell-conversion"; import { markSpellPublished } from "@/services/spell-storage"; -import { EventFactory } from "applesauce-factory"; +import { EventFactory } from "applesauce-core/event-factory"; import { SpellEvent } from "@/types/spell"; import { relayListCache } from "@/services/relay-list-cache"; import { AGGREGATOR_RELAYS } from "@/services/loaders"; diff --git a/src/actions/publish-spellbook.test.ts b/src/actions/publish-spellbook.test.ts index 13deafa..e4302f6 100644 --- a/src/actions/publish-spellbook.test.ts +++ b/src/actions/publish-spellbook.test.ts @@ -35,10 +35,34 @@ const mockFactory = { })), }; +// Track published events +const publishedEvents: NostrEvent[] = []; + +const mockSign = vi.fn(async (draft: any) => ({ + ...draft, + sig: "test-signature", +})); + +// v5: publish accepts (event | events, relays?) +const mockPublish = vi.fn( + async (event: NostrEvent | NostrEvent[], _relays?: string[]) => { + if (Array.isArray(event)) { + publishedEvents.push(...event); + } else { + publishedEvents.push(event); + } + }, +); + const mockContext: ActionContext = { factory: mockFactory as any, events: {} as any, self: "test-pubkey", + user: {} as any, + signer: mockSigner as any, + sign: mockSign, + publish: mockPublish, + run: vi.fn(), }; const mockState: GrimoireState = { @@ -64,16 +88,17 @@ const mockState: GrimoireState = { workspaceOrder: ["ws-1"], } as any; -// Helper to run action with context +// Helper to run action with context (v5 - async function, not generator) async function runAction( options: Parameters[0], ): Promise { - const events: NostrEvent[] = []; + // Clear published events before each run + publishedEvents.length = 0; + const action = PublishSpellbook(options); - for await (const event of action(mockContext)) { - events.push(event); - } - return events; + await action(mockContext); + + return [...publishedEvents]; } describe("PublishSpellbook action", () => { @@ -267,13 +292,13 @@ describe("PublishSpellbook action", () => { ); }); - it("should call factory.sign with draft", async () => { + it("should call sign with draft", async () => { await runAction({ state: mockState, title: "Test", }); - expect(mockFactory.sign).toHaveBeenCalledWith( + expect(mockSign).toHaveBeenCalledWith( expect.objectContaining({ kind: 30777, }), diff --git a/src/actions/publish-spellbook.ts b/src/actions/publish-spellbook.ts index 0eeb01b..7a07f54 100644 --- a/src/actions/publish-spellbook.ts +++ b/src/actions/publish-spellbook.ts @@ -4,7 +4,6 @@ import { GrimoireState } from "@/types/app"; import { SpellbookContent } from "@/types/spell"; import accountManager from "@/services/accounts"; import type { ActionContext } from "applesauce-actions"; -import type { NostrEvent } from "nostr-tools/core"; export interface PublishSpellbookOptions { state: GrimoireState; @@ -20,24 +19,23 @@ export interface PublishSpellbookOptions { * This action: * 1. Validates inputs (title, account, signer) * 2. Creates spellbook event from state or explicit content - * 3. Signs the event using the action hub's factory - * 4. Yields the signed event (ActionHub handles publishing) + * 3. Signs the event using the action runner's factory + * 4. Publishes the signed event via ActionRunner * * NOTE: This action does NOT mark the local spellbook as published. * The caller should use hub.exec() and call markSpellbookPublished() * AFTER successful publish to ensure data consistency. * * @param options - Spellbook publishing options - * @returns Action generator for ActionHub + * @returns Action for ActionRunner * * @throws Error if title is empty, no active account, or no signer available * * @example * ```typescript - * // Publish via ActionHub with proper side-effect handling + * // Publish via ActionRunner with proper side-effect handling * const event = await lastValueFrom(hub.exec(PublishSpellbook, options)); * if (event) { - * await publishEvent(event); * // Only mark as published AFTER successful relay publish * await markSpellbookPublished(localId, event as SpellbookEvent); * } @@ -46,9 +44,11 @@ export interface PublishSpellbookOptions { export function PublishSpellbook(options: PublishSpellbookOptions) { const { state, title, description, workspaceIds, content } = options; - return async function* ({ + return async function ({ factory, - }: ActionContext): AsyncGenerator { + sign, + publish, + }: ActionContext): Promise { // 1. Validate inputs if (!title || !title.trim()) { throw new Error("Title is required"); @@ -101,12 +101,12 @@ export function PublishSpellbook(options: PublishSpellbookOptions) { tags: eventProps.tags, }); - // 4. Sign the event - const event = (await factory.sign(draft)) as SpellbookEvent; + // 4. Sign and publish the event + const event = (await sign(draft)) as SpellbookEvent; - // 5. Yield signed event - ActionHub handles relay selection and publishing + // 5. Publish event - ActionRunner handles relay selection // NOTE: Caller is responsible for marking local spellbook as published // after successful publish using markSpellbookPublished() - yield event; + await publish(event); }; } diff --git a/src/components/DynamicWindowTitle.tsx b/src/components/DynamicWindowTitle.tsx index 2534683..289fca6 100644 --- a/src/components/DynamicWindowTitle.tsx +++ b/src/components/DynamicWindowTitle.tsx @@ -25,7 +25,7 @@ import { UserName } from "./nostr/UserName"; import { getTagValues } from "@/lib/nostr-utils"; import { getLiveHost } from "@/lib/live-activity"; import type { NostrEvent } from "@/types/nostr"; -import { getZapSender } from "applesauce-core/helpers/zap"; +import { getZapSender } from "applesauce-common/helpers/zap"; export interface WindowTitleData { title: string | ReactElement; diff --git a/src/components/ProfileViewer.tsx b/src/components/ProfileViewer.tsx index c62a6ba..67ae845 100644 --- a/src/components/ProfileViewer.tsx +++ b/src/components/ProfileViewer.tsx @@ -11,7 +11,7 @@ import { Wifi, } from "lucide-react"; import { kinds, nip19 } from "nostr-tools"; -import { useEventStore, useObservableMemo } from "applesauce-react/hooks"; +import { useEventStore, use$ } from "applesauce-react/hooks"; import { getInboxes, getOutboxes } from "applesauce-core/helpers/mailboxes"; import { useCopy } from "../hooks/useCopy"; import { RichText } from "./nostr/RichText"; @@ -100,7 +100,7 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) { }, [resolvedPubkey, eventStore]); // Get mailbox relays (kind 10002) - will update when fresh data arrives - const mailboxEvent = useObservableMemo( + const mailboxEvent = use$( () => resolvedPubkey ? eventStore.replaceable(kinds.RelayList, resolvedPubkey, "") @@ -113,7 +113,7 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) { mailboxEvent && mailboxEvent.tags ? getOutboxes(mailboxEvent) : []; // Get profile metadata event (kind 0) - const profileEvent = useObservableMemo( + const profileEvent = use$( () => resolvedPubkey ? eventStore.replaceable(0, resolvedPubkey, "") diff --git a/src/components/nostr/ChatView.tsx b/src/components/nostr/ChatView.tsx index 9badf81..1973342 100644 --- a/src/components/nostr/ChatView.tsx +++ b/src/components/nostr/ChatView.tsx @@ -6,12 +6,9 @@ import { UserName } from "./UserName"; import { RichText } from "./RichText"; import { Zap, CornerDownRight, Quote } from "lucide-react"; import { cn } from "@/lib/utils"; -import { - getZapAmount, - getZapSender, - getTagValue, -} from "applesauce-core/helpers"; -import { getNip10References } from "applesauce-core/helpers/threading"; +import { getTagValue } from "applesauce-core/helpers"; +import { getZapAmount, getZapSender } from "applesauce-common/helpers/zap"; +import { getNip10References } from "applesauce-common/helpers/threading"; import { useNostrEvent } from "@/hooks/useNostrEvent"; interface ChatViewProps { diff --git a/src/components/nostr/CompactEventRow.tsx b/src/components/nostr/CompactEventRow.tsx index 94a3d85..34dba48 100644 --- a/src/components/nostr/CompactEventRow.tsx +++ b/src/components/nostr/CompactEventRow.tsx @@ -3,7 +3,8 @@ import type { NostrEvent } from "@/types/nostr"; import { kinds } from "nostr-tools"; import { useGrimoire } from "@/core/state"; import { formatTimestamp } from "@/hooks/useLocale"; -import { getTagValue, getZapSender } from "applesauce-core/helpers"; +import { getTagValue } from "applesauce-core/helpers"; +import { getZapSender } from "applesauce-common/helpers/zap"; import { KindBadge } from "@/components/KindBadge"; import { UserName } from "./UserName"; import { compactRenderers, DefaultCompactPreview } from "./compact"; diff --git a/src/components/nostr/SpellDialog.tsx b/src/components/nostr/SpellDialog.tsx index b3dcc37..fa2dbaa 100644 --- a/src/components/nostr/SpellDialog.tsx +++ b/src/components/nostr/SpellDialog.tsx @@ -11,7 +11,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; -import { useObservableMemo } from "applesauce-react/hooks"; +import { use$ } from "applesauce-react/hooks"; import accounts from "@/services/accounts"; import { parseReqCommand } from "@/lib/req-parser"; import { reconstructCommand } from "@/lib/spell-conversion"; @@ -75,7 +75,7 @@ export function SpellDialog({ existingSpell, onSuccess, }: SpellDialogProps) { - const activeAccount = useObservableMemo(() => accounts.active$, []); + const activeAccount = use$(accounts.active$); // Form state const [alias, setAlias] = useState(""); diff --git a/src/components/nostr/compact/ZapCompactPreview.tsx b/src/components/nostr/compact/ZapCompactPreview.tsx index 29d17a3..81f6628 100644 --- a/src/components/nostr/compact/ZapCompactPreview.tsx +++ b/src/components/nostr/compact/ZapCompactPreview.tsx @@ -6,7 +6,7 @@ import { getZapEventPointer, getZapAddressPointer, getZapRequest, -} from "applesauce-core/helpers/zap"; +} from "applesauce-common/helpers/zap"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { UserName } from "../UserName"; import { RichText } from "../RichText"; diff --git a/src/components/nostr/kinds/ArticleDetailRenderer.tsx b/src/components/nostr/kinds/ArticleDetailRenderer.tsx index 6eeb0f2..3e0a705 100644 --- a/src/components/nostr/kinds/ArticleDetailRenderer.tsx +++ b/src/components/nostr/kinds/ArticleDetailRenderer.tsx @@ -4,7 +4,7 @@ import { getArticleSummary, getArticlePublished, getArticleImage, -} from "applesauce-core/helpers/article"; +} from "applesauce-common/helpers/article"; import { UserName } from "../UserName"; import { MediaEmbed } from "../MediaEmbed"; import { MarkdownContent } from "../MarkdownContent"; diff --git a/src/components/nostr/kinds/ArticleRenderer.tsx b/src/components/nostr/kinds/ArticleRenderer.tsx index f13c057..78d62ad 100644 --- a/src/components/nostr/kinds/ArticleRenderer.tsx +++ b/src/components/nostr/kinds/ArticleRenderer.tsx @@ -6,7 +6,7 @@ import { import { getArticleTitle, getArticleSummary, -} from "applesauce-core/helpers/article"; +} from "applesauce-common/helpers/article"; /** * Renderer for Kind 30023 - Long-form Article diff --git a/src/components/nostr/kinds/CommunityNIPRenderer.tsx b/src/components/nostr/kinds/CommunityNIPRenderer.tsx index 4caf252..ccd8dc3 100644 --- a/src/components/nostr/kinds/CommunityNIPRenderer.tsx +++ b/src/components/nostr/kinds/CommunityNIPRenderer.tsx @@ -3,7 +3,7 @@ import { BaseEventProps, ClickableEventTitle, } from "./BaseEventRenderer"; -import { getArticleTitle } from "applesauce-core/helpers"; +import { getArticleTitle } from "applesauce-common/helpers/article"; /** * Renderer for Kind 30817 - Community NIP diff --git a/src/components/nostr/kinds/GenericRelayListRenderer.tsx b/src/components/nostr/kinds/GenericRelayListRenderer.tsx index 70d4328..72ec65c 100644 --- a/src/components/nostr/kinds/GenericRelayListRenderer.tsx +++ b/src/components/nostr/kinds/GenericRelayListRenderer.tsx @@ -1,5 +1,5 @@ import { BaseEventProps, BaseEventContainer } from "./BaseEventRenderer"; -import { getRelaysFromList } from "applesauce-core/helpers/lists"; +import { getRelaysFromList } from "applesauce-common/helpers/lists"; import { RelayLink } from "../RelayLink"; /** diff --git a/src/components/nostr/kinds/HighlightDetailRenderer.tsx b/src/components/nostr/kinds/HighlightDetailRenderer.tsx index c7480fc..8e1b538 100644 --- a/src/components/nostr/kinds/HighlightDetailRenderer.tsx +++ b/src/components/nostr/kinds/HighlightDetailRenderer.tsx @@ -7,7 +7,7 @@ import { getHighlightSourceUrl, getHighlightComment, getHighlightContext, -} from "applesauce-core/helpers/highlight"; +} from "applesauce-common/helpers/highlight"; import { EmbeddedEvent } from "../EmbeddedEvent"; import { UserName } from "../UserName"; import { useGrimoire } from "@/core/state"; diff --git a/src/components/nostr/kinds/HighlightRenderer.tsx b/src/components/nostr/kinds/HighlightRenderer.tsx index 6c7dff4..89e1782 100644 --- a/src/components/nostr/kinds/HighlightRenderer.tsx +++ b/src/components/nostr/kinds/HighlightRenderer.tsx @@ -6,12 +6,12 @@ import { getHighlightComment, getHighlightSourceEventPointer, getHighlightSourceAddressPointer, -} from "applesauce-core/helpers/highlight"; +} from "applesauce-common/helpers/highlight"; import { UserName } from "../UserName"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { useGrimoire } from "@/core/state"; import { RichText } from "../RichText"; -import { getArticleTitle } from "applesauce-core/helpers"; +import { getArticleTitle } from "applesauce-common/helpers/article"; import { KindBadge } from "@/components/KindBadge"; /** diff --git a/src/components/nostr/kinds/Kind1111Renderer.tsx b/src/components/nostr/kinds/Kind1111Renderer.tsx index e036339..ec3de2e 100644 --- a/src/components/nostr/kinds/Kind1111Renderer.tsx +++ b/src/components/nostr/kinds/Kind1111Renderer.tsx @@ -5,7 +5,7 @@ import { isCommentAddressPointer, isCommentEventPointer, type CommentPointer, -} from "applesauce-core/helpers/comment"; +} from "applesauce-common/helpers/comment"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { UserName } from "../UserName"; import { Reply } from "lucide-react"; diff --git a/src/components/nostr/kinds/NoteRenderer.tsx b/src/components/nostr/kinds/NoteRenderer.tsx index 4fbde97..9614de7 100644 --- a/src/components/nostr/kinds/NoteRenderer.tsx +++ b/src/components/nostr/kinds/NoteRenderer.tsx @@ -1,7 +1,7 @@ import { RichText } from "../RichText"; import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer"; import { kinds } from "nostr-tools"; -import { getNip10References } from "applesauce-core/helpers/threading"; +import { getNip10References } from "applesauce-common/helpers/threading"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { UserName } from "../UserName"; import { Reply } from "lucide-react"; diff --git a/src/components/nostr/kinds/ReactionRenderer.tsx b/src/components/nostr/kinds/ReactionRenderer.tsx index 1d052a5..2f751e6 100644 --- a/src/components/nostr/kinds/ReactionRenderer.tsx +++ b/src/components/nostr/kinds/ReactionRenderer.tsx @@ -5,7 +5,7 @@ import { useMemo } from "react"; import { NostrEvent } from "@/types/nostr"; import { KindRenderer } from "./index"; import { EventCardSkeleton } from "@/components/ui/skeleton"; -import { parseCoordinate } from "applesauce-core/helpers/pointers"; +import { parseReplaceableAddress } from "applesauce-core/helpers/pointers"; /** * Renderer for Kind 7 - Reactions @@ -55,9 +55,9 @@ export function Kind7Renderer({ event }: BaseEventProps) { const aTag = event.tags.find((tag) => tag[0] === "a"); const reactedAddress = aTag?.[1]; // Format: kind:pubkey:d-tag - // Parse a tag coordinate using applesauce helper + // Parse a tag coordinate using applesauce helper (renamed in v5) const addressPointer = reactedAddress - ? parseCoordinate(reactedAddress) + ? parseReplaceableAddress(reactedAddress) : null; // Create event pointer for fetching diff --git a/src/components/nostr/kinds/ZapReceiptRenderer.tsx b/src/components/nostr/kinds/ZapReceiptRenderer.tsx index 6487e8a..3a08432 100644 --- a/src/components/nostr/kinds/ZapReceiptRenderer.tsx +++ b/src/components/nostr/kinds/ZapReceiptRenderer.tsx @@ -9,7 +9,7 @@ import { getZapAddressPointer, getZapSender, isValidZap, -} from "applesauce-core/helpers/zap"; +} from "applesauce-common/helpers/zap"; import { useNostrEvent } from "@/hooks/useNostrEvent"; import { KindRenderer } from "./index"; import { RichText } from "../RichText"; diff --git a/src/components/nostr/relay-pool.tsx b/src/components/nostr/relay-pool.tsx index 842b421..46c4a10 100644 --- a/src/components/nostr/relay-pool.tsx +++ b/src/components/nostr/relay-pool.tsx @@ -1,6 +1,6 @@ import { cn } from "@/lib/utils"; import pool from "@/services/relay-pool"; -import { useObservableMemo } from "applesauce-react/hooks"; +import { use$ } from "applesauce-react/hooks"; import { Relay } from "applesauce-relay"; import { Server, ServerOff } from "lucide-react"; @@ -19,7 +19,7 @@ function RelayItem({ relay }: { relay: Relay }) { } export default function RelayPool() { - const relays = useObservableMemo(() => pool.relays$, []); + const relays = use$(pool.relays$); return (
{Array.from(relays.entries()).map(([url, relay]) => ( diff --git a/src/components/nostr/user-menu.tsx b/src/components/nostr/user-menu.tsx index e466e79..bd05dc4 100644 --- a/src/components/nostr/user-menu.tsx +++ b/src/components/nostr/user-menu.tsx @@ -3,7 +3,7 @@ import accounts from "@/services/accounts"; import { ExtensionSigner } from "applesauce-signers"; import { ExtensionAccount } from "applesauce-accounts/accounts"; import { useProfile } from "@/hooks/useProfile"; -import { useObservableMemo } from "applesauce-react/hooks"; +import { use$ } from "applesauce-react/hooks"; import { getDisplayName } from "@/lib/nostr-utils"; import { useGrimoire } from "@/core/state"; import { Button } from "@/components/ui/button"; @@ -52,7 +52,7 @@ function UserLabel({ pubkey }: { pubkey: string }) { } export default function UserMenu() { - const account = useObservableMemo(() => accounts.active$, []); + const account = use$(accounts.active$); const { state, addWindow } = useGrimoire(); const relays = state.activeAccount?.relays; const [showSettings, setShowSettings] = useState(false); diff --git a/src/hooks/useAccountSync.ts b/src/hooks/useAccountSync.ts index 84dd6bc..fe4bd60 100644 --- a/src/hooks/useAccountSync.ts +++ b/src/hooks/useAccountSync.ts @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { useEventStore, useObservableMemo } from "applesauce-react/hooks"; +import { useEventStore, use$ } from "applesauce-react/hooks"; import accounts from "@/services/accounts"; import { useGrimoire } from "@/core/state"; import { addressLoader } from "@/services/loaders"; @@ -14,7 +14,7 @@ export function useAccountSync() { const eventStore = useEventStore(); // Watch active account from accounts service - const activeAccount = useObservableMemo(() => accounts.active$, []); + const activeAccount = use$(accounts.active$); // Sync active account pubkey to state useEffect(() => { diff --git a/src/hooks/useLiveTimeline.ts b/src/hooks/useLiveTimeline.ts index a641ce5..21d0b21 100644 --- a/src/hooks/useLiveTimeline.ts +++ b/src/hooks/useLiveTimeline.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import pool from "@/services/relay-pool"; import type { NostrEvent, Filter } from "nostr-tools"; -import { useEventStore, useObservableMemo } from "applesauce-react/hooks"; +import { useEventStore, use$ } from "applesauce-react/hooks"; import { isNostrEvent } from "@/lib/type-guards"; import { useStableValue, useStableArray } from "./useStable"; @@ -72,8 +72,7 @@ export function useLiveTimeline( })); const observable = pool.subscription(relays, filtersWithLimit, { - retries: 5, - reconnect: 5, + reconnect: 5, // v5: retries renamed to reconnect resubscribe: true, eventStore, // Automatically add events to store }); @@ -112,7 +111,7 @@ export function useLiveTimeline( }, [id, stableFilters, stableRelays, limit, stream, eventStore]); // 2. Observable Effect - Read from EventStore - const timelineEvents = useObservableMemo(() => { + const timelineEvents = use$(() => { // eventStore.timeline returns an Observable that emits sorted array of events matching filter // It updates whenever relevant events are added/removed from store return eventStore.timeline(filters); diff --git a/src/hooks/useNostrEvent.ts b/src/hooks/useNostrEvent.ts index e69bf69..b81bd8e 100644 --- a/src/hooks/useNostrEvent.ts +++ b/src/hooks/useNostrEvent.ts @@ -1,6 +1,6 @@ import { useEffect } from "react"; import type { EventPointer, AddressPointer } from "nostr-tools/nip19"; -import { useEventStore, useObservableMemo } from "applesauce-react/hooks"; +import { useEventStore, use$ } from "applesauce-react/hooks"; import { eventLoader, addressLoader } from "@/services/loaders"; import type { NostrEvent } from "@/types/nostr"; @@ -43,7 +43,7 @@ export function useNostrEvent( const eventStore = useEventStore(); // Watch event store for the specific event - const event = useObservableMemo(() => { + const event = use$(() => { if (!pointer) return undefined; // Handle string ID diff --git a/src/hooks/useReqTimeline.ts b/src/hooks/useReqTimeline.ts index 335a2ec..b8d15c7 100644 --- a/src/hooks/useReqTimeline.ts +++ b/src/hooks/useReqTimeline.ts @@ -75,8 +75,7 @@ export function useReqTimeline( })); const observable = pool.subscription(relays, filtersWithLimit, { - retries: 5, - reconnect: 5, + reconnect: 5, // v5: retries renamed to reconnect resubscribe: true, eventStore, }); diff --git a/src/hooks/useReqTimelineEnhanced.ts b/src/hooks/useReqTimelineEnhanced.ts index 9da1414..cda3c4e 100644 --- a/src/hooks/useReqTimelineEnhanced.ts +++ b/src/hooks/useReqTimelineEnhanced.ts @@ -193,8 +193,7 @@ export function useReqTimelineEnhanced( return relay .subscription(filtersWithLimit, { - retries: 5, - reconnect: 5, + reconnect: 5, // v5: retries renamed to reconnect resubscribe: true, }) .subscribe( diff --git a/src/hooks/useTimeline.ts b/src/hooks/useTimeline.ts index 63465f2..32585f3 100644 --- a/src/hooks/useTimeline.ts +++ b/src/hooks/useTimeline.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import type { NostrEvent, Filter } from "nostr-tools"; -import { useEventStore, useObservableMemo } from "applesauce-react/hooks"; +import { useEventStore, use$ } from "applesauce-react/hooks"; import { createTimelineLoader } from "@/services/loaders"; import pool from "@/services/relay-pool"; import { AGGREGATOR_RELAYS } from "@/services/loaders"; @@ -72,7 +72,7 @@ export function useTimeline( }, [id, stableRelays, limit, eventStore, stableFilters]); // Watch store for matching events - const timeline = useObservableMemo(() => { + const timeline = use$(() => { return eventStore.timeline(filters, false); }, [id]); diff --git a/src/lib/event-title.ts b/src/lib/event-title.ts index 426d3fd..86463b9 100644 --- a/src/lib/event-title.ts +++ b/src/lib/event-title.ts @@ -1,7 +1,7 @@ import { NostrEvent } from "@/types/nostr"; import { getTagValue } from "applesauce-core/helpers"; import { kinds } from "nostr-tools"; -import { getArticleTitle } from "applesauce-core/helpers/article"; +import { getArticleTitle } from "applesauce-common/helpers/article"; import { getRepositoryName, getIssueTitle, diff --git a/src/lib/nostr-utils.ts b/src/lib/nostr-utils.ts index b005177..91ce0a8 100644 --- a/src/lib/nostr-utils.ts +++ b/src/lib/nostr-utils.ts @@ -1,8 +1,8 @@ import type { ProfileContent } from "applesauce-core/helpers"; import type { NostrEvent } from "nostr-tools"; import type { NostrFilter } from "@/types/nostr"; -import { getNip10References } from "applesauce-core/helpers/threading"; -import { getCommentReplyPointer } from "applesauce-core/helpers/comment"; +import { getNip10References } from "applesauce-common/helpers/threading"; +import { getCommentReplyPointer } from "applesauce-common/helpers/comment"; import type { EventPointer, AddressPointer } from "nostr-tools/nip19"; export function derivePlaceholderName(pubkey: string): string { diff --git a/src/services/hub.ts b/src/services/hub.ts index dcbc33c..577457c 100644 --- a/src/services/hub.ts +++ b/src/services/hub.ts @@ -1,6 +1,6 @@ -import { ActionHub } from "applesauce-actions"; +import { ActionRunner } from "applesauce-actions"; import eventStore from "./event-store"; -import { EventFactory } from "applesauce-factory"; +import { EventFactory } from "applesauce-core/event-factory"; import pool from "./relay-pool"; import { relayListCache } from "./relay-list-cache"; import { getSeenRelays } from "applesauce-core/helpers/relays"; @@ -40,7 +40,7 @@ export async function publishEvent(event: NostrEvent): Promise { const factory = new EventFactory(); /** - * Global action hub for Grimoire + * Global action runner for Grimoire * Used to register and execute actions throughout the application * * Configured with: @@ -48,7 +48,7 @@ const factory = new EventFactory(); * - EventFactory: Creates and signs events * - publishEvent: Publishes events to author's outbox relays (with fallback to seen relays) */ -export const hub = new ActionHub(eventStore, factory, publishEvent); +export const hub = new ActionRunner(eventStore, factory, publishEvent); // Sync factory signer with active account // This ensures the hub can sign events when an account is active diff --git a/src/services/loaders.test.ts b/src/services/loaders.test.ts index 368335b..fbd5780 100644 --- a/src/services/loaders.test.ts +++ b/src/services/loaders.test.ts @@ -34,6 +34,7 @@ vi.mock("applesauce-loaders/loaders", () => ({ ), createAddressLoader: vi.fn(() => () => ({ subscribe: () => {} })), createTimelineLoader: vi.fn(), + createEventLoaderForStore: vi.fn(), })); import eventStore from "./event-store"; @@ -172,10 +173,18 @@ describe("eventLoader", () => { }); it("should extract relay hints from e tags", () => { + // Use valid 64-char hex event IDs (v5 validates event ID format) + const validEventId1 = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const validEventId2 = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + const validEventId3 = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + const event = createEventWithTags([ - ["e", "event-id-1", "wss://e-tag1.com/", "reply"], - ["e", "event-id-2", "wss://e-tag2.com/", "root"], - ["e", "event-id-3"], // No relay hint, should be skipped + ["e", validEventId1, "wss://e-tag1.com/", "reply"], + ["e", validEventId2, "wss://e-tag2.com/", "root"], + ["e", validEventId3], // No relay hint, should be skipped ]); const result = eventLoader({ id: "parent123" }, event); @@ -209,11 +218,15 @@ describe("eventLoader", () => { "wss://cached.com/", ]); + // Use valid 64-char hex event ID (v5 validates event ID format) + const validEventId = + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; + const event = createMockEvent({ tags: [ ["p", "author-pubkey"], ["r", "wss://r-tag.com/"], - ["e", "event-id", "wss://e-tag.com/"], + ["e", validEventId, "wss://e-tag.com/"], ], }); @@ -321,9 +334,13 @@ describe("eventLoader", () => { }); it("should handle invalid e tags gracefully", () => { + // Use valid 64-char hex event ID (v5 validates event ID format) + const validEventId = + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + const event = createEventWithTags([ - ["e"], // Missing event ID - ["e", "valid-id", "wss://valid.com/"], + ["e"], // Missing event ID - invalid, should be skipped + ["e", validEventId, "wss://valid.com/"], ]); const result = eventLoader({ id: "test123" }, event); diff --git a/src/services/loaders.ts b/src/services/loaders.ts index 9e485e3..fef398e 100644 --- a/src/services/loaders.ts +++ b/src/services/loaders.ts @@ -2,12 +2,13 @@ import { createEventLoader, createAddressLoader, createTimelineLoader, + createEventLoaderForStore, } from "applesauce-loaders/loaders"; import type { EventPointer } from "nostr-tools/nip19"; import { Observable } from "rxjs"; import { getSeenRelays, mergeRelaySets } from "applesauce-core/helpers/relays"; import { getEventPointerFromETag } from "applesauce-core/helpers/pointers"; -import { getTagValue } from "applesauce-core/helpers/event-tags"; +import { getTagValue } from "applesauce-core/helpers/event"; import pool from "./relay-pool"; import eventStore from "./event-store"; import { relayListCache } from "./relay-list-cache"; @@ -36,12 +37,9 @@ function extractRelayContext(event: NostrEvent): { const eTagRelays = event.tags .filter((t) => t[0] === "e") .map((tag) => { - try { - const pointer = getEventPointerFromETag(tag); - return pointer.relays?.[0]; // First relay hint from the pointer - } catch { - return undefined; // Invalid e tag, skip it - } + const pointer = getEventPointerFromETag(tag); + // v5: returns null for invalid tags instead of throwing + return pointer?.relays?.[0]; // First relay hint from the pointer }) .filter((relay): relay is string => relay !== undefined); @@ -183,3 +181,26 @@ export const profileLoader = createAddressLoader(pool, { // Timeline loader factory - creates loader for event feeds export { createTimelineLoader }; + +/** + * Setup unified event loader for automatic missing event loading + * + * This attaches a loader to the EventStore that automatically fetches + * missing events when they're requested via: + * - eventStore.event({ id: "..." }) + * - eventStore.replaceable({ kind, pubkey, identifier? }) + * + * The loader handles both single events and replaceable/addressable events + * through a single interface, with automatic routing based on pointer type. + * + * Configuration: + * - bufferTime: 200ms - batches requests for efficiency + * - extraRelays: AGGREGATOR_RELAYS - fallback relay discovery + * + * Note: The custom eventLoader() function above is still available for + * explicit loading with smart relay hint merging from context events. + */ +createEventLoaderForStore(eventStore, pool, { + bufferTime: 200, + extraRelays: AGGREGATOR_RELAYS, +}); diff --git a/src/services/relay-selection.test.ts b/src/services/relay-selection.test.ts index fca0201..b923b85 100644 --- a/src/services/relay-selection.test.ts +++ b/src/services/relay-selection.test.ts @@ -6,8 +6,34 @@ import { describe, it, expect, beforeEach } from "vitest"; import { selectRelaysForFilter } from "./relay-selection"; import { EventStore } from "applesauce-core"; import type { NostrEvent } from "nostr-tools"; +import { finalizeEvent, generateSecretKey, getPublicKey } from "nostr-tools"; import relayListCache from "./relay-list-cache"; +// Helper to create valid test events +function createRelayListEvent( + secretKey: Uint8Array, + tags: string[][], +): NostrEvent { + return finalizeEvent( + { + kind: 10002, + created_at: Math.floor(Date.now() / 1000), + tags, + content: "", + }, + secretKey, + ); +} + +// Generate valid test keys using generateSecretKey +const testSecretKeys: Uint8Array[] = []; +const testPubkeys: string[] = []; +for (let i = 0; i < 15; i++) { + const secretKey = generateSecretKey(); + testSecretKeys.push(secretKey); + testPubkeys.push(getPublicKey(secretKey)); +} + describe("selectRelaysForFilter", () => { let eventStore: EventStore; @@ -44,23 +70,14 @@ describe("selectRelaysForFilter", () => { describe("author relay selection", () => { it("should select write relays for authors", async () => { - const authorPubkey = - "32e18273f41e70f79a220d7fb69b36269d74d67f569b8c4b7fc17e5b1d1a1e3e"; + const authorPubkey = testPubkeys[0]; - // Mock kind:10002 event with write relays - const relayListEvent: NostrEvent = { - id: "test-event-id", - pubkey: authorPubkey, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ["r", "wss://relay.damus.io"], - ["r", "wss://nos.lol"], - ["r", "wss://relay.nostr.band", "read"], - ], - content: "", - sig: "test-sig", - }; + // Create valid kind:10002 event with write relays + const relayListEvent = createRelayListEvent(testSecretKeys[0], [ + ["r", "wss://relay.damus.io"], + ["r", "wss://nos.lol"], + ["r", "wss://relay.nostr.band", "read"], + ]); // Add to event store eventStore.add(relayListEvent); @@ -86,31 +103,19 @@ describe("selectRelaysForFilter", () => { }); it("should handle multiple authors", async () => { - const author1 = - "32e18273f41e70f79a220d7fb69b36269d74d67f569b8c4b7fc17e5b1d1a1e3e"; - const author2 = - "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2"; + const author1 = testPubkeys[0]; + const author2 = testPubkeys[1]; - // Mock relay lists for both authors - eventStore.add({ - id: "event1", - pubkey: author1, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [["r", "wss://relay.damus.io"]], - content: "", - sig: "sig1", - }); + // Create valid relay lists for both authors + eventStore.add( + createRelayListEvent(testSecretKeys[0], [ + ["r", "wss://relay.damus.io"], + ]), + ); - eventStore.add({ - id: "event2", - pubkey: author2, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [["r", "wss://nos.lol"]], - content: "", - sig: "sig2", - }); + eventStore.add( + createRelayListEvent(testSecretKeys[1], [["r", "wss://nos.lol"]]), + ); const result = await selectRelaysForFilter( eventStore, @@ -130,23 +135,14 @@ describe("selectRelaysForFilter", () => { describe("p-tag relay selection", () => { it("should select read relays for #p tags", async () => { - const mentionedPubkey = - "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2"; + const mentionedPubkey = testPubkeys[2]; - // Mock kind:10002 event with read relays - const relayListEvent: NostrEvent = { - id: "test-event-id", - pubkey: mentionedPubkey, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ["r", "wss://relay.damus.io", "write"], - ["r", "wss://nos.lol", "read"], - ["r", "wss://relay.nostr.band", "read"], - ], - content: "", - sig: "test-sig", - }; + // Create valid kind:10002 event with read relays + const relayListEvent = createRelayListEvent(testSecretKeys[2], [ + ["r", "wss://relay.damus.io", "write"], + ["r", "wss://nos.lol", "read"], + ["r", "wss://relay.nostr.band", "read"], + ]); eventStore.add(relayListEvent); @@ -173,32 +169,22 @@ describe("selectRelaysForFilter", () => { describe("mixed authors and #p tags", () => { it("should combine outbox and inbox relays", async () => { - const author = - "32e18273f41e70f79a220d7fb69b36269d74d67f569b8c4b7fc17e5b1d1a1e3e"; - const mentioned = - "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2"; + const author = testPubkeys[3]; + const mentioned = testPubkeys[4]; // Author has write relays - eventStore.add({ - id: "event1", - pubkey: author, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [["r", "wss://author-relay.com"]], - content: "", - sig: "sig1", - }); + eventStore.add( + createRelayListEvent(testSecretKeys[3], [ + ["r", "wss://author-relay.com"], + ]), + ); // Mentioned user has read relays - eventStore.add({ - id: "event2", - pubkey: mentioned, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [["r", "wss://mention-relay.com", "read"]], - content: "", - sig: "sig2", - }); + eventStore.add( + createRelayListEvent(testSecretKeys[4], [ + ["r", "wss://mention-relay.com", "read"], + ]), + ); const result = await selectRelaysForFilter( eventStore, @@ -229,56 +215,36 @@ describe("selectRelaysForFilter", () => { }); it("should maintain diversity with multiple authors and p-tags", async () => { - const author1 = - "32e18273f41e70f79a220d7fb69b36269d74d67f569b8c4b7fc17e5b1d1a1e3e"; - const author2 = - "42e18273f41e70f79a220d7fb69b36269d74d67f569b8c4b7fc17e5b1d1a1e3e"; - const mentioned1 = - "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2"; - const mentioned2 = - "92341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2"; + const author1 = testPubkeys[5]; + const author2 = testPubkeys[6]; + const mentioned1 = testPubkeys[7]; + const mentioned2 = testPubkeys[8]; // Authors have write relays - eventStore.add({ - id: "event1", - pubkey: author1, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [["r", "wss://author1-relay.com"]], - content: "", - sig: "sig1", - }); + eventStore.add( + createRelayListEvent(testSecretKeys[5], [ + ["r", "wss://author1-relay.com"], + ]), + ); - eventStore.add({ - id: "event2", - pubkey: author2, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [["r", "wss://author2-relay.com"]], - content: "", - sig: "sig2", - }); + eventStore.add( + createRelayListEvent(testSecretKeys[6], [ + ["r", "wss://author2-relay.com"], + ]), + ); // Mentioned users have read relays - eventStore.add({ - id: "event3", - pubkey: mentioned1, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [["r", "wss://mention1-relay.com", "read"]], - content: "", - sig: "sig3", - }); + eventStore.add( + createRelayListEvent(testSecretKeys[7], [ + ["r", "wss://mention1-relay.com", "read"], + ]), + ); - eventStore.add({ - id: "event4", - pubkey: mentioned2, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [["r", "wss://mention2-relay.com", "read"]], - content: "", - sig: "sig4", - }); + eventStore.add( + createRelayListEvent(testSecretKeys[8], [ + ["r", "wss://mention2-relay.com", "read"], + ]), + ); const result = await selectRelaysForFilter( eventStore, @@ -314,22 +280,15 @@ describe("selectRelaysForFilter", () => { describe("relay limits", () => { it("should respect maxRelays limit", async () => { - // Create many authors with different relays + // Create many authors with different relays (use first 10 test keys) const authors = Array.from({ length: 10 }, (_, i) => ({ - pubkey: `pubkey${i}`.padEnd(64, "0"), + secretKey: testSecretKeys[i], + pubkey: testPubkeys[i], relay: `wss://relay${i}.com`, })); - authors.forEach(({ pubkey, relay }) => { - eventStore.add({ - id: `event-${pubkey}`, - pubkey, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [["r", relay]], - content: "", - sig: "sig", - }); + authors.forEach(({ secretKey, relay }) => { + eventStore.add(createRelayListEvent(secretKey, [["r", relay]])); }); const result = await selectRelaysForFilter( @@ -347,8 +306,8 @@ describe("selectRelaysForFilter", () => { describe("edge cases", () => { it("should handle users with no relay lists", async () => { - const pubkeyWithoutList = - "32e18273f41e70f79a220d7fb69b36269d74d67f569b8c4b7fc17e5b1d1a1e3e"; + // Use a pubkey that doesn't have a relay list added + const pubkeyWithoutList = testPubkeys[14]; const result = await selectRelaysForFilter( eventStore, @@ -363,22 +322,15 @@ describe("selectRelaysForFilter", () => { }); it("should handle invalid relay URLs gracefully", async () => { - const pubkey = - "32e18273f41e70f79a220d7fb69b36269d74d67f569b8c4b7fc17e5b1d1a1e3e"; + const pubkey = testPubkeys[10]; // Add relay list with invalid URL - eventStore.add({ - id: "event", - pubkey, - kind: 10002, - created_at: Math.floor(Date.now() / 1000), - tags: [ + eventStore.add( + createRelayListEvent(testSecretKeys[10], [ ["r", "not-a-valid-url"], ["r", "wss://valid-relay.com"], - ], - content: "", - sig: "sig", - }); + ]), + ); const result = await selectRelaysForFilter( eventStore, diff --git a/tailwind.config.js b/tailwind.config.js index 8795f69..23c5e58 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,83 +1,79 @@ /** @type {import('tailwindcss').Config} */ export default { - darkMode: ['class'], - content: [ - './index.html', - './src/**/*.{js,ts,jsx,tsx}', - ], + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: { fontFamily: { - mono: ['"Oxygen Mono"', 'monospace'], + mono: ['"Oxygen Mono"', "monospace"], }, borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", }, keyframes: { - 'accordion-down': { - from: { height: '0' }, - to: { height: 'var(--radix-accordion-content-height)' } + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, }, - 'accordion-up': { - from: { height: 'var(--radix-accordion-content-height)' }, - to: { height: '0' } + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + "skeleton-pulse": { + "0%, 100%": { opacity: "1" }, + "50%": { opacity: "0.5" }, }, - 'skeleton-pulse': { - '0%, 100%': { opacity: '1' }, - '50%': { opacity: '0.5' } - } }, animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out', - 'skeleton-pulse': 'skeleton-pulse 1.5s ease-in-out infinite' + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + "skeleton-pulse": "skeleton-pulse 1.5s ease-in-out infinite", }, colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", }, popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", }, primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", }, secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", }, muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", }, accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", }, destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - } - } - } + 1: "hsl(var(--chart-1))", + 2: "hsl(var(--chart-2))", + 3: "hsl(var(--chart-3))", + 4: "hsl(var(--chart-4))", + 5: "hsl(var(--chart-5))", + }, + }, + }, }, plugins: [], -} - +}; diff --git a/vercel.json b/vercel.json index 6a92648..3a48e56 100644 --- a/vercel.json +++ b/vercel.json @@ -1,5 +1,3 @@ { - "rewrites": [ - { "source": "/(.*)", "destination": "/" } - ] + "rewrites": [{ "source": "/(.*)", "destination": "/" }] }