This repository has no description
0

Configure Feed

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

Add revoke search, pagination, and time revoke

+354 -55
+34
src/components/Verifier/Verifier.css
··· 533 533 .verifier-options input[type="checkbox"] { 534 534 cursor: pointer; 535 535 margin: 0px; 536 + } 537 + 538 + /* Styles for Time-Based Revocation */ 539 + .verifier-time-revoke-wrapper { 540 + margin-top: 15px; /* Add some space above the time controls */ 541 + } 542 + 543 + .verifier-time-revoke-wrapper p { 544 + margin-bottom: 10px; /* Space between description and radios */ 545 + color: var(--text); 546 + } 547 + 548 + .verifier-time-range-selector { 549 + display: flex; 550 + flex-direction: column; /* Stack radio buttons vertically */ 551 + gap: 10px; /* Space between radio buttons */ 552 + margin-bottom: 15px; /* Space below the radio group */ 553 + } 554 + 555 + .verifier-time-range-selector label { 556 + cursor: pointer; 557 + display: flex; 558 + align-items: center; 559 + gap: 8px; /* Space between radio and text */ 560 + color: var(--text); 561 + } 562 + 563 + .verifier-time-range-selector input[type="radio"] { 564 + cursor: pointer; 565 + margin: 0; 566 + } 567 + 568 + .verifier-time-revoke-wrapper .verifier-revoke-button { 569 + margin-top: 5px; /* Adjust if needed */ 536 570 }
+320 -55
src/components/Verifier/Verifier.js
··· 1 - import React, { useState, useEffect, useCallback, useRef } from 'react'; 1 + import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; 2 2 import { useAuth } from '../../contexts/AuthContext'; 3 3 import { Agent } from '@atproto/api'; 4 4 import './Verifier.css'; ··· 151 151 const [bulkVerifyProgress, setBulkVerifyProgress] = useState(''); // Progress indicator (e.g., "10/50") 152 152 153 153 // State for list revocation 154 - const [revokeMode, setRevokeMode] = useState('single'); // 'single' or 'list' 154 + const [revokeMode, setRevokeMode] = useState('single'); // 'single' or 'list' or 'time' 155 155 const [selectedListUriForRevoke, setSelectedListUriForRevoke] = useState(''); 156 156 const [bulkRevokeStatus, setBulkRevokeStatus] = useState(''); // Status message for bulk revoke 157 157 const [bulkRevokeProgress, setBulkRevokeProgress] = useState(''); // Progress for bulk revoke 158 + 159 + // State for filtering verified accounts 160 + const [verificationSearchTerm, setVerificationSearchTerm] = useState(''); 161 + 162 + // State for time-based revocation 163 + const [revokeTimeRange, setRevokeTimeRange] = useState('30m'); // Default: 30 minutes 164 + 165 + // State for verification list pagination 166 + const [verificationsCursor, setVerificationsCursor] = useState(null); 167 + const [isLoadingMoreVerifications, setIsLoadingMoreVerifications] = useState(false); 158 168 159 169 // Verification options 160 170 const [skipDuplicates, setSkipDuplicates] = useState(true); ··· 181 191 } 182 192 }, [session]); 183 193 184 - const fetchVerifications = useCallback(async () => { 194 + const fetchVerifications = useCallback(async (cursor) => { 185 195 if (!agent || !session) return; 186 - setIsLoadingVerifications(true); 196 + 197 + // Determine loading state based on whether a cursor is provided 198 + if (cursor) { 199 + setIsLoadingMoreVerifications(true); 200 + } else { 201 + setIsLoadingVerifications(true); 202 + setVerifications([]); // Clear existing on initial fetch 203 + setVerificationsCursor(null); // Reset cursor on initial fetch 204 + } 205 + 187 206 try { 188 - const response = await agent.api.com.atproto.repo.listRecords({ 207 + const params = { 189 208 repo: session.did, 190 209 collection: 'app.bsky.graph.verification', 191 - limit: 100, 192 - }); 193 - console.log('Fetched verifications:', response.data); 194 - if (response.data.records) { 195 - const formatted = response.data.records.map(record => ({ 210 + limit: 100, // Keep fetching 100 at a time 211 + }; 212 + if (cursor) { 213 + params.cursor = cursor; 214 + } 215 + 216 + const response = await agent.api.com.atproto.repo.listRecords(params); 217 + console.log('Fetched verifications page:', response.data); 218 + 219 + if (response.data.records && response.data.records.length > 0) { 220 + const newFormatted = response.data.records.map(record => ({ 196 221 uri: record.uri, 197 222 cid: record.cid, 198 223 handle: record.value.handle, 199 224 displayName: record.value.displayName, 200 225 subject: record.value.subject, 201 226 createdAt: record.value.createdAt, 202 - isValid: true, 227 + isValid: true, // Assume valid initially 203 228 validityChecked: false 204 229 })); 205 - setVerifications(formatted); 206 - checkVerificationsValidity(formatted); 230 + 231 + // Append if loading more, replace if initial fetch 232 + setVerifications(prevVerifications => 233 + cursor ? [...prevVerifications, ...newFormatted] : newFormatted 234 + ); 235 + setVerificationsCursor(response.data.cursor || null); // Store the new cursor 236 + 237 + // Get the updated list *after* state update (or construct it) 238 + const updatedVerifications = cursor ? [...verifications, ...newFormatted] : newFormatted; 239 + 240 + // Check validity for the entire updated list 241 + // Consider optimizing this later if performance is an issue 242 + checkVerificationsValidity(updatedVerifications); 207 243 } else { 208 - setVerifications([]); 244 + // If initial fetch resulted in no records, ensure list is empty 245 + if (!cursor) { 246 + setVerifications([]); 247 + setVerificationsCursor(null); 248 + } 249 + // If loading more resulted in no records, just clear the cursor 250 + if (cursor) { 251 + setVerificationsCursor(null); 252 + } 209 253 } 210 254 } catch (error) { 211 255 console.error('Failed to fetch verifications:', error); 212 - setStatusMessage(`Failed to load verifications: ${error.message || 'Unknown error'}`); 256 + // Use appropriate status based on load type 257 + const statusMsg = `Failed to load verifications: ${error.message || 'Unknown error'}`; 258 + if(cursor) setRevokeStatusMessage(statusMsg); // Show error near list 259 + else setStatusMessage(statusMsg); // Show error near top form 213 260 } finally { 214 - setIsLoadingVerifications(false); 261 + if (cursor) { 262 + setIsLoadingMoreVerifications(false); 263 + } else { 264 + setIsLoadingVerifications(false); 265 + } 215 266 } 216 - }, [agent, session]); 267 + // Note: Removing 'verifications' from dependency array to prevent potential infinite loop 268 + // The logic relies on setVerifications using the functional update form or constructing the new list manually. 269 + }, [agent, session, checkVerificationsValidity]); 217 270 218 271 const checkVerificationsValidity = useCallback(async (verificationsList) => { 219 - if (!agent || verificationsList.length === 0) return; 220 - if (verificationsList.length === 0) return; 272 + if (!verificationsList || verificationsList.length === 0) { 273 + console.log("checkVerificationsValidity called with empty or null list."); 274 + return; // Exit early if list is empty 275 + } 276 + // if (!agent || verificationsList.length === 0) return; 277 + // Removed agent check as it's not directly used here anymore 221 278 222 279 setIsCheckingValidity(true); 223 280 const updatedVerifications = [...verificationsList]; ··· 456 513 const followsPseudoList = { 457 514 uri: 'special:follows', 458 515 name: 'My Follows', 459 - // We don't fetch the count here for performance, handle display in component 460 - listItemCount: null // Indicate count is unknown/dynamic 516 + // Use follows count from userInfo if available 517 + listItemCount: userInfo?.followsCount ?? 0 // Default to 0 if not found 461 518 }; 462 519 463 520 setUserLists([followsPseudoList, ...(lists || [])]); // Add follows list at the beginning ··· 478 535 setBulkVerifyStatus(''); 479 536 } 480 537 } 481 - }, [agent, session]); 538 + }, [agent, session, userInfo]); 482 539 483 540 useEffect(() => { 484 - if (agent) { 485 - fetchVerifications(); 486 - fetchUserLists(); // Fetch lists when agent is ready 541 + if (agent && userInfo) { // Wait for both agent and userInfo 542 + fetchVerifications(); // Initial fetch (no cursor) 543 + fetchUserLists(); // Fetch lists when agent and userInfo are ready 487 544 } 488 - }, [agent, fetchVerifications, fetchUserLists]); // Add fetchUserLists dependency 545 + // Intentionally not depending on fetchVerifications/fetchUserLists to avoid loops if they change identity 546 + // We only want this effect to run when agent or userInfo changes. 547 + }, [agent, userInfo, fetchVerifications, fetchUserLists]); // Add userInfo dependency 489 548 490 549 const checkOfficialVerification = useCallback(async () => { 491 550 if (!session?.did) return; ··· 979 1038 } 980 1039 }; 981 1040 1041 + // Handler for revoking by time range 1042 + const handleRevokeByTime = async () => { 1043 + if (!agent || !session || !revokeTimeRange || verifications.length === 0) { 1044 + setBulkRevokeStatus('Cannot revoke by time: Missing agent, session, time range, or no verifications found.'); 1045 + return; 1046 + } 1047 + 1048 + // Calculate cutoff time 1049 + const now = new Date(); 1050 + let cutoffTime = new Date(now); // Copy current time 1051 + switch (revokeTimeRange) { 1052 + case '30m': 1053 + cutoffTime.setMinutes(now.getMinutes() - 30); 1054 + break; 1055 + case '1h': 1056 + cutoffTime.setHours(now.getHours() - 1); 1057 + break; 1058 + case '1d': 1059 + cutoffTime.setDate(now.getDate() - 1); 1060 + break; 1061 + default: 1062 + setBulkRevokeStatus('Invalid time range selected.'); 1063 + return; 1064 + } 1065 + 1066 + // Filter verifications created after the cutoff time 1067 + const verificationsToRevoke = verifications.filter(v => 1068 + new Date(v.createdAt) > cutoffTime 1069 + ); 1070 + 1071 + const count = verificationsToRevoke.length; 1072 + if (count === 0) { 1073 + setBulkRevokeStatus(`No verifications found created within the selected time range (${revokeTimeRange}).`); 1074 + return; 1075 + } 1076 + 1077 + // Confirmation dialog 1078 + if (!window.confirm(`Are you sure you want to revoke ${count} verification(s) created in the last ${revokeTimeRange}? This cannot be undone.`)) { 1079 + return; 1080 + } 1081 + 1082 + setIsRevoking(true); 1083 + setBulkRevokeStatus(`Starting revocation for ${count} record(s) created in the last ${revokeTimeRange}...`); 1084 + setBulkRevokeProgress(''); 1085 + setRevokeStatusMessage(''); // Clear single revoke status 1086 + 1087 + let successCount = 0; 1088 + let failureCount = 0; 1089 + const errors = []; 1090 + 1091 + try { 1092 + // Iterate and revoke each matching verification 1093 + for (let i = 0; i < verificationsToRevoke.length; i++) { 1094 + const verification = verificationsToRevoke[i]; 1095 + const handle = verification.handle || verification.subject; // Use handle if available 1096 + setBulkRevokeProgress(`Revoking ${i + 1} of ${count}: @${handle} (Created: ${new Date(verification.createdAt).toLocaleTimeString()})`); 1097 + 1098 + try { 1099 + const parts = verification.uri.split('/'); 1100 + const rkey = parts[parts.length - 1]; 1101 + 1102 + await agent.api.com.atproto.repo.deleteRecord({ 1103 + repo: session.did, 1104 + collection: 'app.bsky.graph.verification', 1105 + rkey: rkey 1106 + }); 1107 + successCount++; 1108 + } catch (error) { 1109 + console.error(`Failed to revoke @${handle} (URI: ${verification.uri}):`, error); 1110 + failureCount++; 1111 + errors.push(`@${handle}: ${error.message || 'Unknown error'}`); 1112 + } 1113 + } 1114 + 1115 + // Final status message 1116 + let finalMessage = `Time-based revocation complete (${revokeTimeRange}). \n`; 1117 + finalMessage += `Successfully revoked: ${successCount}. \n`; 1118 + if (failureCount > 0) { 1119 + finalMessage += `Failed: ${failureCount}. \n`; 1120 + console.log("Time-based revocation errors:", errors); 1121 + finalMessage += `Check console for details on failures.`; 1122 + } 1123 + setBulkRevokeStatus(finalMessage); 1124 + fetchVerifications(); // Refresh the list of verified accounts 1125 + 1126 + } catch (error) { 1127 + console.error('Error during time-based revocation process:', error); 1128 + setBulkRevokeStatus(`Error during time-based revocation (${revokeTimeRange}): ${error.message || 'Unknown error'}`); 1129 + } finally { 1130 + setIsRevoking(false); 1131 + setBulkRevokeProgress(''); 1132 + } 1133 + }; 1134 + 982 1135 // Handler to hide suggestions when clicking outside 983 1136 useEffect(() => { 984 1137 const handleClickOutside = (event) => { ··· 999 1152 const handleInputFocus = () => { 1000 1153 if (targetHandle.trim() !== '' && suggestions.length > 0) { 1001 1154 setShowSuggestions(true); 1155 + } 1156 + }; 1157 + 1158 + // Handler to load more verifications 1159 + const handleLoadMoreVerifications = () => { 1160 + if (verificationsCursor && !isLoadingMoreVerifications) { 1161 + fetchVerifications(verificationsCursor); 1002 1162 } 1003 1163 }; 1004 1164 ··· 1243 1403 /> 1244 1404 Revoke by List 1245 1405 </label> 1406 + <label> 1407 + <input 1408 + type="radio" 1409 + name="revokeMode" 1410 + value="time" 1411 + checked={revokeMode === 'time'} 1412 + onChange={() => setRevokeMode('time')} 1413 + disabled={isRevoking || isFetchingLists} 1414 + /> 1415 + Revoke by Time 1416 + </label> 1246 1417 </div> 1247 1418 1248 1419 {/* Combined Status Area for Revocation */} ··· 1264 1435 {/* Conditional Revoke Area */} 1265 1436 {revokeMode === 'single' ? ( 1266 1437 <> {/* Use Fragment to avoid unnecessary divs */} 1267 - {isLoadingVerifications ? (<p>Loading...</p>) : verifications.length === 0 ? (<p>You haven't verified any accounts.</p>) : ( 1268 - <ul className="verifier-list"> 1269 - {verifications.map((verification) => ( 1270 - <li key={verification.uri} className={`verifier-list-item ${verification.validityChecked && !verification.isValid ? 'verifier-list-item-invalid' : ''}`}> 1271 - <div className="verifier-list-item-content"> 1272 - <a href={`https://bsky.app/profile/${verification.handle}`} target="_blank" rel="noopener noreferrer" className="verifier-profile-link"> 1273 - <span className="verifier-display-name">{verification.displayName}</span> 1274 - <span className="verifier-list-item-handle">@{verification.handle}</span> 1275 - </a> 1276 - {verification.validityChecked && ( 1277 - <span className={`verifier-validity-status ${verification.isValid ? 'valid' : 'invalid'}`}> 1278 - {verification.isValid ? '✅ Valid' : '❌ Changed'} 1279 - </span> 1280 - )} 1281 - {!verification.validityChecked && isCheckingValidity && ( 1282 - <span className="verifier-validity-status checking">⏳ Checking...</span> 1283 - )} 1284 - <div className="verifier-list-item-date">Verified: {new Date(verification.createdAt).toLocaleString()}</div> 1285 - </div> 1286 - <div className="verifier-list-item-actions"> 1287 - <button onClick={() => handleRevoke(verification)} disabled={isRevoking || isLoadingVerifications} className="verifier-revoke-button"> 1288 - {(isRevoking && revokeStatusMessage.includes(verification.handle)) ? 'Revoking...' : 'Revoke'} 1289 - </button> 1290 - </div> 1291 - </li> 1292 - ))} 1293 - </ul> 1294 - )} 1438 + {/* Search Input */} 1439 + <div className="verifier-search-input-wrapper"> 1440 + <input 1441 + type="text" 1442 + placeholder="Search verified accounts..." 1443 + value={verificationSearchTerm} 1444 + onChange={(e) => setVerificationSearchTerm(e.target.value)} 1445 + className="verifier-input-field" 1446 + disabled={isLoadingVerifications || isRevoking} 1447 + /> 1448 + </div> 1449 + 1450 + {/* Use the new VerificationList component */} 1451 + <VerificationList 1452 + verifications={verifications} 1453 + isLoading={isLoadingVerifications} 1454 + isCheckingValidity={isCheckingValidity} 1455 + isRevoking={isRevoking} 1456 + revokeStatusMessage={revokeStatusMessage} // Pass single revoke message 1457 + handleRevoke={handleRevoke} 1458 + searchTerm={verificationSearchTerm} 1459 + // Pass pagination props 1460 + isLoadingMore={isLoadingMoreVerifications} 1461 + cursor={verificationsCursor} 1462 + onLoadMore={handleLoadMoreVerifications} 1463 + /> 1295 1464 </> 1296 - ) : ( 1465 + ) : revokeMode === 'list' ? ( 1297 1466 <div className="verifier-input-wrapper"> {/* Reuse wrapper for consistent spacing */} 1298 1467 <form onSubmit={handleRevokeList} className="verifier-form-container" style={{ marginBottom: 0 }}> 1299 1468 <select ··· 1315 1484 </button> 1316 1485 </form> 1317 1486 </div> 1487 + ) : ( /* revokeMode === 'time' */ 1488 + <div className="verifier-time-revoke-wrapper"> 1489 + <p>Select the time range to revoke verifications created within:</p> 1490 + <div className="verifier-time-range-selector"> 1491 + <label> 1492 + <input type="radio" name="revokeTimeRange" value="30m" checked={revokeTimeRange === '30m'} onChange={(e) => setRevokeTimeRange(e.target.value)} disabled={isRevoking} /> 1493 + Last 30 Minutes 1494 + </label> 1495 + <label> 1496 + <input type="radio" name="revokeTimeRange" value="1h" checked={revokeTimeRange === '1h'} onChange={(e) => setRevokeTimeRange(e.target.value)} disabled={isRevoking} /> 1497 + Last Hour 1498 + </label> 1499 + <label> 1500 + <input type="radio" name="revokeTimeRange" value="1d" checked={revokeTimeRange === '1d'} onChange={(e) => setRevokeTimeRange(e.target.value)} disabled={isRevoking} /> 1501 + Last 24 Hours 1502 + </label> 1503 + </div> 1504 + <button 1505 + onClick={handleRevokeByTime} // Need to create this handler 1506 + disabled={isRevoking || !revokeTimeRange} 1507 + className="verifier-revoke-button" 1508 + > 1509 + {isRevoking ? 'Revoking by Time...' : 'Revoke Selected Range'} 1510 + </button> 1511 + </div> 1318 1512 )} 1319 1513 </div> 1320 1514 </div> 1321 1515 ); 1516 + } 1517 + 1518 + // Helper component to render the verification list (incorporating search/filter) 1519 + function VerificationList({ 1520 + verifications, 1521 + isLoading, 1522 + isCheckingValidity, 1523 + isRevoking, 1524 + revokeStatusMessage, 1525 + handleRevoke, 1526 + searchTerm, 1527 + isLoadingMore, 1528 + cursor, 1529 + onLoadMore, 1530 + }) { 1531 + const filteredVerifications = useMemo(() => { 1532 + if (!searchTerm) { 1533 + return verifications; 1534 + } 1535 + const lowerCaseSearchTerm = searchTerm.toLowerCase(); 1536 + return verifications.filter(v => 1537 + v.handle?.toLowerCase().includes(lowerCaseSearchTerm) || 1538 + v.displayName?.toLowerCase().includes(lowerCaseSearchTerm) 1539 + ); 1540 + }, [verifications, searchTerm]); 1541 + 1542 + if (isLoading) return <p>Loading...</p>; 1543 + if (verifications.length === 0) return <p>You haven't verified any accounts.</p>; 1544 + if (filteredVerifications.length === 0 && searchTerm) return <p>No verified accounts match "{searchTerm}".</p>; 1545 + 1546 + return ( 1547 + <> 1548 + <ul className="verifier-list"> 1549 + {filteredVerifications.map((verification) => ( 1550 + <li key={verification.uri} className={`verifier-list-item ${verification.validityChecked && !verification.isValid ? 'verifier-list-item-invalid' : ''}`}> 1551 + <div className="verifier-list-item-content"> 1552 + <a href={`https://bsky.app/profile/${verification.handle}`} target="_blank" rel="noopener noreferrer" className="verifier-profile-link"> 1553 + <span className="verifier-display-name">{verification.displayName}</span> 1554 + <span className="verifier-list-item-handle">@{verification.handle}</span> 1555 + </a> 1556 + {verification.validityChecked && ( 1557 + <span className={`verifier-validity-status ${verification.isValid ? 'valid' : 'invalid'}`}> 1558 + {verification.isValid ? '✅ Valid' : '❌ Changed'} 1559 + </span> 1560 + )} 1561 + {!verification.validityChecked && isCheckingValidity && ( 1562 + <span className="verifier-validity-status checking">⏳ Checking...</span> 1563 + )} 1564 + <div className="verifier-list-item-date">Verified: {new Date(verification.createdAt).toLocaleString()}</div> 1565 + </div> 1566 + <div className="verifier-list-item-actions"> 1567 + <button onClick={() => handleRevoke(verification)} disabled={isRevoking || isLoading} className="verifier-revoke-button"> 1568 + {(isRevoking && revokeStatusMessage?.includes(verification.handle)) ? 'Revoking...' : 'Revoke'} 1569 + </button> 1570 + </div> 1571 + </li> 1572 + ))} 1573 + </ul> 1574 + {cursor && ( 1575 + <div className="verifier-load-more-container"> 1576 + <button 1577 + onClick={onLoadMore} 1578 + disabled={isLoadingMore} 1579 + className="verifier-action-button" 1580 + > 1581 + {isLoadingMore ? 'Loading...' : 'Load More'} 1582 + </button> 1583 + </div> 1584 + )} 1585 + </> 1586 + ); 1322 1587 } 1323 1588 1324 1589 export default Verifier;