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