Add NIP-85 stats integration with fallback to manual calculation
- Add nip85StatsPubkey to AppConfig (default: 5f68e85ee174102ca8978eef302129f081f03456c884185d5ec1c1224ab633ea) - Create useNip85Stats hook for querying NIP-85 events (kinds 30382, 30383, 30384) - Update useEventInteractions to fetch NIP-85 event stats and reduce query limits when available - Update useComments to fetch NIP-85 stats for events and addressable events - Add NIP-85 Stats Source section to Advanced Settings with pubkey input field - Falls back to manual stats calculation when NIP-85 events are not available
This commit is contained in:
Generated
+20
-7
@@ -11,7 +11,6 @@
|
||||
"@fontsource-variable/inter": "^5.2.6",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lucide/lab": "^0.1.2",
|
||||
"@nostrify/nostrify": "^0.50.0",
|
||||
"@nostrify/react": "^0.2.25",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
@@ -1176,12 +1175,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lucide/lab": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@lucide/lab/-/lab-0.1.2.tgz",
|
||||
"integrity": "sha512-VprF2BJa7ZuTGOhUd5cf8tHJXyL63wdxcGieAiVVoR9hO0YmPsnZO0AGqDiX2/br+/MC6n8BoJcmPilltOXIJA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
|
||||
@@ -2741,6 +2734,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2754,6 +2748,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2767,6 +2762,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2780,6 +2776,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2793,6 +2790,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2806,6 +2804,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2819,6 +2818,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2832,6 +2832,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2845,6 +2846,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2858,6 +2860,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2871,6 +2874,7 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2884,6 +2888,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2897,6 +2902,7 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2910,6 +2916,7 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2923,6 +2930,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2936,6 +2944,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2949,6 +2958,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2962,6 +2972,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2975,6 +2986,7 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2988,6 +3000,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -53,6 +53,7 @@ const defaultConfig: AppConfig = {
|
||||
feedIncludeColors: false,
|
||||
feedIncludePacks: false,
|
||||
},
|
||||
nip85StatsPubkey: "5f68e85ee174102ca8978eef302129f081f03456c884185d5ec1c1224ab633ea",
|
||||
};
|
||||
|
||||
export function App() {
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronDown, ChevronUp, Wrench } from 'lucide-react';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { WalletSettings } from '@/components/WalletSettings';
|
||||
import { RelayListManager } from '@/components/RelayListManager';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
|
||||
export function AdvancedSettings() {
|
||||
const { user } = useCurrentUser();
|
||||
const { config, updateConfig } = useAppContext();
|
||||
const { toast } = useToast();
|
||||
const [walletOpen, setWalletOpen] = useState(false);
|
||||
const [relaysOpen, setRelaysOpen] = useState(false);
|
||||
const [statsOpen, setStatsOpen] = useState(false);
|
||||
const [statsPubkey, setStatsPubkey] = useState(config.nip85StatsPubkey);
|
||||
|
||||
const handleStatsPubkeyChange = (value: string) => {
|
||||
setStatsPubkey(value);
|
||||
|
||||
// Validate hex format (64 characters)
|
||||
if (value.length === 64 && /^[0-9a-f]{64}$/i.test(value)) {
|
||||
updateConfig(() => ({ nip85StatsPubkey: value.toLowerCase() }));
|
||||
toast({
|
||||
title: 'Stats source updated',
|
||||
description: 'NIP-85 stats pubkey has been saved.',
|
||||
});
|
||||
} else if (value.length === 0) {
|
||||
// Allow clearing the field
|
||||
updateConfig(() => ({ nip85StatsPubkey: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -76,6 +100,57 @@ export function AdvancedSettings() {
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
{/* Stats Source Section */}
|
||||
<div>
|
||||
<Collapsible open={statsOpen} onOpenChange={setStatsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between px-3 py-3.5 h-auto hover:bg-muted/20 rounded-none border-b-[4px] border-primary"
|
||||
>
|
||||
<span className="text-base font-semibold">Stats Source</span>
|
||||
{statsOpen ? (
|
||||
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="pb-4 pt-4 px-3 space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="stats-pubkey" className="text-sm font-medium">
|
||||
NIP-85 Stats Pubkey
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1 mb-2">
|
||||
Enter a trusted pubkey for pre-computed engagement stats (likes, reposts, comments).
|
||||
Leave empty to always calculate stats manually.
|
||||
</p>
|
||||
<Input
|
||||
id="stats-pubkey"
|
||||
value={statsPubkey}
|
||||
onChange={(e) => handleStatsPubkeyChange(e.target.value)}
|
||||
placeholder="Enter 64-character hex pubkey"
|
||||
className="font-mono text-sm"
|
||||
maxLength={64}
|
||||
/>
|
||||
{statsPubkey && statsPubkey.length !== 64 && (
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
Pubkey must be exactly 64 hexadecimal characters
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p className="font-medium">Default pubkey:</p>
|
||||
<p className="font-mono break-all">
|
||||
5f68e85ee174102ca8978eef302129f081f03456c884185d5ec1c1224ab633ea
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ const AppConfigSchema = z.object({
|
||||
relayMetadata: RelayMetadataSchema,
|
||||
useAppRelays: z.boolean(),
|
||||
feedSettings: FeedSettingsSchema,
|
||||
nip85StatsPubkey: z.string().length(64),
|
||||
}) satisfies z.ZodType<AppConfig>;
|
||||
|
||||
export function AppProvider(props: AppProviderProps) {
|
||||
|
||||
@@ -48,6 +48,8 @@ export interface AppConfig {
|
||||
useAppRelays: boolean;
|
||||
/** Feed and sidebar content settings */
|
||||
feedSettings: FeedSettings;
|
||||
/** NIP-85 stats pubkey source (hex format) */
|
||||
nip85StatsPubkey: string;
|
||||
}
|
||||
|
||||
export interface AppContextType {
|
||||
|
||||
@@ -1,13 +1,41 @@
|
||||
import { NKinds, NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNip85EventStats, useNip85AddrStats } from '@/hooks/useNip85Stats';
|
||||
|
||||
export function useComments(root: NostrEvent | URL, limit?: number) {
|
||||
const { nostr } = useNostr();
|
||||
|
||||
// Determine the identifier for NIP-85 stats
|
||||
let nip85Identifier: string | undefined;
|
||||
let nip85Type: 'event' | 'addr' | undefined;
|
||||
|
||||
if (root instanceof URL) {
|
||||
// URLs don't have NIP-85 stats
|
||||
nip85Identifier = undefined;
|
||||
} else if (NKinds.addressable(root.kind)) {
|
||||
const d = root.tags.find(([name]) => name === 'd')?.[1] ?? '';
|
||||
nip85Identifier = `${root.kind}:${root.pubkey}:${d}`;
|
||||
nip85Type = 'addr';
|
||||
} else if (NKinds.replaceable(root.kind)) {
|
||||
nip85Identifier = `${root.kind}:${root.pubkey}:`;
|
||||
nip85Type = 'addr';
|
||||
} else {
|
||||
nip85Identifier = root.id;
|
||||
nip85Type = 'event';
|
||||
}
|
||||
|
||||
const nip85EventStats = useNip85EventStats(nip85Type === 'event' ? nip85Identifier : undefined);
|
||||
const nip85AddrStats = useNip85AddrStats(nip85Type === 'addr' ? nip85Identifier : undefined);
|
||||
const nip85Stats = nip85Type === 'event' ? nip85EventStats.data : nip85AddrStats.data;
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['nostr', 'comments', root instanceof URL ? root.toString() : root.id, limit],
|
||||
queryKey: ['nostr', 'comments', root instanceof URL ? root.toString() : root.id, limit, nip85Stats],
|
||||
queryFn: async (c) => {
|
||||
// If we have NIP-85 comment count and no limit specified, use a smaller default limit
|
||||
const hasNip85CommentCount = !!nip85Stats?.commentCount;
|
||||
const effectiveLimit = limit ?? (hasNip85CommentCount ? 10 : undefined);
|
||||
|
||||
const filter: NostrFilter = { kinds: [1111] };
|
||||
|
||||
if (root instanceof URL) {
|
||||
@@ -21,8 +49,8 @@ export function useComments(root: NostrEvent | URL, limit?: number) {
|
||||
filter['#E'] = [root.id];
|
||||
}
|
||||
|
||||
if (typeof limit === 'number') {
|
||||
filter.limit = limit;
|
||||
if (typeof effectiveLimit === 'number') {
|
||||
filter.limit = effectiveLimit;
|
||||
}
|
||||
|
||||
// Query for all kind 1111 comments that reference this addressable event regardless of depth
|
||||
@@ -90,7 +118,8 @@ export function useComments(root: NostrEvent | URL, limit?: number) {
|
||||
});
|
||||
// Sort direct replies by creation time (oldest first for threaded display)
|
||||
return directReplies.sort((a, b) => a.created_at - b.created_at);
|
||||
}
|
||||
},
|
||||
nip85Stats,
|
||||
};
|
||||
},
|
||||
enabled: !!root,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import { useNip85EventStats } from '@/hooks/useNip85Stats';
|
||||
|
||||
export interface RepostEntry {
|
||||
pubkey: string;
|
||||
@@ -32,6 +33,13 @@ export interface EventInteractions {
|
||||
quotes: QuoteEntry[];
|
||||
reactions: ReactionEntry[];
|
||||
zaps: ZapEntry[];
|
||||
/** NIP-85 stats if available */
|
||||
nip85Stats?: {
|
||||
commentCount: number;
|
||||
repostCount: number;
|
||||
reactionCount: number;
|
||||
zapCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** Extracts the zap amount in millisatoshis from a kind 9735 zap receipt. */
|
||||
@@ -117,20 +125,26 @@ function extractZapMessage(event: NostrEvent): string {
|
||||
/** Fetches interaction events (reposts, quotes, reactions, zaps) for a given event ID. */
|
||||
export function useEventInteractions(eventId: string | undefined) {
|
||||
const { nostr } = useNostr();
|
||||
const nip85Stats = useNip85EventStats(eventId);
|
||||
|
||||
return useQuery<EventInteractions>({
|
||||
queryKey: ['event-interactions', eventId ?? ''],
|
||||
queryKey: ['event-interactions', eventId ?? '', nip85Stats.data],
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!eventId) return { reposts: [], quotes: [], reactions: [], zaps: [] };
|
||||
|
||||
// Try NIP-85 stats first - if available, use a smaller limit for actual events
|
||||
const hasNip85Stats = !!nip85Stats.data;
|
||||
const interactionLimit = hasNip85Stats ? 10 : 50;
|
||||
const quoteLimit = hasNip85Stats ? 5 : 20;
|
||||
|
||||
const timeout = AbortSignal.timeout(5000);
|
||||
const combined = AbortSignal.any([signal, timeout]);
|
||||
|
||||
// Single query with two filter objects — relay handles as OR
|
||||
const allEvents = await nostr.query(
|
||||
[
|
||||
{ kinds: [6, 7, 9735], '#e': [eventId], limit: 50 },
|
||||
{ kinds: [1], '#q': [eventId], limit: 20 },
|
||||
{ kinds: [6, 7, 9735], '#e': [eventId], limit: interactionLimit },
|
||||
{ kinds: [1], '#q': [eventId], limit: quoteLimit },
|
||||
],
|
||||
{ signal: combined },
|
||||
);
|
||||
@@ -191,7 +205,13 @@ export function useEventInteractions(eventId: string | undefined) {
|
||||
reactions.sort((a, b) => b.createdAt - a.createdAt);
|
||||
zaps.sort((a, b) => b.amountSats - a.amountSats); // Sort zaps by amount (largest first)
|
||||
|
||||
return { reposts, quotes, reactions, zaps };
|
||||
return {
|
||||
reposts,
|
||||
quotes,
|
||||
reactions,
|
||||
zaps,
|
||||
nip85Stats: nip85Stats.data,
|
||||
};
|
||||
},
|
||||
enabled: !!eventId,
|
||||
staleTime: 60 * 1000,
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
|
||||
export interface Nip85EventStats {
|
||||
commentCount: number;
|
||||
repostCount: number;
|
||||
reactionCount: number;
|
||||
zapCount: number;
|
||||
}
|
||||
|
||||
export interface Nip85UserStats {
|
||||
followers: number;
|
||||
postCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches NIP-85 event stats (kind 30383) from the configured stats pubkey.
|
||||
* Returns undefined if no stats are available.
|
||||
*/
|
||||
export function useNip85EventStats(eventId: string | undefined) {
|
||||
const { nostr } = useNostr();
|
||||
const { config } = useAppContext();
|
||||
const statsPubkey = config.nip85StatsPubkey;
|
||||
|
||||
return useQuery<Nip85EventStats | undefined>({
|
||||
queryKey: ['nip85-event-stats', eventId, statsPubkey],
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!eventId || !statsPubkey) return undefined;
|
||||
|
||||
const timeout = AbortSignal.timeout(2000);
|
||||
const combined = AbortSignal.any([signal, timeout]);
|
||||
|
||||
try {
|
||||
const events = await nostr.query(
|
||||
[
|
||||
{
|
||||
kinds: [30383],
|
||||
authors: [statsPubkey],
|
||||
'#d': [eventId],
|
||||
limit: 1,
|
||||
},
|
||||
],
|
||||
{ signal: combined },
|
||||
);
|
||||
|
||||
if (events.length === 0) return undefined;
|
||||
|
||||
const event = events[0];
|
||||
const getTagValue = (tagName: string): number => {
|
||||
const tag = event.tags.find(([name]) => name === tagName);
|
||||
return tag?.[1] ? parseInt(tag[1], 10) : 0;
|
||||
};
|
||||
|
||||
return {
|
||||
commentCount: getTagValue('comment_cnt'),
|
||||
repostCount: getTagValue('repost_cnt'),
|
||||
reactionCount: getTagValue('reaction_cnt'),
|
||||
zapCount: getTagValue('zap_cnt'),
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
enabled: !!eventId && !!statsPubkey,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
retry: false, // Don't retry on failure, fall back to manual calculation
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches NIP-85 user stats (kind 30382) from the configured stats pubkey.
|
||||
* Returns undefined if no stats are available.
|
||||
*/
|
||||
export function useNip85UserStats(pubkey: string | undefined) {
|
||||
const { nostr } = useNostr();
|
||||
const { config } = useAppContext();
|
||||
const statsPubkey = config.nip85StatsPubkey;
|
||||
|
||||
return useQuery<Nip85UserStats | undefined>({
|
||||
queryKey: ['nip85-user-stats', pubkey, statsPubkey],
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!pubkey || !statsPubkey) return undefined;
|
||||
|
||||
const timeout = AbortSignal.timeout(2000);
|
||||
const combined = AbortSignal.any([signal, timeout]);
|
||||
|
||||
try {
|
||||
const events = await nostr.query(
|
||||
[
|
||||
{
|
||||
kinds: [30382],
|
||||
authors: [statsPubkey],
|
||||
'#d': [pubkey],
|
||||
limit: 1,
|
||||
},
|
||||
],
|
||||
{ signal: combined },
|
||||
);
|
||||
|
||||
if (events.length === 0) return undefined;
|
||||
|
||||
const event = events[0];
|
||||
const getTagValue = (tagName: string): number => {
|
||||
const tag = event.tags.find(([name]) => name === tagName);
|
||||
return tag?.[1] ? parseInt(tag[1], 10) : 0;
|
||||
};
|
||||
|
||||
return {
|
||||
followers: getTagValue('followers'),
|
||||
postCount: getTagValue('post_cnt'),
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
enabled: !!pubkey && !!statsPubkey,
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
retry: false, // Don't retry on failure, fall back to manual calculation
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches NIP-85 addressable event stats (kind 30384) from the configured stats pubkey.
|
||||
* Returns undefined if no stats are available.
|
||||
*/
|
||||
export function useNip85AddrStats(addr: string | undefined) {
|
||||
const { nostr } = useNostr();
|
||||
const { config } = useAppContext();
|
||||
const statsPubkey = config.nip85StatsPubkey;
|
||||
|
||||
return useQuery<Nip85EventStats | undefined>({
|
||||
queryKey: ['nip85-addr-stats', addr, statsPubkey],
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!addr || !statsPubkey) return undefined;
|
||||
|
||||
const timeout = AbortSignal.timeout(2000);
|
||||
const combined = AbortSignal.any([signal, timeout]);
|
||||
|
||||
try {
|
||||
const events = await nostr.query(
|
||||
[
|
||||
{
|
||||
kinds: [30384],
|
||||
authors: [statsPubkey],
|
||||
'#d': [addr],
|
||||
limit: 1,
|
||||
},
|
||||
],
|
||||
{ signal: combined },
|
||||
);
|
||||
|
||||
if (events.length === 0) return undefined;
|
||||
|
||||
const event = events[0];
|
||||
const getTagValue = (tagName: string): number => {
|
||||
const tag = event.tags.find(([name]) => name === tagName);
|
||||
return tag?.[1] ? parseInt(tag[1], 10) : 0;
|
||||
};
|
||||
|
||||
return {
|
||||
commentCount: getTagValue('comment_cnt'),
|
||||
repostCount: getTagValue('repost_cnt'),
|
||||
reactionCount: getTagValue('reaction_cnt'),
|
||||
zapCount: getTagValue('zap_cnt'),
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
enabled: !!addr && !!statsPubkey,
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
retry: false, // Don't retry on failure, fall back to manual calculation
|
||||
});
|
||||
}
|
||||
@@ -45,6 +45,7 @@ export function TestApp({ children }: TestAppProps) {
|
||||
feedIncludeColors: false,
|
||||
feedIncludePacks: false,
|
||||
},
|
||||
nip85StatsPubkey: '5f68e85ee174102ca8978eef302129f081f03456c884185d5ec1c1224ab633ea',
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user