Add three-mode stats calculation: NIP-85 Only, Manual Only, and Both
Replace the binary nip85OnlyMode boolean with a statsMode enum that supports three options: - "nip85-only": Show only pre-computed NIP-85 stats (fastest, may be empty) - "manual-only": Always calculate stats from relay queries (slower, guaranteed) - "both": Use NIP-85 when available with manual fallback (recommended default) Updated components: - AppContext: Changed nip85OnlyMode to statsMode with StatsMode type - AppProvider: Updated Zod schema to validate three-mode enum - useTrending.ts: Implemented logic for all three modes in useEventStats and useBatchEventStats - AdvancedSettings: Replaced switch with radio group for better UX Default remains "nip85-only" for optimal performance. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
+1
-1
@@ -54,7 +54,7 @@ const defaultConfig: AppConfig = {
|
||||
feedIncludePacks: false,
|
||||
},
|
||||
nip85StatsPubkey: "5f68e85ee174102ca8978eef302129f081f03456c884185d5ec1c1224ab633ea",
|
||||
nip85OnlyMode: true,
|
||||
statsMode: "nip85-only",
|
||||
};
|
||||
|
||||
export function App() {
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2.5 px-3 border-t border-border">
|
||||
<div className="min-w-0">
|
||||
<span className="text-sm">NIP-85 Only Mode</span>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Disable manual stat calculation. Stats will only show if NIP-85 pubkey provides them.
|
||||
<div className="py-3 px-3 border-t border-border space-y-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Stats Calculation Mode</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1 mb-3">
|
||||
Choose how engagement stats are calculated.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="nip85-only-mode"
|
||||
checked={config.nip85OnlyMode}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig(() => ({ nip85OnlyMode: checked }));
|
||||
<RadioGroup
|
||||
value={config.statsMode}
|
||||
onValueChange={(value: StatsMode) => {
|
||||
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"
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="nip85-only" id="nip85-only" disabled={!statsPubkey} />
|
||||
<div className="grid gap-0.5 leading-none">
|
||||
<Label
|
||||
htmlFor="nip85-only"
|
||||
className={`text-sm font-medium cursor-pointer ${!statsPubkey ? 'opacity-50' : ''}`}
|
||||
>
|
||||
NIP-85 Only
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Show only pre-computed stats. Faster, but may be empty if NIP-85 source is unavailable.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="manual-only" id="manual-only" />
|
||||
<div className="grid gap-0.5 leading-none">
|
||||
<Label htmlFor="manual-only" className="text-sm font-medium cursor-pointer">
|
||||
Manual Only
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Always calculate stats from relay queries. Slower, but guaranteed to work.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="both" id="both" disabled={!statsPubkey} />
|
||||
<div className="grid gap-0.5 leading-none">
|
||||
<Label
|
||||
htmlFor="both"
|
||||
className={`text-sm font-medium cursor-pointer ${!statsPubkey ? 'opacity-50' : ''}`}
|
||||
>
|
||||
Both (Recommended)
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Use NIP-85 when available, fall back to manual calculation. Best balance of speed and reliability.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
|
||||
@@ -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<AppConfig>;
|
||||
|
||||
export function AppProvider(props: AppProviderProps) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, EventStats>();
|
||||
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<string, { commentCount: number; repostCount: number; reactionCount: number; zapCount: number }>();
|
||||
|
||||
@@ -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<string, EventStats>();
|
||||
for (const id of uniqueIds) {
|
||||
emptyMap.set(id, EMPTY_STATS);
|
||||
|
||||
Reference in New Issue
Block a user