diff --git a/src/App.tsx b/src/App.tsx index a8b840ca..0cd08fbb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,7 +54,7 @@ const defaultConfig: AppConfig = { feedIncludePacks: false, }, nip85StatsPubkey: "5f68e85ee174102ca8978eef302129f081f03456c884185d5ec1c1224ab633ea", - nip85OnlyMode: true, + statsMode: "nip85-only", }; export function App() { diff --git a/src/components/AdvancedSettings.tsx b/src/components/AdvancedSettings.tsx index 4b49e970..bb9a5c03 100644 --- a/src/components/AdvancedSettings.tsx +++ b/src/components/AdvancedSettings.tsx @@ -4,12 +4,13 @@ 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 { Switch } from '@/components/ui/switch'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; 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'; +import type { StatsMode } from '@/contexts/AppContext'; export function AdvancedSettings() { const { user } = useCurrentUser(); @@ -153,28 +154,70 @@ export function AdvancedSettings() { -
-
- NIP-85 Only Mode -

- Disable manual stat calculation. Stats will only show if NIP-85 pubkey provides them. +

+
+ +

+ Choose how engagement stats are calculated.

- { - updateConfig(() => ({ nip85OnlyMode: checked })); + { + updateConfig(() => ({ statsMode: value })); + const descriptions = { + 'nip85-only': 'Only NIP-85 pre-computed stats will be shown.', + 'manual-only': 'Stats will be calculated manually from relay queries.', + 'both': 'NIP-85 stats with manual fallback when unavailable.', + }; toast({ - title: checked ? 'NIP-85 only mode enabled' : 'NIP-85 only mode disabled', - description: checked - ? 'Manual stat calculation is disabled.' - : 'Manual stat calculation will be used as fallback.', + title: 'Stats mode updated', + description: descriptions[value], }); }} - disabled={!statsPubkey} - className="scale-90" - /> + disabled={!statsPubkey && config.statsMode !== 'manual-only'} + className="gap-3" + > +
+ +
+ +

+ Show only pre-computed stats. Faster, but may be empty if NIP-85 source is unavailable. +

+
+
+
+ +
+ +

+ Always calculate stats from relay queries. Slower, but guaranteed to work. +

+
+
+
+ +
+ +

+ Use NIP-85 when available, fall back to manual calculation. Best balance of speed and reliability. +

+
+
+
diff --git a/src/components/AppProvider.tsx b/src/components/AppProvider.tsx index 9ed3a2e3..42a6998a 100644 --- a/src/components/AppProvider.tsx +++ b/src/components/AppProvider.tsx @@ -48,7 +48,7 @@ const AppConfigSchema = z.object({ (val) => val.length === 0 || (val.length === 64 && /^[0-9a-f]{64}$/i.test(val)), { message: 'Must be empty or a valid 64-character hex pubkey' } ), - nip85OnlyMode: z.boolean(), + statsMode: z.enum(['nip85-only', 'manual-only', 'both']), }) satisfies z.ZodType; export function AppProvider(props: AppProviderProps) { diff --git a/src/contexts/AppContext.ts b/src/contexts/AppContext.ts index 21e4c694..8e7cd35a 100644 --- a/src/contexts/AppContext.ts +++ b/src/contexts/AppContext.ts @@ -39,6 +39,9 @@ export interface FeedSettings { feedIncludePacks: boolean; } +/** Stats calculation mode: NIP-85 only, manual only, or both */ +export type StatsMode = "nip85-only" | "manual-only" | "both"; + export interface AppConfig { /** Current theme */ theme: Theme; @@ -50,8 +53,8 @@ export interface AppConfig { feedSettings: FeedSettings; /** NIP-85 stats pubkey source (hex format) */ nip85StatsPubkey: string; - /** Whether to disable manual stat calculation fallback (NIP-85 only mode) */ - nip85OnlyMode: boolean; + /** Stats calculation mode: NIP-85 only, manual only, or both */ + statsMode: StatsMode; } export interface AppContextType { diff --git a/src/hooks/useTrending.ts b/src/hooks/useTrending.ts index 26b65b8b..e71993b6 100644 --- a/src/hooks/useTrending.ts +++ b/src/hooks/useTrending.ts @@ -259,13 +259,26 @@ export function useEventStats(eventId: string | undefined) { const { nostr } = useNostr(); const { config } = useAppContext(); const statsPubkey = config.nip85StatsPubkey; - const nip85OnlyMode = config.nip85OnlyMode; + const statsMode = config.statsMode; return useQuery({ queryKey: ['event-stats', eventId ?? ''], queryFn: async ({ signal }) => { if (!eventId) return EMPTY_STATS; + // Manual-only mode: skip NIP-85 entirely + if (statsMode === 'manual-only') { + const combined = AbortSignal.any([signal, AbortSignal.timeout(5000)]); + const events = await nostr.query( + [ + { kinds: [1, 6, 7, 9735], '#e': [eventId], limit: 50 }, + { kinds: [1], '#q': [eventId], limit: 20 }, + ], + { signal: combined }, + ); + return computeStats(eventId, events); + } + // Try NIP-85 first with aggressive timeout (500ms) let nip85Stats: NostrEvent[] = []; if (statsPubkey) { @@ -287,7 +300,7 @@ export function useEventStats(eventId: string | undefined) { const hasNip85 = nip85Stats.length > 0; // If NIP-85 only mode is enabled and we don't have NIP-85 stats, return empty - if (nip85OnlyMode && !hasNip85) { + if (statsMode === 'nip85-only' && !hasNip85) { return EMPTY_STATS; } @@ -426,7 +439,7 @@ export function useBatchEventStats(eventIds: string[], enabled = true) { const queryClient = useQueryClient(); const { config } = useAppContext(); const statsPubkey = config.nip85StatsPubkey; - const nip85OnlyMode = config.nip85OnlyMode; + const statsMode = config.statsMode; const uniqueIds = [...new Set(eventIds)].sort(); @@ -435,6 +448,26 @@ export function useBatchEventStats(eventIds: string[], enabled = true) { queryFn: async ({ signal }) => { if (uniqueIds.length === 0) return new Map(); + // Manual-only mode: skip NIP-85 entirely + if (statsMode === 'manual-only') { + const combined = AbortSignal.any([signal, AbortSignal.timeout(6000)]); + const events = await nostr.query( + [ + { kinds: [1, 6, 7, 9735], '#e': uniqueIds, limit: uniqueIds.length * 10 }, + { kinds: [1], '#q': uniqueIds, limit: uniqueIds.length * 3 }, + ], + { signal: combined }, + ); + + const statsMap = new Map(); + for (const id of uniqueIds) { + const stats = computeStats(id, events); + statsMap.set(id, stats); + queryClient.setQueryData(['event-stats', id], stats); + } + return statsMap; + } + // Try NIP-85 first with aggressive timeout (500ms) const nip85StatsMap = new Map(); @@ -474,7 +507,7 @@ export function useBatchEventStats(eventIds: string[], enabled = true) { const hasAnyNip85Stats = nip85StatsMap.size > 0; // If NIP-85 only mode is enabled and we have no stats, return empty map - if (nip85OnlyMode && !hasAnyNip85Stats) { + if (statsMode === 'nip85-only' && !hasAnyNip85Stats) { const emptyMap = new Map(); for (const id of uniqueIds) { emptyMap.set(id, EMPTY_STATS);