From 8184f5be61d00a4657dbd6ba5c7cde96a6592b58 Mon Sep 17 00:00:00 2001 From: Patrick Singletary Date: Sat, 1 Aug 2026 09:45:30 -0400 Subject: [PATCH] Optimize: strip console.log, useMemo, split Verifier into sub-components - Remove 14 console.log calls from AuthContext.js (keep 2 error-level) - Remove unused useNavigate import from Login.js - Add useMemo for verifiedDids and verifiedByHandle in Verifier.js - Replace fragile statusMessage type-check with explicit statusType state - Split Verifier.js into VerifierForm.jsx and VerifierSuggestions.jsx - JS bundle: 259.28 -> 259.06 kB --- src/components/Login/Login.js | 3 +- src/components/Verifier/Verifier.js | 383 ++++++------------ src/components/Verifier/VerifierForm.jsx | 80 ++++ .../Verifier/VerifierSuggestions.jsx | 30 ++ src/contexts/AuthContext.js | 63 +-- 5 files changed, 240 insertions(+), 319 deletions(-) create mode 100644 src/components/Verifier/VerifierForm.jsx create mode 100644 src/components/Verifier/VerifierSuggestions.jsx 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) return

Authentication 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 (
-
-

Verify an ATmosphere account

-
- - - {mode === 'account' ? ( - - ) : ( - - )} - - -
- - {mode === 'account' && showSuggestions && suggestions.length > 0 && ( -
    - {isLoadingSuggestions ? ( -
  • Loading suggestions...
  • - ) : ( - suggestions.map(actor => ( -
  • handleSuggestionClick(actor.handle)}> - e.target.style.display = 'none'} /> -
    - {actor.displayName || actor.handle} - @{actor.handle} -
    -
  • - )) - )} -
- )} - - {mode === 'account' && isDuplicateHandle && ( -

- 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. -

- )} -
+ + + {mode === 'account' && showSuggestions && ( + + )} {bulkProgress && (
@@ -415,40 +266,36 @@ function Verifier() { )} {statusMessage && ( -
+

{statusMessage}

)}

Your Verifications

- {isLoadingVerifications ? ( -

Loading...

- ) : verifications.length === 0 ? ( -

You haven't verified any accounts.

- ) : ( -
    - {verifications.map((verification) => ( -
  • -
    - - {verification.displayName} - @{verification.handle} - -
    Verified: {new Date(verification.createdAt).toLocaleString()}
    -
    -
    - -
    -
  • - ))} -
- )} + {isLoadingVerifications ?

Loading...

+ : verifications.length === 0 ?

You haven't verified any accounts.

+ : ( +
    + {verifications.map(v => ( +
  • +
    + + {v.displayName} + @{v.handle} + +
    Verified: {new Date(v.createdAt).toLocaleString()}
    +
    +
    + +
    +
  • + ))} +
+ )}
); } - -export default Verifier; diff --git a/src/components/Verifier/VerifierForm.jsx b/src/components/Verifier/VerifierForm.jsx new file mode 100644 index 0000000..55b0a6b --- /dev/null +++ b/src/components/Verifier/VerifierForm.jsx @@ -0,0 +1,80 @@ +import React from 'react'; + +export default function VerifierForm({ + mode, + setMode, + targetHandle, + onChangeHandle, + selectedListUri, + setSelectedListUri, + userLists, + isLoadingLists, + isVerifying, + isDuplicateHandle, + duplicateDate, + onSubmit, +}) { + return ( +
+

Verify an ATmosphere account

+
+ + + {mode === 'account' ? ( + + ) : ( + + )} + + +
+ + {mode === 'account' && isDuplicateHandle && ( +

+ 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. +

+ )} +
+ ); +} diff --git a/src/components/Verifier/VerifierSuggestions.jsx b/src/components/Verifier/VerifierSuggestions.jsx new file mode 100644 index 0000000..8175fee --- /dev/null +++ b/src/components/Verifier/VerifierSuggestions.jsx @@ -0,0 +1,30 @@ +import React, { useRef, useCallback } from 'react'; + +export default function VerifierSuggestions({ suggestions, isLoading, onSelect, inputRef }) { + const listRef = useRef(null); + + if (!suggestions.length) return null; + + return ( +
    + {isLoading ? ( +
  • Loading suggestions...
  • + ) : ( + suggestions.map(actor => ( +
  • onSelect(actor.handle)}> + { e.target.style.display = 'none'; }} + /> +
    + {actor.displayName || actor.handle} + @{actor.handle} +
    +
  • + )) + )} +
