Skip to content

Commit 1fd9063

Browse files
committed
re-enable Proofmode tag parsing and "Human Made" badge and filtering
1 parent 6a3d352 commit 1fd9063

File tree

7 files changed

+118
-7
lines changed

7 files changed

+118
-7
lines changed

src/components/ProofModeBadge.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ABOUTME: Badge component for displaying ProofMode verification status
1+
// ABOUTME: Badge component for displaying Proofmode verification status
22
// ABOUTME: Shows different icons and colors based on verification level with detailed tooltip
33

44
import { useState } from 'react';
@@ -153,17 +153,17 @@ function getProofModeConfig(level: ProofModeLevel) {
153153
label: 'Human Made',
154154
className: 'border-brand-green text-brand-dark-green dark:text-brand-green bg-brand-light-green dark:bg-brand-dark-green',
155155
iconColor: 'text-brand-dark-green dark:text-brand-green',
156-
tooltip: 'Human made - captured on secure mobile device with ProofMode',
157-
description: 'This video was captured by a human on a mobile device with hardware-backed security attestation. ProofMode provides cryptographic proof that the content is authentic and has not been tampered with.'
156+
tooltip: 'Human made - captured on secure mobile device with Proofmode',
157+
description: 'This video was captured by a human on a mobile device with hardware-backed security attestation. Proofmode provides cryptographic proof that the content is authentic and has not been tampered with.'
158158
};
159159
case 'verified_web':
160160
return {
161161
icon: HumanMadeIcon,
162162
label: 'Human Made',
163163
className: 'border-brand-blue text-brand-blue-dark dark:text-brand-blue bg-brand-blue-light dark:bg-brand-blue-dark',
164164
iconColor: 'text-brand-blue-dark dark:text-brand-blue',
165-
tooltip: 'Human made - valid ProofMode signature',
166-
description: 'This video was created by a human and has been cryptographically signed with ProofMode. The signature confirms the content has not been altered since creation.'
165+
tooltip: 'Human made - valid Proofmode signature',
166+
description: 'This video was created by a human and has been cryptographically signed with Proofmode. The signature confirms the content has not been altered since creation.'
167167
};
168168
case 'basic_proof':
169169
return {

src/components/VideoFeed.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { AddToListDialog } from '@/components/AddToListDialog';
1010
import { useVideoProvider } from '@/hooks/useVideoProvider';
1111
import { useBatchedAuthors } from '@/hooks/useBatchedAuthors';
1212
import { useContentModeration } from '@/hooks/useModeration';
13+
import { useProofModeEnrichment } from '@/hooks/useProofModeEnrichment';
1314
import { Card, CardContent } from '@/components/ui/card';
1415
import { Loader2 } from 'lucide-react';
1516
import InfiniteScroll from 'react-infinite-scroll-component';
@@ -83,7 +84,7 @@ export function VideoFeed({
8384

8485
// Flatten all pages into single array, deduplicating by pubkey:kind:d-tag
8586
// (addressable event key per NIP-33, not just event ID)
86-
const allVideos = useMemo(() => {
87+
const dedupedVideos = useMemo(() => {
8788
const videos = data?.pages.flatMap(page => page.videos) ?? [];
8889
const seen = new Set<string>();
8990
return videos.filter(video => {
@@ -94,6 +95,10 @@ export function VideoFeed({
9495
});
9596
}, [data]);
9697

98+
// Enrich Funnelcake feed videos with ProofMode data via WebSocket
99+
// Feed videos from REST API don't have event tags, so proofMode is always undefined
100+
const allVideos = useProofModeEnrichment(dedupedVideos);
101+
97102
// Filter videos based on mute list and verification status
98103
const filteredVideos = useMemo(() => {
99104
if (!allVideos || allVideos.length === 0) return [];
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// ABOUTME: Enriches Funnelcake feed videos with ProofMode data via WebSocket
2+
// ABOUTME: Queries relay for full events to extract verification tags missing from REST API
3+
4+
import { useMemo } from 'react';
5+
import { useNostr } from '@nostrify/react';
6+
import { useQuery } from '@tanstack/react-query';
7+
import type { ParsedVideoData } from '@/types/video';
8+
import type { ProofModeData } from '@/types/video';
9+
import { getProofModeData } from '@/lib/videoParser';
10+
import { debugLog } from '@/lib/debug';
11+
12+
const BATCH_SIZE = 20;
13+
const STALE_TIME = 5 * 60 * 1000; // 5 minutes
14+
15+
/**
16+
* Hook that enriches videos with ProofMode data from WebSocket queries
17+
*
18+
* Feed videos from Funnelcake REST API don't include event tags,
19+
* so ProofMode data is always undefined. This hook fetches the full
20+
* events via WebSocket to extract verification tags.
21+
*
22+
* Returns the same videos array with proofMode populated where available.
23+
*/
24+
export function useProofModeEnrichment(videos: ParsedVideoData[]): ParsedVideoData[] {
25+
const { nostr } = useNostr();
26+
27+
// Find videos that need enrichment (no proofMode and have an ID)
28+
const videosNeedingEnrichment = useMemo(
29+
() => videos.filter(v => !v.proofMode && v.id).slice(0, BATCH_SIZE),
30+
[videos]
31+
);
32+
33+
const videoIds = useMemo(
34+
() => videosNeedingEnrichment.map(v => v.id),
35+
[videosNeedingEnrichment]
36+
);
37+
38+
// Query relay for full events by ID
39+
const { data: proofModeMap } = useQuery<Map<string, ProofModeData>>({
40+
queryKey: ['proofmode-enrichment', ...videoIds],
41+
queryFn: async ({ signal }) => {
42+
if (videoIds.length === 0) return new Map();
43+
44+
debugLog(`[ProofModeEnrichment] Fetching ${videoIds.length} events for ProofMode data`);
45+
46+
const events = await nostr.query(
47+
[{ ids: videoIds, limit: BATCH_SIZE }],
48+
{ signal: AbortSignal.any([signal, AbortSignal.timeout(10000)]) },
49+
);
50+
51+
debugLog(`[ProofModeEnrichment] Got ${events.length} events`);
52+
53+
const map = new Map<string, ProofModeData>();
54+
for (const event of events) {
55+
const proofMode = getProofModeData(event);
56+
if (proofMode) {
57+
map.set(event.id, proofMode);
58+
}
59+
}
60+
61+
debugLog(`[ProofModeEnrichment] Found ${map.size} videos with ProofMode data`);
62+
return map;
63+
},
64+
enabled: videoIds.length > 0,
65+
staleTime: STALE_TIME,
66+
gcTime: STALE_TIME * 2,
67+
});
68+
69+
// Merge ProofMode data into videos
70+
return useMemo(() => {
71+
if (!proofModeMap || proofModeMap.size === 0) return videos;
72+
73+
return videos.map(video => {
74+
if (video.proofMode) return video; // Already has data
75+
const proofMode = proofModeMap.get(video.id);
76+
if (!proofMode) return video;
77+
return { ...video, proofMode };
78+
});
79+
}, [videos, proofModeMap]);
80+
}

src/lib/funnelcakeClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ export async function fetchVideoById(
614614
engagement_score: stats.engagement_score,
615615
trending_score: stats.trending_score,
616616
loops: stats.embedded_loops || parseInt(getTag('loops') || '0') || null,
617+
tags: event.tags,
617618
};
618619
}
619620
} catch {

src/lib/funnelcakeTransform.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { parseByteArrayId } from './funnelcakeClient';
55
import { SHORT_VIDEO_KIND, type ParsedVideoData } from '@/types/video';
66
import type { FunnelcakeVideoRaw, FunnelcakeResponse } from '@/types/funnelcake';
77
import { debugLog } from './debug';
8+
import { getProofModeData } from './videoParser';
9+
import type { NostrEvent } from '@nostrify/nostrify';
810

911
/**
1012
* Parse loop count from video content text
@@ -79,6 +81,17 @@ export function transformFunnelcakeVideo(raw: FunnelcakeVideoRaw): ParsedVideoDa
7981
externalId: raw.d_tag || '',
8082
} : undefined,
8183

84+
// ProofMode data - extract from tags when available (single video endpoint)
85+
proofMode: raw.tags ? getProofModeData({
86+
id: id,
87+
pubkey: pubkey,
88+
created_at: raw.created_at,
89+
kind: raw.kind,
90+
tags: raw.tags,
91+
content: raw.content || '',
92+
sig: '',
93+
} as NostrEvent) : undefined,
94+
8295
// Empty reposts array (Funnelcake doesn't return individual reposts)
8396
reposts: [],
8497

src/lib/videoParser.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,10 +522,13 @@ export function getOriginalCommentCount(event: NostrEvent): number | undefined {
522522
* - pgp_fingerprint: PGP public key fingerprint
523523
*/
524524
export function getProofModeData(event: NostrEvent): ProofModeData | undefined {
525+
console.log('[ProofMode] Event tags:', JSON.stringify(event.tags, null, 2));
526+
525527
const levelTag = event.tags.find(tag => tag[0] === 'verification');
526528

527529
// If no verification level tag found, return undefined
528530
if (!levelTag?.[1]) {
531+
console.log('[ProofMode] No verification tag found');
529532
return undefined;
530533
}
531534

@@ -542,23 +545,29 @@ export function getProofModeData(event: NostrEvent): ProofModeData | undefined {
542545
const attestationTag = event.tags.find(tag => tag[0] === 'device_attestation');
543546
const fingerprintTag = event.tags.find(tag => tag[0] === 'pgp_fingerprint');
544547

548+
if (attestationTag != null)
549+
level = "verified_mobile";
550+
545551
// Parse manifest JSON if present
546552
let manifestData: Record<string, unknown> | undefined;
547553
if (manifestTag?.[1]) {
548554
try {
549555
manifestData = JSON.parse(manifestTag[1]);
556+
level = "verified_web";
550557
} catch {
551558
// Invalid JSON, ignore
552559
}
553560
}
554561

555-
return {
562+
const result = {
556563
level,
557564
manifest: manifestTag?.[1],
558565
manifestData,
559566
deviceAttestation: attestationTag?.[1],
560567
pgpFingerprint: fingerprintTag?.[1],
561568
};
569+
console.log('[ProofMode] Result:', result);
570+
return result;
562571
}
563572

564573
/**

src/types/funnelcake.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export interface FunnelcakeVideoRaw {
3939
// Platform origin (for filtering classic vines)
4040
platform?: string; // 'vine', 'tiktok', etc.
4141
classic?: boolean; // Whether this is a classic/archived vine
42+
43+
// Full event tags (only present from /api/videos/{id} endpoint)
44+
tags?: string[][]; // Nostr event tags for ProofMode extraction
4245
}
4346

4447
/**

0 commit comments

Comments
 (0)