This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

add verifier tool

+1367 -1
+2
src/App.jsx
··· 21 21 import CollectionsFeed from './components/CollectionsFeed/CollectionsFeed'; 22 22 import Login from './components/Login/Login'; 23 23 import LoginCallback from './components/Login/LoginCallback'; 24 + import Verifier from './components/Verifier/Verifier'; 24 25 import { AuthProvider } from './contexts/AuthContext'; 25 26 import "./App.css"; 26 27 ··· 50 51 <Route path="/definitions" element={<Definitions />} /> 51 52 <Route path="/leaderboard" element={<Leaderboard />} /> 52 53 <Route path="/resources" element={<Resources />} /> 54 + <Route path="/verifier" element={<Verifier />} /> 53 55 <Route path="/shortcut" element={<Shortcut />} /> 54 56 <Route path="/zen" element={<ZenPage />} /> 55 57 <Route path="/methodology" element={<ScoringMethodology />} />
+2 -1
src/components/Navbar/Navbar.js
··· 131 131 items: [ 132 132 { title: "library", path: "/resources" }, 133 133 { title: "alt text rating", path: "/alt-text" }, 134 - { title: "omnifeed", path: "/omnifeed" } 134 + { title: "omnifeed", path: "/omnifeed" }, 135 + { title: "verifier", path: "/verifier" } 135 136 ] 136 137 }; 137 138
+331
src/components/Verifier/Verifier.css
··· 1 + /* frontend-cred-blue/src/components/Verifier/Verifier.css */ 2 + 3 + /* General container */ 4 + .verifier-container { 5 + font-family: "articulat-cf", sans-serif; 6 + max-width: 800px; /* Adjust as needed */ 7 + margin: 20px auto; 8 + padding: 20px; 9 + color: var(--text); 10 + background-color: var(--background); 11 + } 12 + 13 + .verifier-container h1, 14 + .verifier-container h2 { 15 + color: var(--button-bg); /* Match heading color */ 16 + text-align: left; 17 + margin-bottom: 15px; 18 + } 19 + 20 + .verifier-container h1 { 21 + font-size: 2em; /* Adjust */ 22 + } 23 + 24 + .verifier-container h2 { 25 + font-size: 1.5em; /* Adjust */ 26 + margin-top: 30px; /* Space between sections */ 27 + border-bottom: 1px solid var(--card-border); /* Separator */ 28 + padding-bottom: 10px; 29 + } 30 + 31 + .verifier-intro-text, 32 + .verifier-section p { 33 + color: var(--text); 34 + line-height: 1.6; 35 + margin-bottom: 10px; 36 + text-align: left; 37 + } 38 + 39 + .verifier-page-header { 40 + display: flex; 41 + justify-content: space-between; 42 + align-items: center; 43 + margin-bottom: 15px; 44 + flex-wrap: wrap; /* Allow wrapping on small screens */ 45 + gap: 10px; 46 + } 47 + 48 + .verifier-user-info { 49 + font-size: 0.9em; 50 + color: var(--text-muted, var(--text)); 51 + margin: 0; /* Remove default paragraph margin */ 52 + } 53 + 54 + /* Buttons */ 55 + .verifier-sign-out-button, 56 + .verifier-submit-button, 57 + .verifier-action-button, 58 + .verifier-revoke-button { 59 + background: var(--button-bg); 60 + color: var(--button-text); 61 + border: none; 62 + border-radius: 6px; 63 + padding: 8px 15px; /* Slightly smaller padding */ 64 + font-weight: 700; 65 + font-size: 0.9em; 66 + cursor: pointer; 67 + transition: background-color 0.3s ease; 68 + } 69 + 70 + .verifier-sign-out-button:hover, 71 + .verifier-submit-button:hover, 72 + .verifier-action-button:hover, 73 + .verifier-revoke-button:hover { 74 + background: var(--button-hover-bg, #0056b3); /* Use main hover color */ 75 + } 76 + 77 + .verifier-sign-out-button:disabled, 78 + .verifier-submit-button:disabled, 79 + .verifier-action-button:disabled, 80 + .verifier-revoke-button:disabled { 81 + background-color: var(--button-disabled-bg, #cccccc); /* Add disabled style */ 82 + cursor: not-allowed; 83 + opacity: 0.7; 84 + } 85 + 86 + /* Form Styles */ 87 + .verifier-section { 88 + background: var(--navbar-bg); 89 + border: 1px solid var(--card-border); 90 + border-radius: 12px; 91 + padding: 20px; 92 + margin-bottom: 20px; 93 + } 94 + 95 + .verifier-input-container { 96 + position: relative; /* For autocomplete positioning */ 97 + max-width: 400px; /* Limit width */ 98 + } 99 + 100 + .verifier-form-container { 101 + display: flex; 102 + gap: 10px; 103 + align-items: center; 104 + flex-wrap: wrap; /* Allow wrapping */ 105 + } 106 + 107 + .verifier-input-field { 108 + flex-grow: 1; /* Take available space */ 109 + border: 2px solid var(--card-border); 110 + border-radius: 6px; 111 + padding: 9px; 112 + font-size: 1em; 113 + background-color: var(--navbar-bg); 114 + color: var(--text); 115 + transition: all 0.3s ease; 116 + font-family: inherit; /* Use main font */ 117 + min-width: 200px; /* Ensure minimum width */ 118 + } 119 + 120 + .verifier-input-field:hover, 121 + .verifier-input-field:focus { 122 + border-color: var(--button-bg); 123 + background-color: var(--background); /* Match main app focus */ 124 + outline: none; 125 + } 126 + 127 + /* Autocomplete Styles */ 128 + .verifier-suggestions-list { 129 + position: absolute; 130 + top: 100%; /* Position below input */ 131 + left: 0; 132 + right: 0; 133 + background-color: var(--navbar-bg); 134 + border: 1px solid var(--card-border); 135 + border-top: none; /* Avoid double border */ 136 + border-radius: 0 0 6px 6px; 137 + list-style: none; 138 + padding: 0; 139 + margin: 0; 140 + max-height: 200px; 141 + overflow-y: auto; 142 + z-index: 10; 143 + box-shadow: 0 4px 6px rgba(0,0,0,0.1); 144 + } 145 + 146 + .verifier-suggestion-item { 147 + display: flex; 148 + align-items: center; 149 + padding: 10px; 150 + cursor: pointer; 151 + border-bottom: 1px solid var(--card-border); 152 + } 153 + .verifier-suggestion-item:last-child { 154 + border-bottom: none; 155 + } 156 + 157 + .verifier-suggestion-item:hover { 158 + background-color: var(--background); /* Use main background for hover */ 159 + } 160 + 161 + .verifier-suggestion-avatar { 162 + width: 30px; 163 + height: 30px; 164 + border-radius: 50%; 165 + margin-right: 10px; 166 + object-fit: cover; 167 + } 168 + 169 + .verifier-suggestion-text { 170 + display: flex; 171 + flex-direction: column; 172 + } 173 + 174 + .verifier-suggestion-display-name { 175 + font-weight: bold; 176 + color: var(--text); 177 + } 178 + 179 + .verifier-suggestion-handle { 180 + font-size: 0.9em; 181 + color: var(--text-muted, var(--text)); 182 + } 183 + 184 + /* Status Box */ 185 + .verifier-status-box { 186 + padding: 15px; 187 + border-radius: 6px; 188 + margin-top: 15px; 189 + text-align: center; 190 + } 191 + .verifier-status-box-success { 192 + background-color: var(--success-bg, #d4edda); /* Add theme variables */ 193 + color: var(--success-text, #155724); 194 + border: 1px solid var(--success-border, #c3e6cb); 195 + } 196 + .verifier-status-box-error { 197 + background-color: var(--error-bg, #f8d7da); /* Add theme variables */ 198 + color: var(--error-text, #721c24); 199 + border: 1px solid var(--error-border, #f5c6cb); 200 + } 201 + .verifier-status-box .intent-link { 202 + color: var(--success-text, #155724); 203 + font-weight: bold; 204 + text-decoration: underline; 205 + } 206 + 207 + /* Verification Lists */ 208 + .verifier-list-header { 209 + display: flex; 210 + justify-content: space-between; 211 + align-items: center; 212 + margin-bottom: 10px; /* Space above list */ 213 + } 214 + .verifier-list-header h2 { 215 + margin: 0; 216 + border: none; /* Remove border inherited from h2 general style */ 217 + padding: 0; 218 + } 219 + 220 + .verifier-list, 221 + .verifier-verifier-list { 222 + list-style: none; 223 + padding: 0; 224 + margin: 0; 225 + } 226 + 227 + .verifier-list-item { 228 + display: flex; 229 + justify-content: space-between; 230 + align-items: flex-start; /* Align items to top */ 231 + background-color: var(--navbar-bg); /* Match form background */ 232 + padding: 15px; 233 + border: 1px solid var(--card-border); 234 + border-radius: 8px; 235 + margin-bottom: 10px; 236 + flex-wrap: wrap; /* Allow actions to wrap */ 237 + gap: 10px; 238 + } 239 + .verifier-list-item-content { 240 + flex-grow: 1; 241 + } 242 + .verifier-list-item-handle { 243 + font-size: 0.9em; 244 + color: var(--text-muted, var(--text)); 245 + margin: 2px 0; 246 + } 247 + .verifier-list-item-date { 248 + font-size: 0.8em; 249 + color: var(--text-muted, var(--text)); 250 + margin-top: 5px; 251 + } 252 + 253 + .verifier-list-item-actions { 254 + flex-shrink: 0; /* Prevent button shrinking */ 255 + } 256 + 257 + .verifier-list-item-invalid { 258 + border-left: 5px solid var(--warning-border, orange); /* Highlight invalid items */ 259 + } 260 + 261 + .verifier-validity-warning { 262 + margin-top: 10px; 263 + padding: 10px; 264 + background-color: var(--warning-bg, #fff3cd); 265 + border: 1px solid var(--warning-border, #ffeeba); 266 + color: var(--warning-text, #856404); 267 + border-radius: 4px; 268 + font-size: 0.9em; 269 + } 270 + .verifier-validity-warning p { 271 + margin: 5px 0; 272 + color: var(--warning-text, #856404); /* Ensure text color consistency */ 273 + text-align: left; 274 + } 275 + 276 + /* Network Verifications */ 277 + .verifier-check-network-button { 278 + font-size: 0.9em; 279 + } 280 + .verifier-network-status { 281 + font-style: italic; 282 + color: var(--text-muted, var(--text)); 283 + margin: 10px 0; 284 + } 285 + .verifier-network-results { 286 + margin-top: 15px; 287 + } 288 + .verifier-network-results .verifier-verifier-list { 289 + margin-bottom: 15px; /* Space between lists */ 290 + } 291 + .verifier-additional-context { 292 + font-size: 0.9em; 293 + color: var(--text-muted, var(--text)); 294 + margin-top: 15px; 295 + border-top: 1px dashed var(--card-border); 296 + padding-top: 10px; 297 + } 298 + .verifier-share-stats-link { 299 + display: inline-block; 300 + margin-top: 15px; 301 + font-size: 0.9em; 302 + font-weight: bold; 303 + } 304 + 305 + 306 + /* Official Verifier Status */ 307 + .verifier-official-verifier-tooltip { 308 + cursor: help; 309 + display: inline-block; 310 + margin-left: 5px; 311 + font-weight: bold; 312 + border: 1px solid var(--text-muted, #ccc); 313 + border-radius: 50%; 314 + width: 1.2em; 315 + height: 1.2em; 316 + line-height: 1.2em; 317 + text-align: center; 318 + font-size: 0.8em; /* Smaller question mark */ 319 + color: var(--text-muted, var(--text)); 320 + } 321 + 322 + .verifier-official-verifier-note { 323 + font-size: 0.9em; 324 + margin: 5px 0; 325 + padding-left: 5px; /* Indent slightly */ 326 + } 327 + 328 + .verifier-verified-status { color: var(--success-text, green); } 329 + .verifier-not-verified-status { color: var(--text-muted, grey); } 330 + .verifier-error-status { color: var(--error-text, red); } 331 + .verifier-checking-status, .verifier-idle-status { color: var(--text-muted, grey); }
+1032
src/components/Verifier/Verifier.js
··· 1 + import React, { useState, useEffect, useRef, useCallback } from 'react'; 2 + import { useAuth } from '../../contexts/AuthContext'; // Updated import path 3 + import { Agent } from '@atproto/api'; 4 + import './Verifier.css'; // Updated CSS import 5 + 6 + // Define trusted verifiers (updated list) 7 + const TRUSTED_VERIFIERS = [ 8 + 'bsky.app', 9 + 'nytimes.com', 10 + 'wired.com', 11 + 'theathletic.bsky.social' 12 + ]; 13 + 14 + // Helper function to fetch all paginated results using a specific agent instance 15 + async function fetchAllPaginated(agentInstance, apiMethod, initialParams) { 16 + let results = []; 17 + let cursor = initialParams.cursor; 18 + const params = { ...initialParams }; 19 + let operationName = apiMethod.name; // Get the name for logging 20 + 21 + // Attempt to determine a more specific name if bound 22 + if (apiMethod.name === 'bound dispatch') { 23 + const boundFnString = apiMethod.toString(); 24 + // This is hacky, relies on internal representation which might change 25 + const match = boundFnString.match(/Target function: (\w+)/); 26 + if (match && match[1]) operationName = match[1]; 27 + } 28 + 29 + do { 30 + try { 31 + if (cursor) { 32 + params.cursor = cursor; 33 + } 34 + // Call the method bound to the correct agent context 35 + const response = await apiMethod(params); 36 + const listKey = Object.keys(response.data).find(key => Array.isArray(response.data[key])); 37 + if (listKey && response.data[listKey]) { 38 + results = results.concat(response.data[listKey]); 39 + } 40 + cursor = response.data.cursor; 41 + } catch (error) { 42 + // Use the determined operation name in the error message 43 + console.error(`Error during paginated fetch for ${operationName}:`, error); 44 + cursor = undefined; 45 + } 46 + } while (cursor); 47 + 48 + return results; 49 + } 50 + 51 + // Updated function to get PDS endpoint from PLC directory OR well-known URI for did:web 52 + async function getPdsEndpoint(did) { 53 + let didDocUrl; 54 + if (did.startsWith('did:plc:')) { 55 + didDocUrl = `https://plc.directory/${did}`; 56 + } else if (did.startsWith('did:web:')) { 57 + const domain = did.substring(8); // Extract domain after 'did:web:' 58 + // Decode percent-encoded characters in domain (e.g., for ports) 59 + const decodedDomain = decodeURIComponent(domain); 60 + didDocUrl = `https://${decodedDomain}/.well-known/did.json`; 61 + } else { 62 + console.warn(`Unsupported DID method for PDS lookup: ${did}`); 63 + return null; 64 + } 65 + 66 + try { 67 + console.log(`Fetching DID document from: ${didDocUrl}`); // Log the URL being fetched 68 + const response = await fetch(didDocUrl); 69 + if (!response.ok) { 70 + console.warn(`Could not resolve DID document for ${did} at ${didDocUrl}: ${response.status}`); 71 + return null; 72 + } 73 + const didDoc = await response.json(); 74 + const service = didDoc.service?.find(s => s.type === 'AtprotoPersonalDataServer'); 75 + const endpoint = service?.serviceEndpoint || null; 76 + if (!endpoint) { 77 + console.warn(`No AtprotoPersonalDataServer service endpoint found in DID document for ${did}`); 78 + } 79 + return endpoint; 80 + } catch (error) { 81 + console.error(`Error fetching or parsing DID document for ${did} from ${didDocUrl}:`, error); 82 + return null; 83 + } 84 + } 85 + 86 + // --- Debounce Hook --- 87 + function useDebounce(value, delay) { 88 + const [debouncedValue, setDebouncedValue] = useState(value); 89 + 90 + useEffect(() => { 91 + const handler = setTimeout(() => { 92 + setDebouncedValue(value); 93 + }, delay); 94 + 95 + // Cancel the timeout if value changes (also on delay change or unmount) 96 + return () => { 97 + clearTimeout(handler); 98 + }; 99 + }, [value, delay]); 100 + 101 + return debouncedValue; 102 + } 103 + // --- End Debounce Hook --- 104 + 105 + // Renamed component to Verifier 106 + function Verifier() { 107 + // Use the main app's AuthContext 108 + const { session, loading: isAuthLoading, error: authError, logout: signOut } = useAuth(); 109 + const [targetHandle, setTargetHandle] = useState(''); 110 + const [statusMessage, setStatusMessage] = useState(''); 111 + const [isVerifying, setIsVerifying] = useState(false); 112 + const [isRevoking, setIsRevoking] = useState(false); 113 + const [agent, setAgent] = useState(null); 114 + const [userInfo, setUserInfo] = useState(null); 115 + const [verifications, setVerifications] = useState([]); 116 + const [isLoadingVerifications, setIsLoadingVerifications] = useState(false); 117 + const [networkVerifications, setNetworkVerifications] = useState({ 118 + mutualsVerifiedMe: [], 119 + followsVerifiedMe: [], 120 + mutualsVerifiedAnyone: 0, 121 + followsVerifiedAnyone: 0, 122 + fetchedMutualsCount: 0, 123 + fetchedFollowsCount: 0, 124 + }); 125 + const [isLoadingNetwork, setIsLoadingNetwork] = useState(false); 126 + const [networkChecked, setNetworkChecked] = useState(false); 127 + const [isCheckingValidity, setIsCheckingValidity] = useState(false); 128 + const [networkStatusMessage, setNetworkStatusMessage] = useState(''); 129 + const [officialVerifiersStatus, setOfficialVerifiersStatus] = useState({}); // Stores status per verifier identifier 130 + 131 + // --- Autocomplete State --- (Keep as is) 132 + const [suggestions, setSuggestions] = useState([]); 133 + const [isFetchingSuggestions, setIsFetchingSuggestions] = useState(false); 134 + const [showSuggestions, setShowSuggestions] = useState(false); 135 + const debouncedSearchTerm = useDebounce(targetHandle, 300); // 300ms debounce 136 + const suggestionsRef = useRef(null); // Ref for suggestions container 137 + const inputRef = useRef(null); // Ref for input field 138 + // --- End Autocomplete State --- 139 + 140 + useEffect(() => { 141 + // If session exists, create an Agent instance 142 + if (session) { 143 + // The session object from the main AuthContext might be structured differently. 144 + // Assuming it has an `accessJwt` and `did` (or `sub`) 145 + const agentInstance = new Agent({ 146 + service: 'https://bsky.social', // Or get from session if available 147 + session: session // Pass the session object directly 148 + }); 149 + setAgent(agentInstance); 150 + 151 + // Fetch logged-in user's profile info using the authenticated API 152 + agentInstance.api.app.bsky.actor.getProfile({ actor: session.did /* Ensure correct DID property */ }) 153 + .then(res => { 154 + console.log('Logged-in user profile fetched successfully:', res.data); 155 + setUserInfo(res.data); 156 + }) 157 + .catch(err => { 158 + console.error("Failed to fetch user profile:", err); 159 + // Attempt to use basic info from session as fallback 160 + setUserInfo({ handle: session.handle, displayName: session.displayName || session.handle, did: session.did }); 161 + }); 162 + } else { 163 + setAgent(null); 164 + setUserInfo(null); 165 + } 166 + // No redirection here, handled by main app routing if needed 167 + }, [session]); 168 + 169 + // Fetch all verification records created by the current user 170 + const fetchVerifications = async () => { 171 + if (!agent || !session) return; 172 + 173 + setIsLoadingVerifications(true); 174 + try { 175 + const response = await agent.api.com.atproto.repo.listRecords({ 176 + repo: session.did, // Use session.did 177 + collection: 'app.bsky.graph.verification', 178 + limit: 100, 179 + }); 180 + 181 + console.log('Fetched verifications:', response.data); 182 + 183 + // If we have records, set them in state 184 + if (response.data.records) { 185 + const formattedVerifications = response.data.records.map(record => ({ 186 + uri: record.uri, 187 + cid: record.cid, 188 + handle: record.value.handle, 189 + displayName: record.value.displayName, 190 + subject: record.value.subject, 191 + createdAt: record.value.createdAt, 192 + isValid: true, // Default, will be checked later 193 + validityChecked: false 194 + })); 195 + setVerifications(formattedVerifications); 196 + 197 + // Check validity of each verification 198 + checkVerificationsValidity(formattedVerifications); 199 + } else { 200 + setVerifications([]); 201 + } 202 + } catch (error) { 203 + console.error('Failed to fetch verifications:', error); 204 + setStatusMessage(`Failed to load verifications: ${error.message || 'Unknown error'}`); 205 + } finally { 206 + setIsLoadingVerifications(false); 207 + } 208 + }; 209 + 210 + // Check if verifications are still valid (handle/displayName still match) 211 + const checkVerificationsValidity = async (verificationsList) => { 212 + if (!agent || verificationsList.length === 0) return; 213 + 214 + setIsCheckingValidity(true); 215 + const updatedVerifications = [...verificationsList]; 216 + 217 + try { 218 + // Process in batches to avoid too many concurrent requests 219 + const batchSize = 5; 220 + for (let i = 0; i < updatedVerifications.length; i += batchSize) { 221 + const batch = updatedVerifications.slice(i, i + batchSize); 222 + 223 + await Promise.all(batch.map(async (verification, index) => { 224 + try { 225 + // Get current profile data 226 + const profileRes = await agent.api.app.bsky.actor.getProfile({ 227 + actor: verification.handle 228 + }); 229 + 230 + // Check if handle and displayName still match 231 + const currentHandle = profileRes.data.handle; 232 + const currentDisplayName = profileRes.data.displayName || profileRes.data.handle; 233 + 234 + // Update verification validity 235 + const batchIndex = i + index; 236 + updatedVerifications[batchIndex].validityChecked = true; 237 + updatedVerifications[batchIndex].isValid = 238 + currentHandle === verification.handle && 239 + currentDisplayName === verification.displayName; 240 + 241 + // If not valid, store current values for reference 242 + if (!updatedVerifications[batchIndex].isValid) { 243 + updatedVerifications[batchIndex].currentHandle = currentHandle; 244 + updatedVerifications[batchIndex].currentDisplayName = currentDisplayName; 245 + } 246 + 247 + // Update state as we go to show progress 248 + setVerifications([...updatedVerifications]); 249 + } catch (err) { 250 + console.error(`Failed to check validity for ${verification.handle}:`, err); 251 + // Mark as could not check 252 + const batchIndex = i + index; 253 + updatedVerifications[batchIndex].validityChecked = true; 254 + updatedVerifications[batchIndex].isValid = false; 255 + updatedVerifications[batchIndex].validityError = true; 256 + } 257 + })); 258 + } 259 + 260 + console.log('Verified all records validity:', updatedVerifications); 261 + } catch (error) { 262 + console.error('Failed to check verifications validity:', error); 263 + } finally { 264 + setIsCheckingValidity(false); 265 + } 266 + }; 267 + 268 + // Updated function: Check mutuals (authenticated) and all follows (public) 269 + const checkNetworkVerifications = async () => { 270 + // Ensure authenticated agent is available for mutuals check 271 + if (!agent || !session || !userInfo) return; 272 + 273 + setIsLoadingNetwork(true); 274 + setNetworkChecked(false); 275 + // Reset state 276 + setNetworkVerifications({ 277 + mutualsVerifiedMe: [], followsVerifiedMe: [], 278 + mutualsVerifiedAnyone: 0, followsVerifiedAnyone: 0, 279 + fetchedMutualsCount: 0, fetchedFollowsCount: 0 280 + }); 281 + setNetworkStatusMessage("Fetching network lists (mutuals, follows)..."); 282 + 283 + const publicAgent = new Agent({ service: 'https://public.api.bsky.app' }); 284 + 285 + try { 286 + // Fetch follows (public) and known followers/mutuals (authenticated) 287 + const [follows, mutuals] = await Promise.all([ 288 + fetchAllPaginated(publicAgent, publicAgent.api.app.bsky.graph.getFollows.bind(publicAgent.api.app.bsky.graph), { actor: session.did, limit: 100 }), // Use session.did 289 + // Use the main authenticated agent for getKnownFollowers 290 + fetchAllPaginated(agent, agent.api.app.bsky.graph.getKnownFollowers.bind(agent.api.app.bsky.graph), { actor: session.did, limit: 100 }) // Use session.did 291 + ]); 292 + 293 + console.log(`Fetched ${follows.length} follows (public), ${mutuals.length} mutuals (authenticated).`); // Updated log 294 + setNetworkStatusMessage(`Fetched ${follows.length} follows, ${mutuals.length} mutuals. Discovering PDS and checking verifications...`); 295 + 296 + // Update fetched counts 297 + setNetworkVerifications(prev => ({ 298 + ...prev, 299 + fetchedMutualsCount: mutuals.length, 300 + fetchedFollowsCount: follows.length, 301 + })); 302 + 303 + const followsSet = new Set(follows.map(f => f.did)); 304 + const mutualsSet = new Set(mutuals.map(m => m.did)); 305 + 306 + const allProfilesMap = new Map(); 307 + [...follows, ...mutuals].forEach(user => { 308 + if (!allProfilesMap.has(user.did)) { 309 + allProfilesMap.set(user.did, user); 310 + } 311 + }); 312 + 313 + const uniqueUserDids = Array.from(allProfilesMap.keys()); 314 + 315 + if (uniqueUserDids.length === 0) { 316 + setNetworkStatusMessage("No mutuals or follows found to check."); 317 + setIsLoadingNetwork(false); 318 + setNetworkChecked(true); 319 + return; 320 + } 321 + 322 + let results = { 323 + mutualsVerifiedMe: [], followsVerifiedMe: [], 324 + mutualsVerifiedAnyone: 0, followsVerifiedAnyone: 0 325 + }; 326 + 327 + const batchSize = 5; 328 + for (let i = 0; i < uniqueUserDids.length; i += batchSize) { 329 + const batchDids = uniqueUserDids.slice(i, i + batchSize); 330 + setNetworkStatusMessage(`Checking network... (${i + batchDids.length}/${uniqueUserDids.length})`); 331 + 332 + await Promise.all(batchDids.map(async (did) => { 333 + const profile = allProfilesMap.get(did); 334 + if (!profile) return; 335 + 336 + const isMutual = mutualsSet.has(did); 337 + const isFollow = followsSet.has(did); 338 + 339 + const pdsEndpoint = await getPdsEndpoint(did); 340 + if (!pdsEndpoint) { 341 + console.warn(`Skipping verification check for ${profile.handle} (no PDS found).`); 342 + return; 343 + } 344 + 345 + let foundVerificationForMe = null; 346 + let hasVerifiedAnyone = false; 347 + let listRecordsCursor = undefined; 348 + 349 + do { 350 + try { 351 + const listParams = new URLSearchParams({ repo: did, collection: 'app.bsky.graph.verification', limit: '100' }); 352 + if (listRecordsCursor) listParams.set('cursor', listRecordsCursor); 353 + const listRecordsUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.listRecords?${listParams.toString()}`; 354 + const listResponse = await fetch(listRecordsUrl); 355 + if (!listResponse.ok) { break; } 356 + const listData = await listResponse.json(); 357 + const records = listData.records || []; 358 + if (records.length > 0) { 359 + hasVerifiedAnyone = true; 360 + const matchingRecord = records.find(record => record.value?.subject === session.did); // Use session.did 361 + if (matchingRecord) { foundVerificationForMe = matchingRecord; break; } 362 + } 363 + listRecordsCursor = listData.cursor; 364 + } catch (err) { 365 + console.error(`Network error fetching listRecords for ${did} from ${pdsEndpoint}:`, err); 366 + listRecordsCursor = undefined; 367 + } 368 + } while (listRecordsCursor); 369 + 370 + if (hasVerifiedAnyone) { 371 + if (isMutual) results.mutualsVerifiedAnyone++; 372 + if (isFollow) results.followsVerifiedAnyone++; 373 + } 374 + if (foundVerificationForMe) { 375 + const accountInfo = { ...profile, verification: foundVerificationForMe }; 376 + if (isMutual) results.mutualsVerifiedMe.push(accountInfo); 377 + if (isFollow) results.followsVerifiedMe.push(accountInfo); 378 + } 379 + 380 + })); 381 + 382 + setNetworkVerifications(prev => ({ 383 + ...prev, 384 + mutualsVerifiedMe: [...results.mutualsVerifiedMe], 385 + followsVerifiedMe: [...results.followsVerifiedMe], 386 + mutualsVerifiedAnyone: results.mutualsVerifiedAnyone, 387 + followsVerifiedAnyone: results.followsVerifiedAnyone, 388 + })); 389 + } 390 + 391 + console.log('Network check complete. Results:', results); 392 + setNetworkStatusMessage("Network verification check complete."); 393 + 394 + } catch (error) { 395 + // Catch errors from initial Promise.all or other setup issues 396 + console.error('Fatal error during network verification check:', error); 397 + setStatusMessage(`Fatal error checking network: ${error.message || 'Unknown error'}`); 398 + setNetworkStatusMessage(""); 399 + } finally { 400 + setIsLoadingNetwork(false); 401 + setNetworkChecked(true); 402 + } 403 + }; 404 + 405 + // Call fetchVerifications when agent is available 406 + useEffect(() => { 407 + if (agent) { 408 + fetchVerifications(); 409 + } 410 + }, [agent]); 411 + 412 + // Updated function to check each official verifier individually 413 + const checkOfficialVerification = async () => { 414 + if (!agent || !session) return; 415 + 416 + // Initialize status for all verifiers to 'checking' 417 + const initialStatuses = {}; 418 + TRUSTED_VERIFIERS.forEach(id => { initialStatuses[id] = 'checking'; }); 419 + setOfficialVerifiersStatus(initialStatuses); 420 + 421 + const publicAgent = new Agent({ service: 'https://public.api.bsky.app' }); 422 + 423 + // Use Promise.all to run checks concurrently (optional, but can be faster) 424 + await Promise.all(TRUSTED_VERIFIERS.map(async (verifierIdentifier) => { 425 + let verifierDid = null; 426 + let verifierHandle = verifierIdentifier; 427 + let currentStatus = 'checking'; // Status for this specific verifier 428 + 429 + try { 430 + // Resolve handle/DID 431 + if (!verifierIdentifier.startsWith('did:')) { 432 + const resolveResult = await publicAgent.resolveHandle({ handle: verifierIdentifier }); 433 + verifierDid = resolveResult.data.did; 434 + } else { 435 + verifierDid = verifierIdentifier; 436 + try { 437 + const profileRes = await publicAgent.api.app.bsky.actor.getProfile({ actor: verifierDid }); 438 + verifierHandle = profileRes.data.handle; 439 + } catch (profileError) { /* ignore */ } 440 + } 441 + if (!verifierDid) throw new Error('Could not resolve identifier'); 442 + 443 + // Discover PDS 444 + const pdsEndpoint = await getPdsEndpoint(verifierDid); 445 + if (!pdsEndpoint) throw new Error('Could not find PDS'); 446 + 447 + // Paginate through their listRecords 448 + let listRecordsCursor = undefined; 449 + let foundMatch = false; 450 + do { 451 + const listParams = new URLSearchParams({ repo: verifierDid, collection: 'app.bsky.graph.verification', limit: '100' }); 452 + if (listRecordsCursor) listParams.set('cursor', listRecordsCursor); 453 + const listRecordsUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.listRecords?${listParams.toString()}`; 454 + const listResponse = await fetch(listRecordsUrl); 455 + 456 + if (!listResponse.ok) { 457 + // Treat 400 (repo/collection not found) as simply not verified by this one 458 + if (listResponse.status !== 400) { 459 + console.warn(`Failed fetch for ${verifierHandle}: ${listResponse.status}`); 460 + throw new Error(`Fetch failed with status ${listResponse.status}`); // Throw for other errors 461 + } 462 + break; // Stop checking this verifier on 400 or other errors 463 + } 464 + 465 + const listData = await listResponse.json(); 466 + const records = listData.records || []; 467 + const matchingRecord = records.find(record => record.value?.subject === session.did); // Use session.did 468 + 469 + if (matchingRecord) { 470 + console.log(`Found official verification by ${verifierHandle}`); 471 + currentStatus = 'verified'; 472 + foundMatch = true; 473 + break; // Exit pagination loop for THIS verifier 474 + } 475 + listRecordsCursor = listData.cursor; 476 + } while (listRecordsCursor); 477 + 478 + // If loop completed without finding a match for this verifier 479 + if (!foundMatch) { 480 + currentStatus = 'not_verified'; 481 + } 482 + 483 + } catch (error) { 484 + console.error(`Error checking official verifier ${verifierIdentifier}:`, error); 485 + currentStatus = 'error'; // Set status to error for this specific verifier 486 + } 487 + 488 + // Update the state for this specific verifier 489 + setOfficialVerifiersStatus(prev => ({ ...prev, [verifierIdentifier]: currentStatus })); 490 + 491 + })); // End Promise.all map 492 + 493 + console.log("Finished checking all official verifiers."); 494 + 495 + }; // End checkOfficialVerification 496 + 497 + // Effect to check official verification status on load 498 + useEffect(() => { 499 + // Run check when agent/session are ready 500 + if (agent && session?.did) { // Changed from session.sub 501 + checkOfficialVerification(); 502 + } 503 + // Run once when agent/session become available 504 + }, [agent, session]); 505 + 506 + // --- Fetch Autocomplete Suggestions --- (Keep as is) 507 + const fetchSuggestions = useCallback(async (query) => { 508 + if (!query || query.trim().length < 2) { // Minimum 2 chars to search 509 + setSuggestions([]); 510 + // Don't explicitly setShowSuggestions(false) here, let onChange handle it 511 + return; 512 + } 513 + 514 + setIsFetchingSuggestions(true); 515 + // Don't set showSuggestions(true) here either, should be true already if we got here 516 + 517 + try { 518 + const url = new URL('https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead'); 519 + url.searchParams.append('q', query); 520 + url.searchParams.append('limit', '5'); // Fetch 5 suggestions 521 + 522 + const response = await fetch(url.toString()); 523 + if (!response.ok) { 524 + throw new Error(`HTTP error! status: ${response.status}`); 525 + } 526 + const data = await response.json(); 527 + console.log('Suggestions fetched:', data.actors); 528 + setSuggestions(data.actors || []); 529 + } catch (error) { 530 + console.error('Failed to fetch suggestions:', error); 531 + setSuggestions([]); // Clear suggestions on error 532 + } finally { 533 + setIsFetchingSuggestions(false); 534 + } 535 + }, []); 536 + 537 + // Effect to fetch suggestions based on debounced search term AND if suggestions should be shown 538 + useEffect(() => { 539 + // Only fetch if the user is likely typing (suggestions are meant to be shown) 540 + // and the term is long enough. 541 + if (debouncedSearchTerm && showSuggestions) { 542 + fetchSuggestions(debouncedSearchTerm); 543 + } else if (!debouncedSearchTerm) { // Always clear if term is empty 544 + setSuggestions([]); 545 + // setShowSuggestions(false); // Let onChange handle hiding when empty 546 + } 547 + // If showSuggestions is false (e.g., after a click), this effect won't trigger a fetch. 548 + }, [debouncedSearchTerm, fetchSuggestions, showSuggestions]); // Add showSuggestions dependency 549 + 550 + // --- Click Outside Handler for Suggestions --- (Keep as is) 551 + useEffect(() => { 552 + function handleClickOutside(event) { 553 + if (suggestionsRef.current && !suggestionsRef.current.contains(event.target) && 554 + inputRef.current && !inputRef.current.contains(event.target)) { 555 + setShowSuggestions(false); 556 + } 557 + } 558 + // Bind the event listener 559 + document.addEventListener("mousedown", handleClickOutside); 560 + return () => { 561 + // Unbind the event listener on clean up 562 + document.removeEventListener("mousedown", handleClickOutside); 563 + }; 564 + }, [suggestionsRef, inputRef]); // Add inputRef dependency 565 + // --- End Click Outside Handler --- 566 + 567 + // --- handleVerify --- (Keep as is) 568 + const handleVerify = async (e) => { 569 + e.preventDefault(); 570 + if (!agent || !session) { 571 + setStatusMessage('Error: Not logged in or agent not initialized.'); 572 + return; 573 + } 574 + if (!targetHandle) { 575 + setStatusMessage('Please enter a handle to verify.'); 576 + return; 577 + } 578 + setIsVerifying(true); 579 + setStatusMessage(`Verifying ${targetHandle}...`); 580 + setShowSuggestions(false); // Hide suggestions when submitting 581 + 582 + try { 583 + // 1. Get profile of targetHandle (resolve handle to DID and get display name) 584 + setStatusMessage(`Fetching profile for ${targetHandle}...`); 585 + // Use the proper API namespace method 586 + const profileRes = await agent.api.app.bsky.actor.getProfile({ actor: targetHandle }); 587 + const targetDid = profileRes.data.did; 588 + const targetDisplayName = profileRes.data.displayName || profileRes.data.handle; 589 + console.log('Target Profile:', profileRes.data); 590 + 591 + // 2. Construct the verification record object 592 + const verificationRecord = { 593 + $type: 'app.bsky.graph.verification', // Using the type you provided 594 + subject: targetDid, 595 + handle: targetHandle, // Include handle for context 596 + displayName: targetDisplayName, // Include display name 597 + createdAt: new Date().toISOString(), 598 + }; 599 + console.log('Verification Record to Create:', verificationRecord); 600 + 601 + // 3. Create the record using the agent's com.atproto.repo.createRecord method 602 + setStatusMessage(`Creating verification record for ${targetHandle} on your profile...`); 603 + 604 + // The correct method is repo.createRecord, not createRecord 605 + const createRes = await agent.api.com.atproto.repo.createRecord({ 606 + repo: session.did, // Use session.did 607 + collection: 'app.bsky.graph.verification', // The NSID of the record type 608 + record: verificationRecord, 609 + }); 610 + 611 + console.log('Create Record Response:', createRes); 612 + 613 + // --- Construct Success Message with Intent Link --- (Keep as is) 614 + const verifiedHandle = targetHandle; // Capture handle for this success 615 + const postText = `I just verified @${verifiedHandle} using Bluesky's new decentralized verification system. Try verifying someone yourself using @cred.blue's new verification tool: https://cred.blue/verify`; 616 + const encodedText = encodeURIComponent(postText); 617 + const intentUrl = `https://bsky.app/intent/compose?text=${encodedText}`; 618 + 619 + const successMessageJSX = ( 620 + <> 621 + Successfully created verification record for {verifiedHandle}!{' '} 622 + <a href={intentUrl} target="_blank" rel="noopener noreferrer" className="verifier-intent-link"> {/* Use plain class */} 623 + Post on Bluesky to let them know. 624 + </a> 625 + </> 626 + ); 627 + setStatusMessage(successMessageJSX); // Set JSX as status message 628 + // --- End Intent Link Construction --- 629 + 630 + setTargetHandle(''); // Clear input on success 631 + 632 + // Refresh the list of verifications 633 + fetchVerifications(); 634 + 635 + } catch (error) { 636 + console.error('Verification failed:', error); 637 + setStatusMessage(`Verification failed: ${error.message || 'Unknown error'}`); 638 + } finally { 639 + setIsVerifying(false); 640 + } 641 + }; 642 + // --- End handleVerify --- 643 + 644 + // Function to revoke (delete) a verification - (Keep as is) 645 + const handleRevoke = async (verification) => { 646 + if (!agent || !session) { 647 + setStatusMessage('Error: Not logged in or agent not initialized.'); 648 + return; 649 + } 650 + 651 + setIsRevoking(true); 652 + setStatusMessage(`Revoking verification for ${verification.handle}...`); 653 + 654 + try { 655 + // Extract rkey from URI 656 + // URI format: at://did:plc:xxx/app.bsky.graph.verification/rkey 657 + const parts = verification.uri.split('/'); 658 + const rkey = parts[parts.length - 1]; 659 + 660 + await agent.api.com.atproto.repo.deleteRecord({ 661 + repo: session.did, // Use session.did 662 + collection: 'app.bsky.graph.verification', 663 + rkey: rkey 664 + }); 665 + 666 + console.log('Revoked verification for:', verification.handle); 667 + setStatusMessage(`Successfully revoked verification for ${verification.handle}`); 668 + 669 + // Refresh the list of verifications 670 + fetchVerifications(); 671 + } catch (error) { 672 + console.error('Revocation failed:', error); 673 + setStatusMessage(`Revocation failed: ${error.message || 'Unknown error'}`); 674 + } finally { 675 + setIsRevoking(false); 676 + } 677 + }; 678 + 679 + // --- handleSuggestionClick --- (Keep as is) 680 + const handleSuggestionClick = (handle) => { 681 + setTargetHandle(handle); 682 + setSuggestions([]); 683 + setShowSuggestions(false); 684 + inputRef.current?.focus(); // Keep focus on input after selection 685 + }; 686 + // --- End handleSuggestionClick --- 687 + 688 + // AuthProvider handles redirection if not logged in during its initial load 689 + if (isAuthLoading) { 690 + return <p>Loading authentication...</p>; 691 + } 692 + 693 + if (authError) { 694 + return <p>Authentication Error: {authError}. <a href="/login">Please login</a>.</p>; 695 + } 696 + 697 + // Display message if session is not available but not loading/erroring 698 + if (!session && !isAuthLoading && !authError) { 699 + return ( 700 + <div className="verifier-container"> 701 + <h1>Bluesky Verifier Tool</h1> 702 + <p>Please <a href="/login">login with Bluesky</a> to use the verifier tool.</p> 703 + </div> 704 + ); 705 + } 706 + 707 + // Update combined loading state 708 + const isAnyOperationInProgress = isVerifying || isRevoking || isLoadingVerifications || isLoadingNetwork || isCheckingValidity; 709 + 710 + // Update tooltip construction to use the (potentially resolved) handles 711 + const trustedVerifiersTooltip = `Checking if any of these Trusted Verifiers have created a verification record for your DID: ${TRUSTED_VERIFIERS.join(', ')}.`; 712 + 713 + // Use standard class names, not styles object 714 + return ( 715 + <div className="verifier-container"> 716 + <h1>Bluesky Verifier Tool</h1> 717 + <p className="verifier-intro-text"> 718 + With Bluesky's new decentralized verification system, anyone can verify anyone else and any Bluesky client can choose which accounts to treat as "Trusted Verifiers". It's a first-of-its-kind verification system for a mainstream social platform of this size. Try verifying an account for yourself or check to see who has verified you! 719 + </p> 720 + <div className="verifier-page-header"> 721 + <p className="verifier-user-info">Logged in as: {userInfo ? `${userInfo.displayName} (@${userInfo.handle})` : session?.did}</p> {/* Safely access session.did */} 722 + <button 723 + onClick={signOut} 724 + disabled={isAnyOperationInProgress} 725 + className="verifier-sign-out-button" 726 + > 727 + Sign Out 728 + </button> 729 + </div> 730 + <hr /> 731 + 732 + {/* Verification form */} 733 + <div className="verifier-section"> 734 + <h2>Verify a Bluesky User</h2> 735 + <p>Enter the handle of the user you want to verify (e.g., targetuser.bsky.social):</p> 736 + {/* --- Input Container for Autocomplete Positioning --- */} 737 + <div className="verifier-input-container"> 738 + <form onSubmit={handleVerify} className="verifier-form-container" style={{ marginBottom: 0 }}> 739 + <input 740 + ref={inputRef} // Assign ref 741 + type="text" 742 + value={targetHandle} 743 + onChange={(e) => { 744 + const newValue = e.target.value; 745 + setTargetHandle(newValue); 746 + if (newValue.length >= 2) { 747 + setShowSuggestions(true); 748 + } else { 749 + setShowSuggestions(false); 750 + setSuggestions([]); 751 + } 752 + }} 753 + onFocus={() => { 754 + if (targetHandle.length >= 2) { 755 + setShowSuggestions(true); 756 + } 757 + }} 758 + placeholder="targetuser.bsky.social" 759 + disabled={isAnyOperationInProgress} 760 + required 761 + className="verifier-input-field" 762 + autoComplete="off" 763 + /> 764 + <button 765 + type="submit" 766 + disabled={isVerifying || isRevoking || isLoadingVerifications || isLoadingNetwork || isCheckingValidity} 767 + className="verifier-submit-button" 768 + > 769 + {isVerifying ? 'Verifying...' : 'Create Verification Record'} 770 + </button> 771 + </form> 772 + {/* --- Suggestions Dropdown --- */} 773 + {showSuggestions && (suggestions.length > 0 || isFetchingSuggestions) && ( 774 + <ul className="verifier-suggestions-list" ref={suggestionsRef}> 775 + {isFetchingSuggestions && suggestions.length === 0 ? ( 776 + <li className="verifier-suggestion-item">Loading...</li> 777 + ) : ( 778 + suggestions.map((actor) => ( 779 + <li 780 + key={actor.did} 781 + className="verifier-suggestion-item" 782 + onMouseDown={(e) => { 783 + e.preventDefault(); 784 + handleSuggestionClick(actor.handle); 785 + }} 786 + > 787 + <img src={actor.avatar} alt="" className="verifier-suggestion-avatar" /> 788 + <div className="verifier-suggestion-text"> 789 + <span className="verifier-suggestion-display-name">{actor.displayName || actor.handle}</span> 790 + <span className="verifier-suggestion-handle">@{actor.handle}</span> 791 + </div> 792 + </li> 793 + )) 794 + )} 795 + {suggestions.length === 0 && !isFetchingSuggestions && targetHandle.length >= 2 && ( 796 + <li className="verifier-suggestion-item">No users found matching "{targetHandle}"</li> 797 + )} 798 + </ul> 799 + )} 800 + </div> 801 + </div> 802 + 803 + {/* Global Status message */} 804 + {statusMessage && ( 805 + <div className={` 806 + verifier-status-box 807 + ${ typeof statusMessage === 'string' && (statusMessage.includes('failed') || statusMessage.includes('Error')) 808 + ? 'verifier-status-box-error' 809 + : 'verifier-status-box-success' 810 + } 811 + `}> 812 + <p>{statusMessage}</p> 813 + </div> 814 + )} 815 + 816 + {/* Updated Official Verifiers section */} 817 + <div className="verifier-section"> 818 + <div style={{display: 'flex', alignItems: 'center', marginBottom: '10px'}}> 819 + <h2 style={{ display: 'inline-block', marginRight: '8px', marginBottom: 0, border: 'none', padding: 0 }}>Your Verification Status</h2> 820 + <span 821 + title={trustedVerifiersTooltip} 822 + className="verifier-official-verifier-tooltip" 823 + style={{ fontSize: '1.2em' }} 824 + > 825 + (?) 826 + </span> 827 + </div> 828 + 829 + {/* Map over trusted verifiers and display individual status */} 830 + <div> 831 + {TRUSTED_VERIFIERS.map(verifierId => { 832 + const status = officialVerifiersStatus[verifierId] || 'idle'; 833 + let message = '...'; 834 + let icon = '⏳'; 835 + let statusClass = ''; 836 + 837 + switch (status) { 838 + case 'checking': 839 + message = `Checking ${verifierId}...`; 840 + icon = '⏳'; 841 + statusClass = 'verifier-checking-status'; 842 + break; 843 + case 'verified': 844 + message = `Verified by ${verifierId}.`; 845 + icon = '✅'; 846 + statusClass = 'verifier-verified-status'; 847 + break; 848 + case 'not_verified': 849 + message = `Not verified by ${verifierId}.`; 850 + icon = '❌'; 851 + statusClass = 'verifier-not-verified-status'; 852 + break; 853 + case 'error': 854 + message = `Error checking ${verifierId}.`; 855 + icon = '⚠️'; 856 + statusClass = 'verifier-error-status'; 857 + break; 858 + default: // idle 859 + message = `Pending check for ${verifierId}.`; 860 + icon = '⏳'; 861 + statusClass = 'verifier-idle-status'; 862 + } 863 + 864 + return ( 865 + <p key={verifierId} className={`verifier-official-verifier-note ${statusClass}`}> 866 + {icon} {message} 867 + </p> 868 + ); 869 + })} 870 + </div> 871 + </div> 872 + 873 + {/* Updated section for Network Verifications */} 874 + <div className="verifier-section"> 875 + <div className="verifier-list-header"> 876 + <h2>Who's Verified You?</h2> 877 + <button 878 + onClick={checkNetworkVerifications} 879 + disabled={isAnyOperationInProgress} 880 + className="verifier-action-button verifier-check-network-button" 881 + > 882 + {isLoadingNetwork ? 'Checking Network...' : 'Check Network Now'} 883 + </button> 884 + </div> 885 + 886 + {/* Display local status message */} 887 + {(isLoadingNetwork || networkStatusMessage) && ( 888 + <p className="verifier-network-status">{networkStatusMessage}</p> 889 + )} 890 + 891 + {!isLoadingNetwork && networkChecked && ( 892 + <div className="verifier-network-results"> 893 + {/* --- Mutuals Verified Me --- */} 894 + <p> 895 + {networkVerifications.mutualsVerifiedMe.length > 0 896 + ? `${networkVerifications.mutualsVerifiedMe.length} mutual(s) have verified you:` 897 + : "None of your mutuals have verified you yet."} 898 + </p> 899 + {networkVerifications.mutualsVerifiedMe.length > 0 && ( 900 + <ul className="verifier-verifier-list"> 901 + {networkVerifications.mutualsVerifiedMe.map(account => ( 902 + <li key={account.did}> 903 + {account.displayName} (@{account.handle}) 904 + </li> 905 + ))} 906 + </ul> 907 + )} 908 + 909 + {/* --- Follows Verified Me --- */} 910 + <p style={{marginTop: '15px'}}> 911 + {networkVerifications.followsVerifiedMe.length > 0 912 + ? `${networkVerifications.followsVerifiedMe.length} account(s) you follow have verified you:` 913 + : "None of the accounts you follow have verified you yet."} 914 + </p> 915 + {networkVerifications.followsVerifiedMe.length > 0 && ( 916 + <ul className="verifier-verifier-list"> 917 + {networkVerifications.followsVerifiedMe.map(account => ( 918 + <li key={account.did}> 919 + {account.displayName} (@{account.handle}) 920 + </li> 921 + ))} 922 + </ul> 923 + )} 924 + 925 + {/* --- Additional Context - Verified Others --- */} 926 + <div className="verifier-additional-context"> 927 + <p> 928 + {networkVerifications.mutualsVerifiedAnyone} of your {networkVerifications.fetchedMutualsCount} fetched mutuals have verified others. 929 + </p> 930 + <p> 931 + {networkVerifications.followsVerifiedAnyone} of the {networkVerifications.fetchedFollowsCount} accounts you follow have verified others. 932 + </p> 933 + </div> 934 + 935 + {/* --- Network Stats Share Link --- */} 936 + {(() => { // IIFE to encapsulate logic 937 + const statsText = `Here are my expanded verification stats:\n\n` + 938 + `${networkVerifications.mutualsVerifiedMe.length} of my mutuals have verified me\n` + 939 + `${networkVerifications.followsVerifiedMe.length} account(s) that I follow have verified me\n` + 940 + `${networkVerifications.mutualsVerifiedAnyone} of my ${networkVerifications.fetchedMutualsCount} mutuals have verified others\n` + 941 + `${networkVerifications.followsVerifiedAnyone} of the ${networkVerifications.fetchedFollowsCount} accounts I follow have verified others\n\n` + 942 + `See who in your network has verified you here: https://cred.blue/verify`; 943 + const encodedStatsText = encodeURIComponent(statsText); 944 + const statsIntentUrl = `https://bsky.app/intent/compose?text=${encodedStatsText}`; 945 + 946 + return ( 947 + <a 948 + href={statsIntentUrl} 949 + target="_blank" 950 + rel="noopener noreferrer" 951 + className="verifier-share-stats-link" 952 + > 953 + Share your verification stats on Bluesky! 954 + </a> 955 + ); 956 + })()} 957 + 958 + </div> 959 + )} 960 + {!isLoadingNetwork && !networkChecked && ( 961 + <p>Click "Check Network Now" to see verifications from your network.</p> 962 + )} 963 + </div> 964 + 965 + {/* List of verified accounts */} 966 + <div className="verifier-section"> 967 + <div className="verifier-list-header"> 968 + <h2>Accounts You've Verified</h2> 969 + <button 970 + onClick={() => fetchVerifications()} 971 + disabled={isAnyOperationInProgress} 972 + className="verifier-action-button verifier-refresh-button" 973 + > 974 + Refresh List 975 + </button> 976 + </div> 977 + {isLoadingVerifications ? ( 978 + <p>Loading verifications...</p> 979 + ) : verifications.length === 0 ? ( 980 + <p>You haven't verified any accounts yet.</p> 981 + ) : ( 982 + <ul className="verifier-list"> 983 + {verifications.map((verification) => ( 984 + <li 985 + key={verification.uri} 986 + className={` 987 + verifier-list-item 988 + ${verification.validityChecked && !verification.isValid ? 'verifier-list-item-invalid' : ''} 989 + `} 990 + > 991 + <div className="verifier-list-item-content"> 992 + <div style={{ fontWeight: 'bold' }}>{verification.displayName}</div> 993 + <div className="verifier-list-item-handle">@{verification.handle}</div> 994 + <div className="verifier-list-item-date"> 995 + Verified: {new Date(verification.createdAt).toLocaleString()} 996 + </div> 997 + 998 + {verification.validityChecked && !verification.isValid && ( 999 + <div className="verifier-validity-warning"> 1000 + {verification.validityError ? ( 1001 + <p>⚠️ Could not verify current profile data</p> 1002 + ) : ( 1003 + <> 1004 + <p><strong>⚠️ Profile has changed since verification</strong></p> 1005 + <p> 1006 + <span>Current handle: @{verification.currentHandle}</span><br /> 1007 + <span>Current display name: {verification.currentDisplayName}</span> 1008 + </p> 1009 + </> 1010 + )} 1011 + </div> 1012 + )} 1013 + </div> 1014 + <div className="verifier-list-item-actions"> 1015 + <button 1016 + onClick={() => handleRevoke(verification)} 1017 + disabled={isAnyOperationInProgress} 1018 + className="verifier-revoke-button" 1019 + > 1020 + {isRevoking ? 'Revoking...' : 'Revoke Verification'} 1021 + </button> 1022 + </div> 1023 + </li> 1024 + ))} 1025 + </ul> 1026 + )} 1027 + </div> 1028 + </div> 1029 + ); 1030 + } 1031 + 1032 + export default Verifier;