mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 23:47:12 +02:00
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
This commit is contained in:
290
.claude/skills/applesauce-common/SKILL.md
Normal file
290
.claude/skills/applesauce-common/SKILL.md
Normal file
@@ -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 (
|
||||
<div>
|
||||
<span>{profile?.name}</span>
|
||||
<p>{note.content}</p>
|
||||
<span>{replies?.length} replies</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
@@ -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
|
||||
|
||||
43
CLAUDE.md
43
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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
);
|
||||
|
||||
320
package-lock.json
generated
320
package-lock.json
generated
@@ -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",
|
||||
|
||||
17
package.json
17
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",
|
||||
|
||||
@@ -3,4 +3,4 @@ export default {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -31,6 +31,12 @@ vi.mock("@/services/relay-list-cache", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/services/event-store", () => ({
|
||||
default: {
|
||||
add: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("PublishSpellAction", () => {
|
||||
let action: PublishSpellAction;
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<typeof PublishSpellbook>[0],
|
||||
): Promise<NostrEvent[]> {
|
||||
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,
|
||||
}),
|
||||
|
||||
@@ -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<NostrEvent> {
|
||||
sign,
|
||||
publish,
|
||||
}: ActionContext): Promise<void> {
|
||||
// 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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, "")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col gap-1">
|
||||
{Array.from(relays.entries()).map(([url, relay]) => (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<void> {
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [],
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{ "source": "/(.*)", "destination": "/" }
|
||||
]
|
||||
"rewrites": [{ "source": "/(.*)", "destination": "/" }]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user