the home of serif.blue
1// ==UserScript==
2// @name Bluesky Community Verifications
3// @namespace https://tangled.sh/@dunkirk.sh/bunplayground
4// @version 0.2
5// @description Shows verification badges from trusted community members on Bluesky
6// @author Kieran Klukas
7// @match https://bsky.app/*
8// @grant none
9// @run-at document-end
10// ==/UserScript==
11
12(() => {
13 // Script has already been initialized check
14 if (window.bskyTrustedUsersInitialized) {
15 console.log("Trusted Users script already initialized");
16 return;
17 }
18
19 // Mark script as initialized
20 window.bskyTrustedUsersInitialized = true;
21
22 // Define storage keys
23 const TRUSTED_USERS_STORAGE_KEY = "bsky_trusted_users";
24 const VERIFICATION_CACHE_STORAGE_KEY = "bsky_verification_cache";
25 const CACHE_EXPIRY_TIME = 24 * 60 * 60 * 1000; // 24 hours
26
27 // Function to get trusted users from local storage
28 const getTrustedUsers = () => {
29 const storedUsers = localStorage.getItem(TRUSTED_USERS_STORAGE_KEY);
30 return storedUsers ? JSON.parse(storedUsers) : [];
31 };
32
33 // Function to save trusted users to local storage
34 const saveTrustedUsers = (users) => {
35 localStorage.setItem(TRUSTED_USERS_STORAGE_KEY, JSON.stringify(users));
36 };
37
38 // Function to add a trusted user
39 const addTrustedUser = (handle) => {
40 const users = getTrustedUsers();
41 if (!users.includes(handle)) {
42 users.push(handle);
43 saveTrustedUsers(users);
44 }
45 };
46
47 // Function to remove a trusted user
48 const removeTrustedUser = (handle) => {
49 const users = getTrustedUsers();
50 const updatedUsers = users.filter((user) => user !== handle);
51 saveTrustedUsers(updatedUsers);
52 };
53
54 // Cache functions
55 const getVerificationCache = () => {
56 const cache = localStorage.getItem(VERIFICATION_CACHE_STORAGE_KEY);
57 return cache ? JSON.parse(cache) : {};
58 };
59
60 const saveVerificationCache = (cache) => {
61 localStorage.setItem(VERIFICATION_CACHE_STORAGE_KEY, JSON.stringify(cache));
62 };
63
64 const getCachedVerifications = (user) => {
65 const cache = getVerificationCache();
66 return cache[user] || null;
67 };
68
69 const cacheVerifications = (user, records) => {
70 const cache = getVerificationCache();
71 cache[user] = {
72 records,
73 timestamp: Date.now(),
74 };
75 saveVerificationCache(cache);
76 };
77
78 const isCacheValid = (cacheEntry) => {
79 return cacheEntry && Date.now() - cacheEntry.timestamp < CACHE_EXPIRY_TIME;
80 };
81
82 // Function to remove a specific user from the verification cache
83 const removeUserFromCache = (handle) => {
84 const cache = getVerificationCache();
85 if (cache[handle]) {
86 delete cache[handle];
87 saveVerificationCache(cache);
88 console.log(`Removed ${handle} from verification cache`);
89 }
90 };
91
92 const clearCache = () => {
93 localStorage.removeItem(VERIFICATION_CACHE_STORAGE_KEY);
94 console.log("Verification cache cleared");
95 };
96
97 // Store all verifiers for a profile
98 let profileVerifiers = [];
99
100 // Store current profile DID
101 let currentProfileDid = null;
102
103 // Function to check if a trusted user has verified the current profile
104 const checkTrustedUserVerifications = async (profileDid) => {
105 currentProfileDid = profileDid; // Store for recheck functionality
106 const trustedUsers = getTrustedUsers();
107 profileVerifiers = []; // Reset the verifiers list
108
109 if (trustedUsers.length === 0) {
110 console.log("No trusted users to check for verifications");
111 return false;
112 }
113
114 console.log(`Checking if any trusted users have verified ${profileDid}`);
115
116 // Use Promise.all to fetch all verification data in parallel
117 const verificationPromises = trustedUsers.map(async (trustedUser) => {
118 try {
119 // Helper function to fetch all verification records with pagination
120 const fetchAllVerifications = async (user) => {
121 // Check cache first
122 const cachedData = getCachedVerifications(user);
123 if (cachedData && isCacheValid(cachedData)) {
124 console.log(`Using cached verification data for ${user}`);
125 return cachedData.records;
126 }
127
128 console.log(`Fetching fresh verification data for ${user}`);
129 let allRecords = [];
130 let cursor = null;
131 let hasMore = true;
132
133 while (hasMore) {
134 const url = cursor
135 ? `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${user}&collection=app.bsky.graph.verification&cursor=${cursor}`
136 : `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${user}&collection=app.bsky.graph.verification`;
137
138 const response = await fetch(url);
139 const data = await response.json();
140
141 if (data.records && data.records.length > 0) {
142 allRecords = [...allRecords, ...data.records];
143 }
144
145 if (data.cursor) {
146 cursor = data.cursor;
147 } else {
148 hasMore = false;
149 }
150 }
151
152 // Save to cache
153 cacheVerifications(user, allRecords);
154 return allRecords;
155 };
156
157 // Fetch all verification records for this trusted user
158 const records = await fetchAllVerifications(trustedUser);
159
160 console.log(`Received verification data from ${trustedUser}`, {
161 records,
162 });
163
164 // Check if this trusted user has verified the current profile
165 if (records.length > 0) {
166 for (const record of records) {
167 if (record.value && record.value.subject === profileDid) {
168 console.log(
169 `${profileDid} is verified by trusted user ${trustedUser}`,
170 );
171
172 // Add to verifiers list
173 profileVerifiers.push(trustedUser);
174 break; // Once we find a verification, we can stop checking
175 }
176 }
177 }
178 return { trustedUser, success: true };
179 } catch (error) {
180 console.error(
181 `Error checking verifications from ${trustedUser}:`,
182 error,
183 );
184 return { trustedUser, success: false, error };
185 }
186 });
187
188 // Wait for all verification checks to complete
189 const results = await Promise.all(verificationPromises);
190
191 // Log summary of API calls
192 console.log(`API calls completed: ${results.length}`);
193 console.log(`Successful calls: ${results.filter((r) => r.success).length}`);
194 console.log(`Failed calls: ${results.filter((r) => !r.success).length}`);
195
196 // If we have verifiers, display the badge
197 if (profileVerifiers.length > 0) {
198 displayVerificationBadge(profileVerifiers);
199 return true;
200 }
201
202 console.log(`${profileDid} is not verified by any trusted users`);
203
204 return false;
205 };
206
207 // Function to display verification badge on the profile
208 const displayVerificationBadge = (verifierHandles) => {
209 // Find the profile header or name element to add the badge to
210 const nameElements = document.querySelectorAll(
211 '[data-testid="profileHeaderDisplayName"]',
212 );
213 const nameElement = nameElements[nameElements.length - 1];
214
215 console.log(nameElement);
216
217 if (nameElement) {
218 // Remove existing badge if present
219 const existingBadge = document.getElementById(
220 "user-trusted-verification-badge",
221 );
222 if (existingBadge) {
223 existingBadge.remove();
224 }
225
226 const badge = document.createElement("span");
227 badge.id = "user-trusted-verification-badge";
228 badge.innerHTML = "✓";
229
230 // Create tooltip text with all verifiers
231 const verifiersText =
232 verifierHandles.length > 1
233 ? `Verified by: ${verifierHandles.join(", ")}`
234 : `Verified by ${verifierHandles[0]}`;
235
236 badge.title = verifiersText;
237 badge.style.cssText = `
238 background-color: #0070ff;
239 color: white;
240 border-radius: 50%;
241 width: 18px;
242 height: 18px;
243 margin-left: 8px;
244 font-size: 12px;
245 font-weight: bold;
246 cursor: help;
247 display: inline-flex;
248 align-items: center;
249 justify-content: center;
250 `;
251
252 // Add a click event to show all verifiers
253 badge.addEventListener("click", (e) => {
254 e.stopPropagation();
255 showVerifiersPopup(verifierHandles);
256 });
257
258 nameElement.appendChild(badge);
259 }
260 };
261
262 // Function to show a popup with all verifiers
263 const showVerifiersPopup = (verifierHandles) => {
264 // Remove existing popup if any
265 const existingPopup = document.getElementById("verifiers-popup");
266 if (existingPopup) {
267 existingPopup.remove();
268 }
269
270 // Create popup
271 const popup = document.createElement("div");
272 popup.id = "verifiers-popup";
273 popup.style.cssText = `
274 position: fixed;
275 top: 50%;
276 left: 50%;
277 transform: translate(-50%, -50%);
278 background-color: #24273A;
279 padding: 20px;
280 border-radius: 10px;
281 z-index: 10002;
282 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
283 max-width: 400px;
284 width: 90%;
285 `;
286
287 // Create popup content
288 popup.innerHTML = `
289 <h3 style="margin-top: 0; color: white;">Profile Verifiers</h3>
290 <div style="max-height: 300px; overflow-y: auto;">
291 ${verifierHandles
292 .map(
293 (handle) => `
294 <div style="padding: 8px 0; border-bottom: 1px solid #444; color: white;">
295 ${handle}
296 </div>
297 `,
298 )
299 .join("")}
300 </div>
301 <button id="close-verifiers-popup" style="
302 margin-top: 15px;
303 padding: 8px 15px;
304 background-color: #473A3A;
305 color: white;
306 border: none;
307 border-radius: 4px;
308 cursor: pointer;
309 ">Close</button>
310 `;
311
312 // Add to body
313 document.body.appendChild(popup);
314
315 // Add close handler
316 document
317 .getElementById("close-verifiers-popup")
318 .addEventListener("click", () => {
319 popup.remove();
320 });
321
322 // Close when clicking outside
323 document.addEventListener("click", function closePopup(e) {
324 if (!popup.contains(e.target)) {
325 popup.remove();
326 document.removeEventListener("click", closePopup);
327 }
328 });
329 };
330
331 // Create settings modal
332 let settingsModal = null;
333
334 // Function to update the list of trusted users in the UI
335 const updateTrustedUsersList = () => {
336 const trustedUsersList = document.getElementById("trustedUsersList");
337 if (!trustedUsersList) return;
338
339 const users = getTrustedUsers();
340 trustedUsersList.innerHTML = "";
341
342 if (users.length === 0) {
343 trustedUsersList.innerHTML = "<p>No trusted users added yet.</p>";
344 return;
345 }
346
347 for (const user of users) {
348 const userItem = document.createElement("div");
349 userItem.style.cssText = `
350 display: flex;
351 justify-content: space-between;
352 align-items: center;
353 padding: 8px 0;
354 border-bottom: 1px solid #eee;
355 `;
356
357 userItem.innerHTML = `
358 <span>${user}</span>
359 <button class="remove-user" data-handle="${user}" style="background-color: #CE3838; color: white; border: none; border-radius: 4px; padding: 5px 10px; cursor: pointer;">Remove</button>
360 `;
361
362 trustedUsersList.appendChild(userItem);
363 }
364
365 // Add event listeners to remove buttons
366 const removeButtons = document.querySelectorAll(".remove-user");
367 for (const btn of removeButtons) {
368 btn.addEventListener("click", (e) => {
369 const handle = e.target.getAttribute("data-handle");
370 removeTrustedUser(handle);
371 removeUserFromCache(handle);
372 updateTrustedUsersList();
373 });
374 }
375 };
376
377 const searchUsers = async (searchQuery) => {
378 if (!searchQuery || searchQuery.length < 2) return [];
379
380 try {
381 const response = await fetch(
382 `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActors?term=${encodeURIComponent(searchQuery)}&limit=5`,
383 );
384 const data = await response.json();
385 return data.actors || [];
386 } catch (error) {
387 console.error("Error searching for users:", error);
388 return [];
389 }
390 };
391
392 // Function to create and show the autocomplete dropdown
393 const showAutocompleteResults = (results, inputElement) => {
394 // Remove existing dropdown if any
395 const existingDropdown = document.getElementById("autocomplete-dropdown");
396 if (existingDropdown) existingDropdown.remove();
397
398 if (results.length === 0) return;
399
400 // Create dropdown
401 const dropdown = document.createElement("div");
402 dropdown.id = "autocomplete-dropdown";
403 dropdown.style.cssText = `
404 position: absolute;
405 background-color: #2A2E3D;
406 border: 1px solid #444;
407 border-radius: 4px;
408 box-shadow: 0 4px 8px rgba(0,0,0,0.2);
409 max-height: 300px;
410 overflow-y: auto;
411 width: ${inputElement.offsetWidth}px;
412 z-index: 10002;
413 margin-top: 2px;
414 `;
415
416 // Position dropdown below input
417 const inputRect = inputElement.getBoundingClientRect();
418 dropdown.style.left = `${inputRect.left}px`;
419 dropdown.style.top = `${inputRect.bottom}px`;
420
421 // Add results to dropdown
422 for (const user of results) {
423 const userItem = document.createElement("div");
424 userItem.className = "autocomplete-item";
425 userItem.style.cssText = `
426 display: flex;
427 align-items: center;
428 padding: 8px 12px;
429 cursor: pointer;
430 color: white;
431 border-bottom: 1px solid #444;
432 `;
433 userItem.onmouseover = () => {
434 userItem.style.backgroundColor = "#3A3F55";
435 };
436 userItem.onmouseout = () => {
437 userItem.style.backgroundColor = "";
438 };
439
440 // Add profile picture
441 const avatar = document.createElement("img");
442 avatar.src = user.avatar || "https://bsky.app/static/default-avatar.png";
443 avatar.style.cssText = `
444 width: 32px;
445 height: 32px;
446 border-radius: 50%;
447 margin-right: 10px;
448 object-fit: cover;
449 `;
450
451 // Add user info
452 const userInfo = document.createElement("div");
453 userInfo.style.cssText = `
454 display: flex;
455 flex-direction: column;
456 `;
457
458 const displayName = document.createElement("div");
459 displayName.textContent = user.displayName || user.handle;
460 displayName.style.fontWeight = "bold";
461
462 const handle = document.createElement("div");
463 handle.textContent = user.handle;
464 handle.style.fontSize = "0.8em";
465 handle.style.opacity = "0.8";
466
467 userInfo.appendChild(displayName);
468 userInfo.appendChild(handle);
469
470 userItem.appendChild(avatar);
471 userItem.appendChild(userInfo);
472
473 // Handle click on user item
474 userItem.addEventListener("click", () => {
475 inputElement.value = user.handle;
476 dropdown.remove();
477 });
478
479 dropdown.appendChild(userItem);
480 }
481
482 document.body.appendChild(dropdown);
483
484 // Close dropdown when clicking outside
485 document.addEventListener("click", function closeDropdown(e) {
486 if (e.target !== inputElement && !dropdown.contains(e.target)) {
487 dropdown.remove();
488 document.removeEventListener("click", closeDropdown);
489 }
490 });
491 };
492
493 // Function to import verifications from the current user
494 const importVerificationsFromSelf = async () => {
495 try {
496 // Check if we can determine the current user
497 const bskyStorageData = localStorage.getItem("BSKY_STORAGE");
498 let userData = null;
499
500 if (bskyStorageData) {
501 try {
502 const bskyStorage = JSON.parse(bskyStorageData);
503 if (bskyStorage.session.currentAccount) {
504 userData = bskyStorage.session.currentAccount;
505 }
506 } catch (error) {
507 console.error("Error parsing BSKY_STORAGE data:", error);
508 }
509 }
510
511 if (!userData || !userData.handle) {
512 alert(
513 "Could not determine your Bluesky handle. Please ensure you're logged in.",
514 );
515 return;
516 }
517
518 if (!userData || !userData.handle) {
519 alert(
520 "Unable to determine your Bluesky handle. Make sure you're logged in.",
521 );
522 return;
523 }
524
525 const userHandle = userData.handle;
526
527 // Show loading state
528 const importButton = document.getElementById("importVerificationsBtn");
529 const originalText = importButton.textContent;
530 importButton.textContent = "Importing...";
531 importButton.disabled = true;
532
533 // Fetch verification records from the user's account with pagination
534 let allRecords = [];
535 let cursor = null;
536 let hasMore = true;
537
538 while (hasMore) {
539 const url = cursor
540 ? `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${userHandle}&collection=app.bsky.graph.verification&cursor=${cursor}`
541 : `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${userHandle}&collection=app.bsky.graph.verification`;
542
543 const verificationResponse = await fetch(url);
544 const data = await verificationResponse.json();
545
546 if (data.records && data.records.length > 0) {
547 allRecords = [...allRecords, ...data.records];
548 }
549
550 if (data.cursor) {
551 cursor = data.cursor;
552 } else {
553 hasMore = false;
554 }
555 }
556
557 const verificationData = { records: allRecords };
558
559 if (!verificationData.records || verificationData.records.length === 0) {
560 alert("No verification records found in your account.");
561 importButton.textContent = originalText;
562 importButton.disabled = false;
563 return;
564 }
565
566 // Extract the handles of verified users
567 const verifiedUsers = [];
568 for (const record of verificationData.records) {
569 console.log(record.value.handle);
570 verifiedUsers.push(record.value.handle);
571 }
572
573 // Add all found users to trusted users
574 let addedCount = 0;
575 for (const handle of verifiedUsers) {
576 const existingUsers = getTrustedUsers();
577 if (!existingUsers.includes(handle)) {
578 addTrustedUser(handle);
579 addedCount++;
580 }
581 }
582
583 // Update the UI
584 updateTrustedUsersList();
585
586 // Reset button state
587 importButton.textContent = originalText;
588 importButton.disabled = false;
589
590 // Show result
591 alert(
592 `Successfully imported ${addedCount} verified users from your account.`,
593 );
594 } catch (error) {
595 console.error("Error importing verifications:", error);
596 alert("Error importing verifications. Check console for details.");
597 const importButton = document.getElementById("importVerificationsBtn");
598 if (importButton) {
599 importButton.textContent = "Import Verifications";
600 importButton.disabled = false;
601 }
602 }
603 };
604
605 const addSettingsButton = () => {
606 // Check if we're on the settings page
607 if (!window.location.href.includes("bsky.app/settings")) {
608 return;
609 }
610
611 // Check if our button already exists to avoid duplicates
612 if (document.getElementById("community-verifications-settings-button")) {
613 return;
614 }
615
616 // Find the right place to insert our button (after content-and-media link)
617 const contentMediaLink = document.querySelector(
618 'a[href="/settings/content-and-media"]',
619 );
620 if (!contentMediaLink) {
621 console.log("Could not find content-and-media link to insert after");
622 return;
623 }
624
625 // Clone the existing link and modify it
626 const verificationButton = contentMediaLink.cloneNode(true);
627 verificationButton.id = "community-verifications-settings-button";
628 verificationButton.href = "#"; // No actual link, we'll handle click with JS
629 verificationButton.setAttribute("aria-label", "Community Verifications");
630
631 const highlightColor =
632 verificationButton.firstChild.style.backgroundColor || "rgb(30,41,54)";
633
634 // Add hover effect to highlight the button
635 verificationButton.addEventListener("mouseover", () => {
636 verificationButton.firstChild.style.backgroundColor = highlightColor;
637 });
638
639 verificationButton.addEventListener("mouseout", () => {
640 verificationButton.firstChild.style.backgroundColor = null;
641 });
642
643 // Update the text content
644 const textDiv = verificationButton.querySelector(".css-146c3p1");
645 if (textDiv) {
646 textDiv.textContent = "Community Verifications";
647 }
648
649 // Update the icon
650 const iconDiv = verificationButton.querySelector(
651 ".css-175oi2r[style*='width: 28px']",
652 );
653 if (iconDiv) {
654 iconDiv.innerHTML = `
655 <svg fill="none" width="28" viewBox="0 0 24 24" height="28" style="color: rgb(241, 243, 245);">
656 <path fill="hsl(211, 20%, 95.3%)" d="M21.2,9.3c-0.5-0.5-1.1-0.7-1.8-0.7h-2.3V6.3c0-2.1-1.7-3.7-3.7-3.7h-3c-2.1,0-3.7,1.7-3.7,3.7v2.3H4.6
657 c-0.7,0-1.3,0.3-1.8,0.7c-0.5,0.5-0.7,1.1-0.7,1.8v9.3c0,0.7,0.3,1.3,0.7,1.8c0.5,0.5,1.1,0.7,1.8,0.7h14.9c0.7,0,1.3-0.3,1.8-0.7
658 c0.5-0.5,0.7-1.1,0.7-1.8v-9.3C22,10.4,21.7,9.8,21.2,9.3z M14.1,15.6l-1.3,1.3c-0.1,0.1-0.3,0.2-0.5,0.2c-0.2,0-0.3-0.1-0.5-0.2l-3.3-3.3
659 c-0.1-0.1-0.2-0.3-0.2-0.5c0-0.2,0.1-0.3,0.2-0.5l1.3-1.3c0.1-0.1,0.3-0.2,0.5-0.2c0.2,0,0.3,0.1,0.5,0.2l1.5,1.5l4.2-4.2
660 c0.1-0.1,0.3-0.2,0.5-0.2c0.2,0,0.3,0.1,0.5,0.2l1.3,1.3c0.1,0.1,0.2,0.3,0.2,0.5c0,0.2-0.1,0.3-0.2,0.5L14.1,15.6z M9.7,6.3
661 c0-0.9,0.7-1.7,1.7-1.7h3c0.9,0,1.7,0.7,1.7,1.7v2.3H9.7V6.3z"/>
662 </svg>
663 `;
664 }
665
666 // Insert our button after the content-and-media link
667 const parentElement = contentMediaLink.parentElement;
668 parentElement.insertBefore(
669 verificationButton,
670 contentMediaLink.nextSibling,
671 );
672
673 // Add click event to open our settings modal
674 verificationButton.addEventListener("click", (e) => {
675 e.preventDefault();
676 if (settingsModal) {
677 settingsModal.style.display = "flex";
678 updateTrustedUsersList();
679 } else {
680 createSettingsModal();
681 }
682 });
683
684 console.log("Added Community Verifications button to settings page");
685 };
686
687 // Function to create the settings modal
688 const createSettingsModal = () => {
689 // Create modal container
690 settingsModal = document.createElement("div");
691 settingsModal.id = "bsky-trusted-settings-modal";
692 settingsModal.style.cssText = `
693 display: none;
694 position: fixed;
695 top: 0;
696 left: 0;
697 width: 100%;
698 height: 100%;
699 background-color: rgba(0, 0, 0, 0.5);
700 z-index: 10001;
701 justify-content: center;
702 align-items: center;
703 `;
704
705 // Create modal content
706 const modalContent = document.createElement("div");
707 modalContent.style.cssText = `
708 background-color: #24273A;
709 padding: 20px;
710 border-radius: 10px;
711 width: 400px;
712 max-height: 80vh;
713 overflow-y: auto;
714 `;
715
716 // Create modal header
717 const modalHeader = document.createElement("div");
718 modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`;
719
720 // Create input form
721 const form = document.createElement("div");
722 form.innerHTML = `
723 <p>Add Bluesky handles you trust:</p>
724 <div style="display: flex; margin-bottom: 15px; position: relative;">
725 <input id="trustedUserInput" type="text" placeholder="Search for a user..." style="flex: 1; padding: 8px; margin-right: 10px; border: 1px solid #ccc; border-radius: 4px;">
726 <button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button>
727 </div>
728 `;
729
730 // Create import button
731 const importContainer = document.createElement("div");
732 importContainer.style.cssText = `
733 margin-top: 10px;
734 margin-bottom: 15px;
735 `;
736
737 const importButton = document.createElement("button");
738 importButton.id = "importVerificationsBtn";
739 importButton.textContent = "Import Your Verifications";
740 importButton.style.cssText = `
741 background-color: #2D578D;
742 color: white;
743 border: none;
744 border-radius: 4px;
745 padding: 8px 15px;
746 cursor: pointer;
747 width: 100%;
748 `;
749
750 importButton.addEventListener("click", importVerificationsFromSelf);
751 importContainer.appendChild(importButton);
752
753 // Create trusted users list
754 const trustedUsersList = document.createElement("div");
755 trustedUsersList.id = "trustedUsersList";
756 trustedUsersList.style.cssText = `
757 margin-top: 15px;
758 border-top: 1px solid #eee;
759 padding-top: 15px;
760 `;
761
762 // Create cache control buttons
763 const cacheControls = document.createElement("div");
764 cacheControls.style.cssText = `
765 margin-top: 15px;
766 padding-top: 15px;
767 border-top: 1px solid #eee;
768 `;
769
770 const clearCacheButton = document.createElement("button");
771 clearCacheButton.textContent = "Clear Verification Cache";
772 clearCacheButton.style.cssText = `
773 padding: 8px 15px;
774 background-color: #735A5A;
775 color: white;
776 border: none;
777 border-radius: 4px;
778 cursor: pointer;
779 margin-right: 10px;
780 `;
781 clearCacheButton.addEventListener("click", () => {
782 clearCache();
783 alert(
784 "Verification cache cleared. Fresh data will be fetched on next check.",
785 );
786 });
787
788 cacheControls.appendChild(clearCacheButton);
789
790 // Create close button
791 const closeButton = document.createElement("button");
792 closeButton.textContent = "Close";
793 closeButton.style.cssText = `
794 margin-top: 20px;
795 padding: 8px 15px;
796 background-color: #473A3A;
797 border: none;
798 border-radius: 4px;
799 cursor: pointer;
800 `;
801
802 // Assemble modal
803 modalContent.appendChild(modalHeader);
804 modalContent.appendChild(form);
805 modalContent.appendChild(importContainer);
806 modalContent.appendChild(trustedUsersList);
807 modalContent.appendChild(cacheControls);
808 modalContent.appendChild(closeButton);
809 settingsModal.appendChild(modalContent);
810
811 // Add to document
812 document.body.appendChild(settingsModal);
813
814 const userInput = document.getElementById("trustedUserInput");
815
816 // Add input event for autocomplete
817 let debounceTimeout;
818 userInput.addEventListener("input", (e) => {
819 clearTimeout(debounceTimeout);
820 debounceTimeout = setTimeout(async () => {
821 const searchQuery = e.target.value.trim();
822 if (searchQuery.length >= 2) {
823 const results = await searchUsers(searchQuery);
824 showAutocompleteResults(results, userInput);
825 } else {
826 const dropdown = document.getElementById("autocomplete-dropdown");
827 if (dropdown) dropdown.remove();
828 }
829 }, 300); // Debounce for 300ms
830 });
831
832 // Event listeners
833 closeButton.addEventListener("click", () => {
834 settingsModal.style.display = "none";
835 });
836
837 // Function to add a user from the input field
838 const addUserFromInput = () => {
839 const input = document.getElementById("trustedUserInput");
840 const handle = input.value.trim();
841 if (handle) {
842 addTrustedUser(handle);
843 input.value = "";
844 updateTrustedUsersList();
845
846 // Remove dropdown if present
847 const dropdown = document.getElementById("autocomplete-dropdown");
848 if (dropdown) dropdown.remove();
849 }
850 };
851
852 // Add trusted user button event
853 document
854 .getElementById("addTrustedUserBtn")
855 .addEventListener("click", addUserFromInput);
856
857 // Add keydown event to input for Enter key
858 userInput.addEventListener("keydown", (e) => {
859 if (e.key === "Enter") {
860 e.preventDefault();
861 addUserFromInput();
862 }
863 });
864
865 // Close modal when clicking outside
866 settingsModal.addEventListener("click", (e) => {
867 if (e.target === settingsModal) {
868 settingsModal.style.display = "none";
869 }
870 });
871
872 // Initialize the list
873 updateTrustedUsersList();
874 };
875
876 // Function to check the current profile
877 const checkCurrentProfile = () => {
878 const currentUrl = window.location.href;
879 // Only trigger on profile pages
880 if (
881 currentUrl.match(/bsky\.app\/profile\/[^\/]+$/) ||
882 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/follows/) ||
883 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/followers/)
884 ) {
885 const handle = currentUrl.split("/profile/")[1].split("/")[0];
886 console.log("Detected profile page for:", handle);
887
888 // Fetch user profile data
889 fetch(
890 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`,
891 )
892 .then((response) => response.json())
893 .then((data) => {
894 console.log("User profile data:", data);
895
896 // Extract the DID from the profile data
897 const did = data.uri.split("/")[2];
898 console.log("User DID:", did);
899
900 // Check if any trusted users have verified this profile using the DID
901 checkTrustedUserVerifications(did);
902 })
903 .catch((error) => {
904 console.error("Error checking profile:", error);
905 });
906
907 console.log("Bluesky profile detected");
908 } else {
909 // Not on a profile page, reset state
910 currentProfileDid = null;
911 profileVerifiers = [];
912
913 // Remove UI elements if present
914 const existingBadge = document.getElementById(
915 "user-trusted-verification-badge",
916 );
917 if (existingBadge) {
918 existingBadge.remove();
919 }
920 }
921 };
922
923 // Initial check
924 checkCurrentProfile();
925
926 const checkUserLinksOnPage = async () => {
927 // Look for profile links with handles
928 // Find all profile links and filter to get only one link per parent
929 const allProfileLinks = Array.from(
930 document.querySelectorAll('a[href^="/profile/"]:not(:has(*))'),
931 );
932
933 // Use a Map to keep track of parent elements and their first child link
934 const parentMap = new Map();
935
936 // For each link, store only the first one found for each parent
937 for (const link of allProfileLinks) {
938 const parent = link.parentElement;
939 if (parent && !parentMap.has(parent)) {
940 parentMap.set(parent, link);
941 }
942 }
943
944 // Get only the first link for each parent
945 const profileLinks = Array.from(parentMap.values());
946
947 if (profileLinks.length === 0) return;
948
949 console.log(`Found ${profileLinks.length} possible user links on page`);
950
951 // Process profile links to identify user containers
952 for (const link of profileLinks) {
953 try {
954 // Check if we already processed this link
955 if (link.getAttribute("data-verification-checked") === "true") continue;
956
957 // Mark as checked
958 link.setAttribute("data-verification-checked", "true");
959
960 // Extract handle from href
961 const handle = link.getAttribute("href").split("/profile/")[1];
962 if (!handle) continue;
963
964 // check if there is anything after the handle
965 const handleTrailing = handle.split("/").length > 1;
966 if (handleTrailing) continue;
967
968 // Find parent container that might contain the handle and verification icon
969 // Look for containers where this link is followed by another link with the same handle
970 const parent = link.parentElement;
971
972 // If we found a container with the verification icon
973 if (parent) {
974 // Check if this user already has our verification badge
975 if (parent.querySelector(".trusted-user-inline-badge")) continue;
976
977 try {
978 // Fetch user profile data to get DID
979 const response = await fetch(
980 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`,
981 );
982 const data = await response.json();
983
984 // Extract the DID from the profile data
985 const did = data.uri.split("/")[2];
986
987 // Check if this user is verified by our trusted users
988 const trustedUsers = getTrustedUsers();
989 let isVerified = false;
990 const verifiers = [];
991
992 // Check cache first for each trusted user
993 for (const trustedUser of trustedUsers) {
994 const cachedData = getCachedVerifications(trustedUser);
995
996 if (cachedData && isCacheValid(cachedData)) {
997 // Use cached verification data
998 const records = cachedData.records;
999
1000 for (const record of records) {
1001 if (record.value && record.value.subject === did) {
1002 isVerified = true;
1003 verifiers.push(trustedUser);
1004 break;
1005 }
1006 }
1007 }
1008 }
1009
1010 // If verified, add a small badge
1011 if (isVerified && verifiers.length > 0) {
1012 // Create a badge element
1013 const smallBadge = document.createElement("span");
1014 smallBadge.className = "trusted-user-inline-badge";
1015 smallBadge.innerHTML = "✓";
1016
1017 // Create tooltip text with all verifiers
1018 const verifiersText =
1019 verifiers.length > 1
1020 ? `Verified by: ${verifiers.join(", ")}`
1021 : `Verified by ${verifiers[0]}`;
1022
1023 smallBadge.title = verifiersText;
1024 smallBadge.style.cssText = `
1025 background-color: #0070ff;
1026 color: white;
1027 border-radius: 50%;
1028 width: 14px;
1029 height: 14px;
1030 font-size: 10px;
1031 font-weight: bold;
1032 cursor: help;
1033 display: inline-flex;
1034 align-items: center;
1035 justify-content: center;
1036 margin-left: 4px;
1037 `;
1038
1039 // Add click event to show verifiers
1040 smallBadge.addEventListener("click", (e) => {
1041 e.stopPropagation();
1042 showVerifiersPopup(verifiers);
1043 });
1044
1045 // Insert badge after the SVG element
1046 parent.firstChild.after(smallBadge);
1047 parent.style.flexDirection = "row";
1048 parent.style.alignItems = "center";
1049 }
1050 } catch (error) {
1051 console.error(`Error checking verification for ${handle}:`, error);
1052 }
1053 }
1054 } catch (error) {
1055 console.error("Error processing profile link:", error);
1056 }
1057 }
1058 };
1059
1060 const observeContentChanges = () => {
1061 // Use a debounced function to check for new user links
1062 const debouncedCheck = () => {
1063 clearTimeout(window.userLinksCheckTimeout);
1064 window.userLinksCheckTimeout = setTimeout(() => {
1065 checkUserLinksOnPage();
1066 }, 300);
1067 };
1068
1069 // Create a mutation observer that watches for DOM changes
1070 const observer = new MutationObserver((mutations) => {
1071 let hasRelevantChanges = false;
1072
1073 // Check if any mutations involve adding new nodes
1074 for (const mutation of mutations) {
1075 if (mutation.addedNodes.length > 0) {
1076 for (const node of mutation.addedNodes) {
1077 if (node.nodeType === Node.ELEMENT_NODE) {
1078 // Check if this element or its children might contain profile links
1079 if (
1080 node.querySelector('a[href^="/profile/"]') ||
1081 (node.tagName === "A" &&
1082 node.getAttribute("href")?.startsWith("/profile/"))
1083 ) {
1084 hasRelevantChanges = true;
1085 break;
1086 }
1087 }
1088 }
1089 }
1090 if (hasRelevantChanges) break;
1091 }
1092
1093 if (hasRelevantChanges) {
1094 debouncedCheck();
1095 }
1096 });
1097
1098 // Observe the entire document for content changes that might include profile links
1099 observer.observe(document.body, { childList: true, subtree: true });
1100
1101 // Also check periodically for posts that might have been loaded but not caught by the observer
1102 setInterval(debouncedCheck, 5000);
1103 };
1104
1105 // Wait for DOM to be fully loaded before initializing
1106 document.addEventListener("DOMContentLoaded", () => {
1107 // Initial check for user links
1108 checkUserLinksOnPage();
1109
1110 // Add settings button if we're on the settings page
1111 if (window.location.href.includes("bsky.app/settings")) {
1112 // Wait for the content-and-media link to appear before adding our button
1113 const waitForSettingsLink = setInterval(() => {
1114 const contentMediaLink = document.querySelector(
1115 'a[href="/settings/content-and-media"]',
1116 );
1117 if (contentMediaLink) {
1118 clearInterval(waitForSettingsLink);
1119 addSettingsButton();
1120 }
1121 }, 200);
1122 }
1123 });
1124
1125 // Start observing for content changes to detect newly loaded posts
1126 observeContentChanges();
1127
1128 // Set up a MutationObserver to watch for URL changes
1129 const observeUrlChanges = () => {
1130 let lastUrl = location.href;
1131
1132 const observer = new MutationObserver(() => {
1133 if (location.href !== lastUrl) {
1134 const oldUrl = lastUrl;
1135 lastUrl = location.href;
1136 console.log("URL changed from:", oldUrl, "to:", location.href);
1137
1138 // Reset current profile DID
1139 currentProfileDid = null;
1140 profileVerifiers = [];
1141
1142 // Clean up UI elements
1143 const existingBadge = document.getElementById(
1144 "user-trusted-verification-badge",
1145 );
1146 if (existingBadge) {
1147 existingBadge.remove();
1148 }
1149
1150 // Check if we're on a profile page now
1151 setTimeout(checkCurrentProfile, 500); // Small delay to ensure DOM has updated
1152
1153 if (window.location.href.includes("bsky.app/settings")) {
1154 // Give the page a moment to fully load
1155 setTimeout(addSettingsButton, 200);
1156 }
1157 }
1158 });
1159
1160 observer.observe(document, { subtree: true, childList: true });
1161 };
1162
1163 // Start observing for URL changes
1164 observeUrlChanges();
1165})();