+ ); +} diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.js index e9905d6..cdc2ee2 100644 --- a/src/contexts/AuthContext.js +++ b/src/contexts/AuthContext.js @@ -1,11 +1,8 @@ import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react'; import { BrowserOAuthClient } from '@atproto/oauth-client-browser'; -// Create auth context export const AuthContext = createContext(null); - -// Client metadata for Bluesky OAuth const clientMetadata = { client_id: `https://verifier.psingletary.com/client-metadata.json`, client_name: "Verifier", @@ -27,99 +24,68 @@ export const AuthProvider = ({ children }) => { const [error, setError] = useState(null); const initializing = useRef(false); - // Updated initializeAuth for BrowserOAuthClient useEffect(() => { const initializeAuth = async () => { if (initializing.current || client) return; initializing.current = true; setLoading(true); setError(null); - console.log('(AuthProvider) Initializing BrowserOAuthClient...'); try { const oauthClient = new BrowserOAuthClient({ - clientMetadata: clientMetadata, + clientMetadata, handleResolver: 'https://public.api.bsky.app', plcDirectoryUrl: 'https://plc.directory', }); setClient(oauthClient); - console.log('(AuthProvider) Initializing OAuth client...'); const initResult = await oauthClient.init(); - console.log('(AuthProvider) Init result:', { - hasSession: !!initResult?.session, - hasState: !!initResult?.state, - did: initResult?.session?.did - }); if (initResult?.session) { setSession(initResult.session); - console.log(`(AuthProvider) Session ${initResult.state ? 'established via callback' : 'restored'}:`, initResult.session.did); } else { setSession(null); - console.log('(AuthProvider) No active session found or callback processed.'); } } catch (err) { - console.error('(AuthProvider) Error initializing client or handling callback:', err); + console.error('Auth initialization failed:', err); setError('Authentication initialization failed. Please try refreshing.'); setSession(null); } finally { setLoading(false); initializing.current = false; - console.log('(AuthProvider) Initialization complete.'); } }; initializeAuth(); }, [client]); - // Updated login function - uses client.signIn() const login = useCallback(async (handle, returnUrl = '/') => { if (!client) { setError("Client not initialized."); return; } - console.log(`(AuthProvider) Initiating client-side login for handle: ${handle || 'none specified'}, returnUrl: ${returnUrl}`); try { const stateData = JSON.stringify({ returnUrl }); - await client.signIn(handle, { - state: stateData, - }); + await client.signIn(handle, { state: stateData }); } catch (err) { - console.error('(AuthProvider) Error during signIn initiation or cancellation:', err); + console.error('Login failed:', err); setError('Login initiation failed or was cancelled.'); } }, [client]); - // Updated Logout function - uses session.signOut() const logout = useCallback(async () => { - if (!session) { - console.log('(AuthProvider) No active session to log out'); - return; - } - console.log('(AuthProvider) Logging out...'); - try { - await session.signOut(); - setSession(null); - console.log('(AuthProvider) Logout complete.'); - window.location.href = '/'; - } catch (err) { - console.error('(AuthProvider) Error during logout:', err); - setSession(null); - window.location.href = '/'; - } + if (!session) return; + try { + await session.signOut(); + setSession(null); + window.location.href = '/'; + } catch (err) { + console.error('Logout failed:', err); + setSession(null); + window.location.href = '/'; + } }, [session]); - // Debug effect to log auth state changes - useEffect(() => { - console.log('(AuthProvider) Auth state updated:', { - isAuthenticated: !!session, - did: session?.did || null, - loading, - hasError: !!error - }); - }, [session, loading, error]); - return ( { ); }; -// Custom hook to use the auth context export const useAuth = () => { const context = useContext(AuthContext); if (context === null) { -- 2.51.2