···
150
150
const [bulkVerifyStatus, setBulkVerifyStatus] = useState(''); // Status message for bulk operations
151
151
const [bulkVerifyProgress, setBulkVerifyProgress] = useState(''); // Progress indicator (e.g., "10/50")
152
152
153
153
+
// State for list revocation
154
154
+
const [revokeMode, setRevokeMode] = useState('single'); // 'single' or 'list'
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
158
+
153
159
useEffect(() => {
154
160
if (session) {
155
161
const agentInstance = new Agent(session);
···
771
777
}
772
778
};
773
779
780
780
+
// Handler for revoking a list
781
781
+
const handleRevokeList = async (e) => {
782
782
+
e.preventDefault();
783
783
+
if (!agent || !session || !selectedListUriForRevoke) {
784
784
+
setBulkRevokeStatus('Please select a list to revoke.');
785
785
+
return;
786
786
+
}
787
787
+
788
788
+
const selectedList = userLists.find(list => list.uri === selectedListUriForRevoke);
789
789
+
if (!selectedList) {
790
790
+
setBulkRevokeStatus('Selected list not found.');
791
791
+
return;
792
792
+
}
793
793
+
794
794
+
// Confirmation dialog
795
795
+
if (!window.confirm(`Are you sure you want to revoke verifications for all users found in the list "${selectedList.name}"? This cannot be undone.`)) {
796
796
+
return;
797
797
+
}
798
798
+
799
799
+
setIsRevoking(true);
800
800
+
setBulkRevokeStatus(`Fetching members of list: ${selectedList.name}...`);
801
801
+
setBulkRevokeProgress('');
802
802
+
setRevokeStatusMessage(''); // Clear single revoke status
803
803
+
804
804
+
let successCount = 0;
805
805
+
let failureCount = 0;
806
806
+
let totalToRevoke = 0;
807
807
+
const errors = [];
808
808
+
809
809
+
try {
810
810
+
// Fetch all items from the selected list
811
811
+
const listItems = await fetchAllPaginated(
812
812
+
agent.api.app.bsky.graph,
813
813
+
'getList',
814
814
+
{ list: selectedListUriForRevoke, limit: 100 },
815
815
+
false
816
816
+
);
817
817
+
818
818
+
if (listItems.length === 0) {
819
819
+
setBulkRevokeStatus(`List "${selectedList.name}" is empty. No users to check for revocation.`);
820
820
+
setIsRevoking(false);
821
821
+
return;
822
822
+
}
823
823
+
824
824
+
const listMemberDids = new Set(listItems.map(item => item.subject.did));
825
825
+
826
826
+
// Filter existing verifications to find those matching list members
827
827
+
const verificationsToRevoke = verifications.filter(verification =>
828
828
+
listMemberDids.has(verification.subject)
829
829
+
);
830
830
+
831
831
+
totalToRevoke = verificationsToRevoke.length;
832
832
+
setBulkRevokeStatus(`Found ${totalToRevoke} existing verification(s) matching users in "${selectedList.name}". Starting revocation...`);
833
833
+
834
834
+
if (totalToRevoke === 0) {
835
835
+
setBulkRevokeStatus(`No existing verifications match users in the list "${selectedList.name}".`);
836
836
+
setIsRevoking(false);
837
837
+
return;
838
838
+
}
839
839
+
840
840
+
// Iterate and revoke each matching verification
841
841
+
for (let i = 0; i < verificationsToRevoke.length; i++) {
842
842
+
const verification = verificationsToRevoke[i];
843
843
+
const handle = verification.handle || verification.subject; // Use handle if available
844
844
+
setBulkRevokeProgress(`Revoking ${i + 1} of ${totalToRevoke}: @${handle}`);
845
845
+
846
846
+
try {
847
847
+
const parts = verification.uri.split('/');
848
848
+
const rkey = parts[parts.length - 1];
849
849
+
850
850
+
await agent.api.com.atproto.repo.deleteRecord({
851
851
+
repo: session.did,
852
852
+
collection: 'app.bsky.graph.verification',
853
853
+
rkey: rkey
854
854
+
});
855
855
+
successCount++;
856
856
+
} catch (error) {
857
857
+
console.error(`Failed to revoke @${handle} (URI: ${verification.uri}):`, error);
858
858
+
failureCount++;
859
859
+
errors.push(`@${handle}: ${error.message || 'Unknown error'}`);
860
860
+
}
861
861
+
}
862
862
+
863
863
+
// Final status message
864
864
+
let finalMessage = `Bulk revocation complete for list "${selectedList.name}". \n`;
865
865
+
finalMessage += `Successfully revoked: ${successCount}. \n`;
866
866
+
if (failureCount > 0) {
867
867
+
finalMessage += `Failed: ${failureCount}. \n`;
868
868
+
console.log("Bulk revocation errors:", errors);
869
869
+
finalMessage += `Check console for details on failures.`;
870
870
+
}
871
871
+
setBulkRevokeStatus(finalMessage);
872
872
+
fetchVerifications(); // Refresh the list of verified accounts
873
873
+
setSelectedListUriForRevoke(''); // Reset selection
874
874
+
875
875
+
} catch (error) {
876
876
+
console.error('Failed to fetch or process list items for revocation:', error);
877
877
+
setBulkRevokeStatus(`Error during bulk revocation for "${selectedList.name}": ${error.message || 'Unknown error'}`);
878
878
+
} finally {
879
879
+
setIsRevoking(false);
880
880
+
setBulkRevokeProgress('');
881
881
+
}
882
882
+
};
883
883
+
774
884
// Handler to hide suggestions when clicking outside
775
885
useEffect(() => {
776
886
const handleClickOutside = (event) => {
···
998
1108
<h2>Accounts You've Verified</h2>
999
1109
</div>
1000
1110
1001
1001
-
{revokeStatusMessage && (
1002
1002
-
<div className={`verifier-status-box ${revokeStatusMessage.includes('failed') ? 'verifier-status-box-error' : 'verifier-status-box-success'}`}>
1003
1003
-
<p>{revokeStatusMessage}</p>
1111
1111
+
{/* Revoke Mode Toggle */}
1112
1112
+
<div className="verifier-mode-toggle">
1113
1113
+
<label>
1114
1114
+
<input
1115
1115
+
type="radio"
1116
1116
+
name="revokeMode"
1117
1117
+
value="single"
1118
1118
+
checked={revokeMode === 'single'}
1119
1119
+
onChange={() => setRevokeMode('single')}
1120
1120
+
disabled={isRevoking || isFetchingLists}
1121
1121
+
/>
1122
1122
+
Manage Individual
1123
1123
+
</label>
1124
1124
+
<label>
1125
1125
+
<input
1126
1126
+
type="radio"
1127
1127
+
name="revokeMode"
1128
1128
+
value="list"
1129
1129
+
checked={revokeMode === 'list'}
1130
1130
+
onChange={() => setRevokeMode('list')}
1131
1131
+
disabled={isRevoking || isFetchingLists}
1132
1132
+
/>
1133
1133
+
Revoke by List
1134
1134
+
</label>
1135
1135
+
</div>
1136
1136
+
1137
1137
+
{/* Combined Status Area for Revocation */}
1138
1138
+
{(revokeStatusMessage || bulkRevokeStatus || bulkRevokeProgress) && (
1139
1139
+
<div className={`verifier-status-box
1140
1140
+
${(revokeStatusMessage && revokeStatusMessage.includes('failed')) ||
1141
1141
+
(bulkRevokeStatus && (bulkRevokeStatus.includes('failed') || bulkRevokeStatus.includes('Error')))
1142
1142
+
? 'verifier-status-box-error'
1143
1143
+
: 'verifier-status-box-success'}
1144
1144
+
${bulkRevokeProgress ? ' verifier-status-box-progress' : ''}
1145
1145
+
`}>
1146
1146
+
{/* Show single status OR bulk status, prioritizing bulk status if active */}
1147
1147
+
{bulkRevokeStatus ? <p>{bulkRevokeStatus}</p> : revokeStatusMessage ? <p>{revokeStatusMessage}</p> : null}
1148
1148
+
{/* Show bulk progress if available */}
1149
1149
+
{bulkRevokeProgress && <p className="verifier-bulk-progress">{bulkRevokeProgress}</p>}
1004
1150
</div>
1005
1151
)}
1006
1152
1007
1007
-
{isLoadingVerifications ? (<p>Loading...</p>) : verifications.length === 0 ? (<p>You haven't verified any accounts.</p>) : (
1008
1008
-
<ul className="verifier-list">
1009
1009
-
{verifications.map((verification) => (
1010
1010
-
<li key={verification.uri} className={`verifier-list-item ${verification.validityChecked && !verification.isValid ? 'verifier-list-item-invalid' : ''}`}>
1011
1011
-
<div className="verifier-list-item-content">
1012
1012
-
<a href={`https://bsky.app/profile/${verification.handle}`} target="_blank" rel="noopener noreferrer" className="verifier-profile-link">
1013
1013
-
<span className="verifier-display-name">{verification.displayName}</span>
1014
1014
-
<span className="verifier-list-item-handle">@{verification.handle}</span>
1015
1015
-
</a>
1016
1016
-
{verification.validityChecked && (
1017
1017
-
<span className={`verifier-validity-status ${verification.isValid ? 'valid' : 'invalid'}`}>
1018
1018
-
{verification.isValid ? '✅ Valid' : '❌ Changed'}
1019
1019
-
</span>
1020
1020
-
)}
1021
1021
-
{!verification.validityChecked && isCheckingValidity && (
1022
1022
-
<span className="verifier-validity-status checking">⏳ Checking...</span>
1023
1023
-
)}
1024
1024
-
<div className="verifier-list-item-date">Verified: {new Date(verification.createdAt).toLocaleString()}</div>
1025
1025
-
</div>
1026
1026
-
<div className="verifier-list-item-actions">
1027
1027
-
<button onClick={() => handleRevoke(verification)} disabled={isRevoking || isLoadingVerifications} className="verifier-revoke-button">
1028
1028
-
{(isRevoking && revokeStatusMessage.includes(verification.handle)) ? 'Revoking...' : 'Revoke'}
1029
1029
-
</button>
1030
1030
-
</div>
1031
1031
-
</li>
1032
1032
-
))}
1033
1033
-
</ul>
1153
1153
+
{/* Conditional Revoke Area */}
1154
1154
+
{revokeMode === 'single' ? (
1155
1155
+
<> {/* Use Fragment to avoid unnecessary divs */}
1156
1156
+
{isLoadingVerifications ? (<p>Loading...</p>) : verifications.length === 0 ? (<p>You haven't verified any accounts.</p>) : (
1157
1157
+
<ul className="verifier-list">
1158
1158
+
{verifications.map((verification) => (
1159
1159
+
<li key={verification.uri} className={`verifier-list-item ${verification.validityChecked && !verification.isValid ? 'verifier-list-item-invalid' : ''}`}>
1160
1160
+
<div className="verifier-list-item-content">
1161
1161
+
<a href={`https://bsky.app/profile/${verification.handle}`} target="_blank" rel="noopener noreferrer" className="verifier-profile-link">
1162
1162
+
<span className="verifier-display-name">{verification.displayName}</span>
1163
1163
+
<span className="verifier-list-item-handle">@{verification.handle}</span>
1164
1164
+
</a>
1165
1165
+
{verification.validityChecked && (
1166
1166
+
<span className={`verifier-validity-status ${verification.isValid ? 'valid' : 'invalid'}`}>
1167
1167
+
{verification.isValid ? '✅ Valid' : '❌ Changed'}
1168
1168
+
</span>
1169
1169
+
)}
1170
1170
+
{!verification.validityChecked && isCheckingValidity && (
1171
1171
+
<span className="verifier-validity-status checking">⏳ Checking...</span>
1172
1172
+
)}
1173
1173
+
<div className="verifier-list-item-date">Verified: {new Date(verification.createdAt).toLocaleString()}</div>
1174
1174
+
</div>
1175
1175
+
<div className="verifier-list-item-actions">
1176
1176
+
<button onClick={() => handleRevoke(verification)} disabled={isRevoking || isLoadingVerifications} className="verifier-revoke-button">
1177
1177
+
{(isRevoking && revokeStatusMessage.includes(verification.handle)) ? 'Revoking...' : 'Revoke'}
1178
1178
+
</button>
1179
1179
+
</div>
1180
1180
+
</li>
1181
1181
+
))}
1182
1182
+
</ul>
1183
1183
+
)}
1184
1184
+
</>
1185
1185
+
) : (
1186
1186
+
<div className="verifier-input-wrapper"> {/* Reuse wrapper for consistent spacing */}
1187
1187
+
<form onSubmit={handleRevokeList} className="verifier-form-container" style={{ marginBottom: 0 }}>
1188
1188
+
<select
1189
1189
+
value={selectedListUriForRevoke}
1190
1190
+
onChange={(e) => setSelectedListUriForRevoke(e.target.value)}
1191
1191
+
disabled={isRevoking || isFetchingLists || userLists.length === 0}
1192
1192
+
required
1193
1193
+
className="verifier-list-select"
1194
1194
+
>
1195
1195
+
<option value="" disabled>{isFetchingLists ? "Loading lists..." : userLists.length === 0 ? "No lists found" : "-- Select list to revoke --"}</option>
1196
1196
+
{userLists.map(list => (
1197
1197
+
<option key={list.uri} value={list.uri}>
1198
1198
+
{list.name} ({list.listItemCount || 0} members)
1199
1199
+
</option>
1200
1200
+
))}
1201
1201
+
</select>
1202
1202
+
<button type="submit" disabled={isRevoking || !selectedListUriForRevoke || isFetchingLists} className="verifier-revoke-button"> {/* Reuse revoke button style */}
1203
1203
+
{isRevoking ? 'Revoking List...' : 'Revoke Selected List'}
1204
1204
+
</button>
1205
1205
+
</form>
1206
1206
+
</div>
1034
1207
)}
1035
1208
</div>
1036
1209
</div>