diff --git a/src/components/Login/Login.js b/src/components/Login/Login.js index 34abb12..57db7c8 100644 --- a/src/components/Login/Login.js +++ b/src/components/Login/Login.js @@ -1,11 +1,10 @@ import React, { useState, useEffect } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; const Login = () => { const [handle, setHandle] = useState(''); const { login, loading, error, isAuthenticated } = useAuth(); - const navigate = useNavigate(); const location = useLocation(); const queryParams = new URLSearchParams(location.search); diff --git a/src/components/Verifier/Verifier.js b/src/components/Verifier/Verifier.js index 31a6860..9be340e 100644 --- a/src/components/Verifier/Verifier.js +++ b/src/components/Verifier/Verifier.js @@ -1,47 +1,39 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useAuth } from '../../contexts/AuthContext'; import { Agent } from '@atproto/api'; +import VerifierForm from './VerifierForm'; +import VerifierSuggestions from './VerifierSuggestions'; -function Verifier() { +export default function Verifier() { const { session, loading: isAuthLoading, error: authError } = useAuth(); const [agent, setAgent] = useState(null); - // Verification records (all pages, loaded automatically) const [verifications, setVerifications] = useState([]); const [isLoadingVerifications, setIsLoadingVerifications] = useState(false); - // User lists (app.bsky.graph.list owned by the account) const [userLists, setUserLists] = useState([]); const [isLoadingLists, setIsLoadingLists] = useState(false); - // Verification mode: 'account' or 'list' const [mode, setMode] = useState('account'); const [targetHandle, setTargetHandle] = useState(''); const [selectedListUri, setSelectedListUri] = useState(''); - // Bulk verification progress const [bulkProgress, setBulkProgress] = useState(null); - const [statusMessage, setStatusMessage] = useState(''); + const [statusType, setStatusType] = useState('success'); const [isVerifying, setIsVerifying] = useState(false); - // Typeahead suggestions const [suggestions, setSuggestions] = useState([]); const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false); - const debounceTimeoutRef = useRef(null); - const suggestionListRef = useRef(null); + const debounceRef = useRef(null); - // Build agent when session exists + // --- Agent --- useEffect(() => { - if (session) { - setAgent(new Agent(session)); - } else { - setAgent(null); - } + setAgent(session ? new Agent(session) : null); }, [session]); - // Fetch ALL verification records (auto-paginate) + // --- Data fetching --- const fetchAllVerifications = useCallback(async () => { if (!agent || !session) return; setIsLoadingVerifications(true); @@ -49,35 +41,27 @@ function Verifier() { try { let cursor = null; do { - const params = { - repo: session.did, - collection: 'app.bsky.graph.verification', - limit: 100, - }; + const params = { repo: session.did, collection: 'app.bsky.graph.verification', limit: 100 }; if (cursor) params.cursor = cursor; - const response = await agent.api.com.atproto.repo.listRecords(params); - if (response.data.records) { - all.push(...response.data.records.map(record => ({ - uri: record.uri, - cid: record.cid, - handle: record.value.handle, - displayName: record.value.displayName, - subject: record.value.subject, - createdAt: record.value.createdAt, + const res = await agent.api.com.atproto.repo.listRecords(params); + if (res.data.records) { + all.push(...res.data.records.map(r => ({ + uri: r.uri, cid: r.cid, + handle: r.value.handle, displayName: r.value.displayName, + subject: r.value.subject, createdAt: r.value.createdAt, }))); } - cursor = response.data.cursor || null; + cursor = res.data.cursor || null; } while (cursor); setVerifications(all); } catch (error) { - console.error('Failed to fetch verifications:', error); setStatusMessage(`Failed to load verifications: ${error.message || 'Unknown error'}`); + setStatusType('error'); } finally { setIsLoadingVerifications(false); } }, [agent, session]); - // Fetch ALL user lists owned by the account (curatelist only) const fetchUserLists = useCallback(async () => { if (!agent || !session) return; setIsLoadingLists(true); @@ -85,52 +69,42 @@ function Verifier() { try { let cursor = null; do { - const params = { actor: session.did, limit: 100 }; - if (cursor) params.cursor = cursor; - const response = await agent.api.app.bsky.graph.getLists(params); - if (response.data.lists) { - all.push(...response.data.lists.filter(l => l.purpose === 'app.bsky.graph.defs#curatelist')); + const res = await agent.api.app.bsky.graph.getLists({ actor: session.did, limit: 100, ...(cursor ? { cursor } : {}) }); + if (res.data.lists) { + all.push(...res.data.lists.filter(l => l.purpose === 'app.bsky.graph.defs#curatelist')); } - cursor = response.data.cursor || null; + cursor = res.data.cursor || null; } while (cursor); setUserLists(all); - } catch (error) { - console.error('Failed to fetch user lists:', error); } finally { setIsLoadingLists(false); } }, [agent, session]); - // Auto-load verifications + lists once agent is ready useEffect(() => { - if (agent && session) { - fetchAllVerifications(); - fetchUserLists(); - } + if (agent && session) { fetchAllVerifications(); fetchUserLists(); } }, [agent, session, fetchAllVerifications, fetchUserLists]); - // Set of already-verified subject DIDs for fast duplicate lookup - const verifiedDids = new Set(verifications.map(v => v.subject)); - const verifiedByHandle = {}; - verifications.forEach(v => { verifiedByHandle[v.handle.toLowerCase()] = v; }); + // --- Derived data (memoized) --- + const verifiedDids = useMemo(() => new Set(verifications.map(v => v.subject)), [verifications]); + const verifiedByHandle = useMemo(() => { + const map = {}; + verifications.forEach(v => { map[v.handle.toLowerCase()] = v; }); + return map; + }, [verifications]); - // Typeahead + // --- Typeahead --- const fetchSuggestions = useCallback(async (query) => { - if (!query || query.length < 1) { - setSuggestions([]); - setShowSuggestions(false); - return; - } + if (!query || query.length < 1) { setSuggestions([]); setShowSuggestions(false); return; } setIsLoadingSuggestions(true); setShowSuggestions(true); try { const url = `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=10`; - const response = await fetch(url); - if (!response.ok) throw new Error(`API Error: ${response.status}`); - const data = await response.json(); + const res = await fetch(url); + if (!res.ok) throw new Error(`API Error: ${res.status}`); + const data = await res.json(); setSuggestions(data.actors || []); - } catch (error) { - console.error('Failed to fetch suggestions:', error); + } catch { setSuggestions([]); } finally { setIsLoadingSuggestions(false); @@ -138,41 +112,24 @@ function Verifier() { }, []); const handleInputChange = (e) => { - const newHandle = e.target.value; - setTargetHandle(newHandle); - if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current); - if (newHandle.trim() === '') { - setSuggestions([]); - setShowSuggestions(false); - setIsLoadingSuggestions(false); - return; - } - debounceTimeoutRef.current = setTimeout(() => fetchSuggestions(newHandle), 300); + const val = e.target.value; + setTargetHandle(val); + if (debounceRef.current) clearTimeout(debounceRef.current); + if (val.trim() === '') { setSuggestions([]); setShowSuggestions(false); return; } + debounceRef.current = setTimeout(() => fetchSuggestions(val), 300); }; - const handleSuggestionClick = (handle) => { - setTargetHandle(handle); - setSuggestions([]); - setShowSuggestions(false); - }; + const handleSuggestionSelect = (handle) => { setTargetHandle(handle); setShowSuggestions(false); setSuggestions([]); }; - // Write a single verification record for a profile object + // --- Verification --- const writeVerification = useCallback(async (did, handle, displayName) => { - const verificationRecord = { - $type: 'app.bsky.graph.verification', - subject: did, - handle, - displayName, - createdAt: new Date().toISOString(), - }; await agent.api.com.atproto.repo.createRecord({ repo: session.did, collection: 'app.bsky.graph.verification', - record: verificationRecord, + record: { $type: 'app.bsky.graph.verification', subject: did, handle, displayName, createdAt: new Date().toISOString() }, }); }, [agent, session]); - // Single account verification const handleVerifyAccount = async (e) => { e.preventDefault(); if (!agent || !session || !targetHandle) return; @@ -184,36 +141,31 @@ function Verifier() { if (verifiedDids.has(targetDid)) { const existing = verifications.find(v => v.subject === targetDid); - setStatusMessage( - `@${targetHandle} is already verified (verified ${new Date(existing.createdAt).toLocaleString()}). See your verifications below.` - ); + setStatusMessage(`@${targetHandle} is already verified (verified ${new Date(existing.createdAt).toLocaleString()}). See your verifications below.`); + setStatusType('success'); return; } - setStatusMessage(`Verifying ${targetHandle}...`); await writeVerification(targetDid, targetHandle, targetDisplayName); - const postText = `I just verified @${targetHandle} using Bluesky's verification system.`; const intentUrl = `https://bsky.app/intent/compose?text=${encodeURIComponent(postText)}`; setStatusMessage( - <> - Successfully created verification for {targetHandle}!{' '} - + <>Successfully verified {targetHandle}!{' '} + Post on Bluesky to let them know? - - > + > ); + setStatusType('success'); setTargetHandle(''); fetchAllVerifications(); } catch (error) { - console.error('Verification failed:', error); setStatusMessage(`Verification failed: ${error.message || 'Unknown error'}`); + setStatusType('error'); } finally { setIsVerifying(false); } }; - // List (bulk) verification const handleVerifyList = async (e) => { e.preventDefault(); if (!agent || !session || !selectedListUri) return; @@ -221,57 +173,37 @@ function Verifier() { setBulkProgress(null); try { setStatusMessage('Fetching list members...'); + setStatusType('success'); const members = []; let cursor = null; do { - const params = { list: selectedListUri, limit: 100 }; - if (cursor) params.cursor = cursor; - const response = await agent.api.app.bsky.graph.getList(params); - if (response.data.items) { - members.push(...response.data.items.map(item => item.subject)); - } - cursor = response.data.cursor || null; + const res = await agent.api.app.bsky.graph.getList({ list: selectedListUri, limit: 100, ...(cursor ? { cursor } : {}) }); + if (res.data.items) members.push(...res.data.items.map(i => i.subject)); + cursor = res.data.cursor || null; } while (cursor); - if (members.length === 0) { - setStatusMessage('This list has no members.'); - return; - } - - let verified = 0; - let skipped = 0; - let failed = 0; + if (!members.length) { setStatusMessage('This list has no members.'); setStatusType('success'); return; } + let verified = 0, skipped = 0, failed = 0; for (let i = 0; i < members.length; i++) { - const member = members[i]; - setBulkProgress({ current: i + 1, total: members.length, currentHandle: member.handle }); - - if (verifiedDids.has(member.did)) { - skipped++; - continue; - } - + const m = members[i]; + setBulkProgress({ current: i + 1, total: members.length, currentHandle: m.handle }); + if (verifiedDids.has(m.did)) { skipped++; continue; } try { - await writeVerification(member.did, member.handle, member.displayName || member.handle); - verifiedDids.add(member.did); - verified++; - } catch (err) { - console.error(`Failed to verify ${member.handle}:`, err); - failed++; - } - + await writeVerification(m.did, m.handle, m.displayName || m.handle); + verifiedDids.add(m.did); verified++; + } catch { failed++; } await new Promise(r => setTimeout(r, 250)); } const listName = userLists.find(l => l.uri === selectedListUri)?.name || 'list'; - setStatusMessage( - `Finished verifying "${listName}": ${verified} verified, ${skipped} already verified, ${failed} failed (${members.length} total).` - ); + setStatusMessage(`Finished verifying "${listName}": ${verified} verified, ${skipped} already verified, ${failed} failed (${members.length} total).`); + setStatusType('success'); setSelectedListUri(''); fetchAllVerifications(); } catch (error) { - console.error('List verification failed:', error); setStatusMessage(`List verification failed: ${error.message || 'Unknown error'}`); + setStatusType('error'); } finally { setIsVerifying(false); setBulkProgress(null); @@ -283,130 +215,49 @@ function Verifier() { const handleRevoke = async (verification) => { if (!agent || !session) return; setIsVerifying(true); - setStatusMessage(`Revoking verification for ${verification.handle}...`); try { - const rkey = verification.uri.split('/').pop(); await agent.api.com.atproto.repo.deleteRecord({ - repo: session.did, - collection: 'app.bsky.graph.verification', - rkey, + repo: session.did, collection: 'app.bsky.graph.verification', rkey: verification.uri.split('/').pop(), }); - setStatusMessage(`Successfully revoked verification for ${verification.handle}`); + setStatusMessage(`Revoked verification for ${verification.handle}`); + setStatusType('success'); fetchAllVerifications(); } catch (error) { - console.error('Revocation failed:', error); setStatusMessage(`Revocation failed: ${error.message || 'Unknown error'}`); + setStatusType('error'); } finally { setIsVerifying(false); } }; - useEffect(() => { - const handleClickOutside = (event) => { - if (suggestionListRef.current && !suggestionListRef.current.contains(event.target)) { - if (!event.target.classList.contains('verifier-input-field')) { - setShowSuggestions(false); - } - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - + // --- Render --- if (isAuthLoading) return
Loading authentication...
; if (authError) returnAuthentication Error: {authError}. Please login.
; - const isDuplicateHandle = targetHandle && verifications.some( - v => v.handle.toLowerCase() === targetHandle.toLowerCase() - ); + const isDuplicateHandle = !!(targetHandle && verifiedByHandle[targetHandle.toLowerCase()]); + const duplicateDate = isDuplicateHandle + ? new Date(verifiedByHandle[targetHandle.toLowerCase()].createdAt).toLocaleString() + : ''; return (- Already verified on {new Date(verifiedByHandle[targetHandle.toLowerCase()]?.createdAt).toLocaleString()} — see your verifications below. -
- )} - - {mode === 'list' && !isLoadingLists && userLists.length === 0 && ( -- You don't have any user lists. Create one on Bluesky first. -
- )} -{statusMessage}
Loading...
- ) : verifications.length === 0 ? ( -You haven't verified any accounts.
- ) : ( -Loading...
+ : verifications.length === 0 ?You haven't verified any accounts.
+ : ( ++ Already verified on {duplicateDate} — see your verifications below. +
+ )} + + {mode === 'list' && !isLoadingLists && userLists.length === 0 && ( ++ You don't have any user lists. Create one on Bluesky first. +
+ )} +