the home of serif.blue
1// ==UserScript==
2// @name Bluesky Community Verifications
3// @namespace https://tangled.sh/@dunkirk.sh/bunplayground
4// @version 0.1
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 // Add recheck button even when no verifications are found
205 createPillButtons();
206
207 return false;
208 };
209
210 // Function to create a pill with recheck and settings buttons
211 const createPillButtons = () => {
212 // Remove existing buttons if any
213 const existingPill = document.getElementById(
214 "trusted-users-pill-container",
215 );
216 if (existingPill) {
217 existingPill.remove();
218 }
219
220 // Create pill container
221 const pillContainer = document.createElement("div");
222 pillContainer.id = "trusted-users-pill-container";
223 pillContainer.style.cssText = `
224 position: fixed;
225 bottom: 20px;
226 right: 20px;
227 z-index: 10000;
228 display: flex;
229 border-radius: 20px;
230 overflow: hidden;
231 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
232 `;
233
234 // Create recheck button (left half of pill)
235 const recheckButton = document.createElement("button");
236 recheckButton.id = "trusted-users-recheck-button";
237 recheckButton.innerHTML = "↻ Recheck";
238 recheckButton.style.cssText = `
239 padding: 10px 15px;
240 background-color: #2D578D;
241 color: white;
242 border: none;
243 cursor: pointer;
244 font-weight: bold;
245 border-top-left-radius: 20px;
246 border-bottom-left-radius: 20px;
247 `;
248
249 // Add click event to recheck
250 recheckButton.addEventListener("click", async () => {
251 if (currentProfileDid) {
252 // Remove any existing badges when rechecking
253 const existingBadge = document.getElementById(
254 "user-trusted-verification-badge",
255 );
256 if (existingBadge) {
257 existingBadge.remove();
258 }
259
260 // Show loading state
261 recheckButton.innerHTML = "⟳ Checking...";
262 recheckButton.disabled = true;
263
264 // Recheck verifications
265 await checkTrustedUserVerifications(currentProfileDid);
266
267 // Reset button
268 recheckButton.innerHTML = "↻ Recheck";
269 recheckButton.disabled = false;
270 }
271 });
272
273 // Create vertical divider
274 const divider = document.createElement("div");
275 divider.style.cssText = `
276 width: 1px;
277 background-color: rgba(255, 255, 255, 0.3);
278 `;
279
280 // Create settings button (right half of pill)
281 const settingsButton = document.createElement("button");
282 settingsButton.id = "bsky-trusted-settings-button";
283 settingsButton.textContent = "Settings";
284 settingsButton.style.cssText = `
285 padding: 10px 15px;
286 background-color: #2D578D;
287 color: white;
288 border: none;
289 cursor: pointer;
290 font-weight: bold;
291 border-top-right-radius: 20px;
292 border-bottom-right-radius: 20px;
293 `;
294
295 // Add elements to pill
296 pillContainer.appendChild(recheckButton);
297 pillContainer.appendChild(divider);
298 pillContainer.appendChild(settingsButton);
299
300 // Add pill to page
301 document.body.appendChild(pillContainer);
302
303 // Add event listener to settings button
304 settingsButton.addEventListener("click", () => {
305 if (settingsModal) {
306 settingsModal.style.display = "flex";
307 updateTrustedUsersList();
308 } else {
309 createSettingsModal();
310 }
311 });
312 };
313
314 // Function to display verification badge on the profile
315 const displayVerificationBadge = (verifierHandles) => {
316 // Find the profile header or name element to add the badge to
317 const nameElements = document.querySelectorAll(
318 '[data-testid="profileHeaderDisplayName"]',
319 );
320 const nameElement = nameElements[nameElements.length - 1];
321
322 console.log(nameElement);
323
324 if (nameElement) {
325 // Remove existing badge if present
326 const existingBadge = document.getElementById(
327 "user-trusted-verification-badge",
328 );
329 if (existingBadge) {
330 existingBadge.remove();
331 }
332
333 const badge = document.createElement("span");
334 badge.id = "user-trusted-verification-badge";
335 badge.innerHTML = "✓";
336
337 // Create tooltip text with all verifiers
338 const verifiersText =
339 verifierHandles.length > 1
340 ? `Verified by: ${verifierHandles.join(", ")}`
341 : `Verified by ${verifierHandles[0]}`;
342
343 badge.title = verifiersText;
344 badge.style.cssText = `
345 background-color: #0070ff;
346 color: white;
347 border-radius: 50%;
348 width: 18px;
349 height: 18px;
350 margin-left: 8px;
351 font-size: 12px;
352 font-weight: bold;
353 cursor: help;
354 display: inline-flex;
355 align-items: center;
356 justify-content: center;
357 `;
358
359 // Add a click event to show all verifiers
360 badge.addEventListener("click", (e) => {
361 e.stopPropagation();
362 showVerifiersPopup(verifierHandles);
363 });
364
365 nameElement.appendChild(badge);
366 }
367
368 // Also add pill buttons when verification is found
369 createPillButtons();
370 };
371
372 // Function to show a popup with all verifiers
373 const showVerifiersPopup = (verifierHandles) => {
374 // Remove existing popup if any
375 const existingPopup = document.getElementById("verifiers-popup");
376 if (existingPopup) {
377 existingPopup.remove();
378 }
379
380 // Create popup
381 const popup = document.createElement("div");
382 popup.id = "verifiers-popup";
383 popup.style.cssText = `
384 position: fixed;
385 top: 50%;
386 left: 50%;
387 transform: translate(-50%, -50%);
388 background-color: #24273A;
389 padding: 20px;
390 border-radius: 10px;
391 z-index: 10002;
392 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
393 max-width: 400px;
394 width: 90%;
395 `;
396
397 // Create popup content
398 popup.innerHTML = `
399 <h3 style="margin-top: 0; color: white;">Profile Verifiers</h3>
400 <div style="max-height: 300px; overflow-y: auto;">
401 ${verifierHandles
402 .map(
403 (handle) => `
404 <div style="padding: 8px 0; border-bottom: 1px solid #444; color: white;">
405 ${handle}
406 </div>
407 `,
408 )
409 .join("")}
410 </div>
411 <button id="close-verifiers-popup" style="
412 margin-top: 15px;
413 padding: 8px 15px;
414 background-color: #473A3A;
415 color: white;
416 border: none;
417 border-radius: 4px;
418 cursor: pointer;
419 ">Close</button>
420 `;
421
422 // Add to body
423 document.body.appendChild(popup);
424
425 // Add close handler
426 document
427 .getElementById("close-verifiers-popup")
428 .addEventListener("click", () => {
429 popup.remove();
430 });
431
432 // Close when clicking outside
433 document.addEventListener("click", function closePopup(e) {
434 if (!popup.contains(e.target)) {
435 popup.remove();
436 document.removeEventListener("click", closePopup);
437 }
438 });
439 };
440
441 // Create settings modal
442 let settingsModal = null;
443
444 // Function to update the list of trusted users in the UI
445 const updateTrustedUsersList = () => {
446 const trustedUsersList = document.getElementById("trustedUsersList");
447 if (!trustedUsersList) return;
448
449 const users = getTrustedUsers();
450 trustedUsersList.innerHTML = "";
451
452 if (users.length === 0) {
453 trustedUsersList.innerHTML = "<p>No trusted users added yet.</p>";
454 return;
455 }
456
457 for (const user of users) {
458 const userItem = document.createElement("div");
459 userItem.style.cssText = `
460 display: flex;
461 justify-content: space-between;
462 align-items: center;
463 padding: 8px 0;
464 border-bottom: 1px solid #eee;
465 `;
466
467 userItem.innerHTML = `
468 <span>${user}</span>
469 <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>
470 `;
471
472 trustedUsersList.appendChild(userItem);
473 }
474
475 // Add event listeners to remove buttons
476 const removeButtons = document.querySelectorAll(".remove-user");
477 for (const btn of removeButtons) {
478 btn.addEventListener("click", (e) => {
479 const handle = e.target.getAttribute("data-handle");
480 removeTrustedUser(handle);
481 removeUserFromCache(handle);
482 updateTrustedUsersList();
483 });
484 }
485 };
486
487 const searchUsers = async (searchQuery) => {
488 if (!searchQuery || searchQuery.length < 2) return [];
489
490 try {
491 const response = await fetch(
492 `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActors?term=${encodeURIComponent(searchQuery)}&limit=5`,
493 );
494 const data = await response.json();
495 return data.actors || [];
496 } catch (error) {
497 console.error("Error searching for users:", error);
498 return [];
499 }
500 };
501
502 // Function to create and show the autocomplete dropdown
503 const showAutocompleteResults = (results, inputElement) => {
504 // Remove existing dropdown if any
505 const existingDropdown = document.getElementById("autocomplete-dropdown");
506 if (existingDropdown) existingDropdown.remove();
507
508 if (results.length === 0) return;
509
510 // Create dropdown
511 const dropdown = document.createElement("div");
512 dropdown.id = "autocomplete-dropdown";
513 dropdown.style.cssText = `
514 position: absolute;
515 background-color: #2A2E3D;
516 border: 1px solid #444;
517 border-radius: 4px;
518 box-shadow: 0 4px 8px rgba(0,0,0,0.2);
519 max-height: 300px;
520 overflow-y: auto;
521 width: ${inputElement.offsetWidth}px;
522 z-index: 10002;
523 margin-top: 2px;
524 `;
525
526 // Position dropdown below input
527 const inputRect = inputElement.getBoundingClientRect();
528 dropdown.style.left = `${inputRect.left}px`;
529 dropdown.style.top = `${inputRect.bottom}px`;
530
531 // Add results to dropdown
532 for (const user of results) {
533 const userItem = document.createElement("div");
534 userItem.className = "autocomplete-item";
535 userItem.style.cssText = `
536 display: flex;
537 align-items: center;
538 padding: 8px 12px;
539 cursor: pointer;
540 color: white;
541 border-bottom: 1px solid #444;
542 `;
543 userItem.onmouseover = () => {
544 userItem.style.backgroundColor = "#3A3F55";
545 };
546 userItem.onmouseout = () => {
547 userItem.style.backgroundColor = "";
548 };
549
550 // Add profile picture
551 const avatar = document.createElement("img");
552 avatar.src = user.avatar || "https://bsky.app/static/default-avatar.png";
553 avatar.style.cssText = `
554 width: 32px;
555 height: 32px;
556 border-radius: 50%;
557 margin-right: 10px;
558 object-fit: cover;
559 `;
560
561 // Add user info
562 const userInfo = document.createElement("div");
563 userInfo.style.cssText = `
564 display: flex;
565 flex-direction: column;
566 `;
567
568 const displayName = document.createElement("div");
569 displayName.textContent = user.displayName || user.handle;
570 displayName.style.fontWeight = "bold";
571
572 const handle = document.createElement("div");
573 handle.textContent = user.handle;
574 handle.style.fontSize = "0.8em";
575 handle.style.opacity = "0.8";
576
577 userInfo.appendChild(displayName);
578 userInfo.appendChild(handle);
579
580 userItem.appendChild(avatar);
581 userItem.appendChild(userInfo);
582
583 // Handle click on user item
584 userItem.addEventListener("click", () => {
585 inputElement.value = user.handle;
586 dropdown.remove();
587 });
588
589 dropdown.appendChild(userItem);
590 }
591
592 document.body.appendChild(dropdown);
593
594 // Close dropdown when clicking outside
595 document.addEventListener("click", function closeDropdown(e) {
596 if (e.target !== inputElement && !dropdown.contains(e.target)) {
597 dropdown.remove();
598 document.removeEventListener("click", closeDropdown);
599 }
600 });
601 };
602
603 // Function to import verifications from the current user
604 const importVerificationsFromSelf = async () => {
605 try {
606 // Check if we can determine the current user
607 const bskyStorageData = localStorage.getItem("BSKY_STORAGE");
608 let userData = null;
609
610 if (bskyStorageData) {
611 try {
612 const bskyStorage = JSON.parse(bskyStorageData);
613 if (bskyStorage.session.currentAccount) {
614 userData = bskyStorage.session.currentAccount;
615 }
616 } catch (error) {
617 console.error("Error parsing BSKY_STORAGE data:", error);
618 }
619 }
620
621 if (!userData || !userData.handle) {
622 alert(
623 "Could not determine your Bluesky handle. Please ensure you're logged in.",
624 );
625 return;
626 }
627
628 if (!userData || !userData.handle) {
629 alert(
630 "Unable to determine your Bluesky handle. Make sure you're logged in.",
631 );
632 return;
633 }
634
635 const userHandle = userData.handle;
636
637 // Show loading state
638 const importButton = document.getElementById("importVerificationsBtn");
639 const originalText = importButton.textContent;
640 importButton.textContent = "Importing...";
641 importButton.disabled = true;
642
643 // Fetch verification records from the user's account with pagination
644 let allRecords = [];
645 let cursor = null;
646 let hasMore = true;
647
648 while (hasMore) {
649 const url = cursor
650 ? `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${userHandle}&collection=app.bsky.graph.verification&cursor=${cursor}`
651 : `https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${userHandle}&collection=app.bsky.graph.verification`;
652
653 const verificationResponse = await fetch(url);
654 const data = await verificationResponse.json();
655
656 if (data.records && data.records.length > 0) {
657 allRecords = [...allRecords, ...data.records];
658 }
659
660 if (data.cursor) {
661 cursor = data.cursor;
662 } else {
663 hasMore = false;
664 }
665 }
666
667 const verificationData = { records: allRecords };
668
669 if (!verificationData.records || verificationData.records.length === 0) {
670 alert("No verification records found in your account.");
671 importButton.textContent = originalText;
672 importButton.disabled = false;
673 return;
674 }
675
676 // Extract the handles of verified users
677 const verifiedUsers = [];
678 for (const record of verificationData.records) {
679 console.log(record.value.handle);
680 verifiedUsers.push(record.value.handle);
681 }
682
683 // Add all found users to trusted users
684 let addedCount = 0;
685 for (const handle of verifiedUsers) {
686 const existingUsers = getTrustedUsers();
687 if (!existingUsers.includes(handle)) {
688 addTrustedUser(handle);
689 addedCount++;
690 }
691 }
692
693 // Update the UI
694 updateTrustedUsersList();
695
696 // Reset button state
697 importButton.textContent = originalText;
698 importButton.disabled = false;
699
700 // Show result
701 alert(
702 `Successfully imported ${addedCount} verified users from your account.`,
703 );
704 } catch (error) {
705 console.error("Error importing verifications:", error);
706 alert("Error importing verifications. Check console for details.");
707 const importButton = document.getElementById("importVerificationsBtn");
708 if (importButton) {
709 importButton.textContent = "Import Verifications";
710 importButton.disabled = false;
711 }
712 }
713 };
714
715 // Function to create the settings modal
716 const createSettingsModal = () => {
717 // Create modal container
718 settingsModal = document.createElement("div");
719 settingsModal.id = "bsky-trusted-settings-modal";
720 settingsModal.style.cssText = `
721 display: none;
722 position: fixed;
723 top: 0;
724 left: 0;
725 width: 100%;
726 height: 100%;
727 background-color: rgba(0, 0, 0, 0.5);
728 z-index: 10001;
729 justify-content: center;
730 align-items: center;
731 `;
732
733 // Create modal content
734 const modalContent = document.createElement("div");
735 modalContent.style.cssText = `
736 background-color: #24273A;
737 padding: 20px;
738 border-radius: 10px;
739 width: 400px;
740 max-height: 80vh;
741 overflow-y: auto;
742 `;
743
744 // Create modal header
745 const modalHeader = document.createElement("div");
746 modalHeader.innerHTML = `<h2 style="margin-top: 0;">Trusted Bluesky Users</h2>`;
747
748 // Create input form
749 const form = document.createElement("div");
750 form.innerHTML = `
751 <p>Add Bluesky handles you trust:</p>
752 <div style="display: flex; margin-bottom: 15px; position: relative;">
753 <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;">
754 <button id="addTrustedUserBtn" style="background-color: #2D578D; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer;">Add</button>
755 </div>
756 `;
757
758 // Create import button
759 const importContainer = document.createElement("div");
760 importContainer.style.cssText = `
761 margin-top: 10px;
762 margin-bottom: 15px;
763 `;
764
765 const importButton = document.createElement("button");
766 importButton.id = "importVerificationsBtn";
767 importButton.textContent = "Import Your Verifications";
768 importButton.style.cssText = `
769 background-color: #2D578D;
770 color: white;
771 border: none;
772 border-radius: 4px;
773 padding: 8px 15px;
774 cursor: pointer;
775 width: 100%;
776 `;
777
778 importButton.addEventListener("click", importVerificationsFromSelf);
779 importContainer.appendChild(importButton);
780
781 // Create trusted users list
782 const trustedUsersList = document.createElement("div");
783 trustedUsersList.id = "trustedUsersList";
784 trustedUsersList.style.cssText = `
785 margin-top: 15px;
786 border-top: 1px solid #eee;
787 padding-top: 15px;
788 `;
789
790 // Create cache control buttons
791 const cacheControls = document.createElement("div");
792 cacheControls.style.cssText = `
793 margin-top: 15px;
794 padding-top: 15px;
795 border-top: 1px solid #eee;
796 `;
797
798 const clearCacheButton = document.createElement("button");
799 clearCacheButton.textContent = "Clear Verification Cache";
800 clearCacheButton.style.cssText = `
801 padding: 8px 15px;
802 background-color: #735A5A;
803 color: white;
804 border: none;
805 border-radius: 4px;
806 cursor: pointer;
807 margin-right: 10px;
808 `;
809 clearCacheButton.addEventListener("click", () => {
810 clearCache();
811 alert(
812 "Verification cache cleared. Fresh data will be fetched on next check.",
813 );
814 });
815
816 cacheControls.appendChild(clearCacheButton);
817
818 // Create close button
819 const closeButton = document.createElement("button");
820 closeButton.textContent = "Close";
821 closeButton.style.cssText = `
822 margin-top: 20px;
823 padding: 8px 15px;
824 background-color: #473A3A;
825 border: none;
826 border-radius: 4px;
827 cursor: pointer;
828 `;
829
830 // Assemble modal
831 modalContent.appendChild(modalHeader);
832 modalContent.appendChild(form);
833 modalContent.appendChild(importContainer);
834 modalContent.appendChild(trustedUsersList);
835 modalContent.appendChild(cacheControls);
836 modalContent.appendChild(closeButton);
837 settingsModal.appendChild(modalContent);
838
839 // Add to document
840 document.body.appendChild(settingsModal);
841
842 const userInput = document.getElementById("trustedUserInput");
843
844 // Add input event for autocomplete
845 let debounceTimeout;
846 userInput.addEventListener("input", (e) => {
847 clearTimeout(debounceTimeout);
848 debounceTimeout = setTimeout(async () => {
849 const searchQuery = e.target.value.trim();
850 if (searchQuery.length >= 2) {
851 const results = await searchUsers(searchQuery);
852 showAutocompleteResults(results, userInput);
853 } else {
854 const dropdown = document.getElementById("autocomplete-dropdown");
855 if (dropdown) dropdown.remove();
856 }
857 }, 300); // Debounce for 300ms
858 });
859
860 // Event listeners
861 closeButton.addEventListener("click", () => {
862 settingsModal.style.display = "none";
863 });
864
865 // Function to add a user from the input field
866 const addUserFromInput = () => {
867 const input = document.getElementById("trustedUserInput");
868 const handle = input.value.trim();
869 if (handle) {
870 addTrustedUser(handle);
871 input.value = "";
872 updateTrustedUsersList();
873
874 // Remove dropdown if present
875 const dropdown = document.getElementById("autocomplete-dropdown");
876 if (dropdown) dropdown.remove();
877 }
878 };
879
880 // Add trusted user button event
881 document
882 .getElementById("addTrustedUserBtn")
883 .addEventListener("click", addUserFromInput);
884
885 // Add keydown event to input for Enter key
886 userInput.addEventListener("keydown", (e) => {
887 if (e.key === "Enter") {
888 e.preventDefault();
889 addUserFromInput();
890 }
891 });
892
893 // Close modal when clicking outside
894 settingsModal.addEventListener("click", (e) => {
895 if (e.target === settingsModal) {
896 settingsModal.style.display = "none";
897 }
898 });
899
900 // Initialize the list
901 updateTrustedUsersList();
902 };
903
904 // Function to create the settings UI if it doesn't exist yet
905 const createSettingsUI = () => {
906 // Create pill with buttons
907 createPillButtons();
908
909 // Create the settings modal if it doesn't exist yet
910 if (!settingsModal) {
911 createSettingsModal();
912 }
913 };
914
915 // Function to check the current profile
916 const checkCurrentProfile = () => {
917 const currentUrl = window.location.href;
918 // Only trigger on profile pages
919 if (
920 currentUrl.match(/bsky\.app\/profile\/[^\/]+$/) ||
921 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/follows/) ||
922 currentUrl.match(/bsky\.app\/profile\/[^\/]+\/followers/)
923 ) {
924 const handle = currentUrl.split("/profile/")[1].split("/")[0];
925 console.log("Detected profile page for:", handle);
926
927 // Create and add the settings UI (only once)
928 createSettingsUI();
929
930 // Fetch user profile data
931 fetch(
932 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`,
933 )
934 .then((response) => response.json())
935 .then((data) => {
936 console.log("User profile data:", data);
937
938 // Extract the DID from the profile data
939 const did = data.uri.split("/")[2];
940 console.log("User DID:", did);
941
942 // Check if any trusted users have verified this profile using the DID
943 checkTrustedUserVerifications(did);
944 })
945 .catch((error) => {
946 console.error("Error checking profile:", error);
947 });
948
949 console.log("Bluesky profile detected");
950 } else {
951 // Not on a profile page, reset state
952 currentProfileDid = null;
953 profileVerifiers = [];
954
955 // Remove UI elements if present
956 const existingBadge = document.getElementById(
957 "user-trusted-verification-badge",
958 );
959 if (existingBadge) {
960 existingBadge.remove();
961 }
962
963 const existingPill = document.getElementById(
964 "trusted-users-pill-container",
965 );
966 if (existingPill) {
967 existingPill.remove();
968 }
969 }
970 };
971
972 // Initial check
973 checkCurrentProfile();
974
975 const checkUserLinksOnPage = async () => {
976 // Look for profile links with handles
977 // Find all profile links and filter to get only one link per parent
978 const allProfileLinks = Array.from(
979 document.querySelectorAll('a[href^="/profile/"]:not(:has(*))'),
980 );
981
982 // Use a Map to keep track of parent elements and their first child link
983 const parentMap = new Map();
984
985 // For each link, store only the first one found for each parent
986 for (const link of allProfileLinks) {
987 const parent = link.parentElement;
988 if (parent && !parentMap.has(parent)) {
989 parentMap.set(parent, link);
990 }
991 }
992
993 // Get only the first link for each parent
994 const profileLinks = Array.from(parentMap.values());
995
996 if (profileLinks.length === 0) return;
997
998 console.log(`Found ${profileLinks.length} possible user links on page`);
999
1000 // Process profile links to identify user containers
1001 for (const link of profileLinks) {
1002 try {
1003 // Check if we already processed this link
1004 if (link.getAttribute("data-verification-checked") === "true") continue;
1005
1006 // Mark as checked
1007 link.setAttribute("data-verification-checked", "true");
1008
1009 // Extract handle from href
1010 const handle = link.getAttribute("href").split("/profile/")[1];
1011 if (!handle) continue;
1012
1013 // check if there is anything after the handle
1014 const handleTrailing = handle.split("/").length > 1;
1015 if (handleTrailing) continue;
1016
1017 // Find parent container that might contain the handle and verification icon
1018 // Look for containers where this link is followed by another link with the same handle
1019 const parent = link.parentElement;
1020
1021 // If we found a container with the verification icon
1022 if (parent) {
1023 // Check if this user already has our verification badge
1024 if (parent.querySelector(".trusted-user-inline-badge")) continue;
1025
1026 try {
1027 // Fetch user profile data to get DID
1028 const response = await fetch(
1029 `https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=${handle}&collection=app.bsky.actor.profile&rkey=self`,
1030 );
1031 const data = await response.json();
1032
1033 // Extract the DID from the profile data
1034 const did = data.uri.split("/")[2];
1035
1036 // Check if this user is verified by our trusted users
1037 const trustedUsers = getTrustedUsers();
1038 let isVerified = false;
1039 const verifiers = [];
1040
1041 // Check cache first for each trusted user
1042 for (const trustedUser of trustedUsers) {
1043 const cachedData = getCachedVerifications(trustedUser);
1044
1045 if (cachedData && isCacheValid(cachedData)) {
1046 // Use cached verification data
1047 const records = cachedData.records;
1048
1049 for (const record of records) {
1050 if (record.value && record.value.subject === did) {
1051 isVerified = true;
1052 verifiers.push(trustedUser);
1053 break;
1054 }
1055 }
1056 }
1057 }
1058
1059 // If verified, add a small badge
1060 if (isVerified && verifiers.length > 0) {
1061 // Create a badge element
1062 const smallBadge = document.createElement("span");
1063 smallBadge.className = "trusted-user-inline-badge";
1064 smallBadge.innerHTML = "✓";
1065
1066 // Create tooltip text with all verifiers
1067 const verifiersText =
1068 verifiers.length > 1
1069 ? `Verified by: ${verifiers.join(", ")}`
1070 : `Verified by ${verifiers[0]}`;
1071
1072 smallBadge.title = verifiersText;
1073 smallBadge.style.cssText = `
1074 background-color: #0070ff;
1075 color: white;
1076 border-radius: 50%;
1077 width: 14px;
1078 height: 14px;
1079 font-size: 10px;
1080 font-weight: bold;
1081 cursor: help;
1082 display: inline-flex;
1083 align-items: center;
1084 justify-content: center;
1085 margin-left: 4px;
1086 `;
1087
1088 // Add click event to show verifiers
1089 smallBadge.addEventListener("click", (e) => {
1090 e.stopPropagation();
1091 showVerifiersPopup(verifiers);
1092 });
1093
1094 // Insert badge after the SVG element
1095 parent.firstChild.after(smallBadge);
1096 parent.style.flexDirection = "row";
1097 parent.style.alignItems = "center";
1098 }
1099 } catch (error) {
1100 console.error(`Error checking verification for ${handle}:`, error);
1101 }
1102 }
1103 } catch (error) {
1104 console.error("Error processing profile link:", error);
1105 }
1106 }
1107 };
1108
1109 const observeContentChanges = () => {
1110 // Use a debounced function to check for new user links
1111 const debouncedCheck = () => {
1112 clearTimeout(window.userLinksCheckTimeout);
1113 window.userLinksCheckTimeout = setTimeout(() => {
1114 checkUserLinksOnPage();
1115 }, 300);
1116 };
1117
1118 // Create a mutation observer that watches for DOM changes
1119 const observer = new MutationObserver((mutations) => {
1120 let hasRelevantChanges = false;
1121
1122 // Check if any mutations involve adding new nodes
1123 for (const mutation of mutations) {
1124 if (mutation.addedNodes.length > 0) {
1125 for (const node of mutation.addedNodes) {
1126 if (node.nodeType === Node.ELEMENT_NODE) {
1127 // Check if this element or its children might contain profile links
1128 if (
1129 node.querySelector('a[href^="/profile/"]') ||
1130 (node.tagName === "A" &&
1131 node.getAttribute("href")?.startsWith("/profile/"))
1132 ) {
1133 hasRelevantChanges = true;
1134 break;
1135 }
1136 }
1137 }
1138 }
1139 if (hasRelevantChanges) break;
1140 }
1141
1142 if (hasRelevantChanges) {
1143 debouncedCheck();
1144 }
1145 });
1146
1147 // Observe the entire document for content changes that might include profile links
1148 observer.observe(document.body, { childList: true, subtree: true });
1149
1150 // Also check periodically for posts that might have been loaded but not caught by the observer
1151 setInterval(debouncedCheck, 5000);
1152 };
1153
1154 // Add these calls to the initialization section
1155 // Initial check for user links
1156 setTimeout(checkUserLinksOnPage, 1000); // Slight delay to ensure page has loaded
1157
1158 // Start observing for content changes to detect newly loaded posts
1159 observeContentChanges();
1160
1161 // Set up a MutationObserver to watch for URL changes
1162 const observeUrlChanges = () => {
1163 let lastUrl = location.href;
1164
1165 const observer = new MutationObserver(() => {
1166 if (location.href !== lastUrl) {
1167 const oldUrl = lastUrl;
1168 lastUrl = location.href;
1169 console.log("URL changed from:", oldUrl, "to:", location.href);
1170
1171 // Reset current profile DID
1172 currentProfileDid = null;
1173 profileVerifiers = [];
1174
1175 // Clean up UI elements
1176 const existingBadge = document.getElementById(
1177 "user-trusted-verification-badge",
1178 );
1179 if (existingBadge) {
1180 existingBadge.remove();
1181 }
1182
1183 const existingPill = document.getElementById(
1184 "trusted-users-pill-container",
1185 );
1186 if (existingPill) {
1187 existingPill.remove();
1188 }
1189
1190 // Check if we're on a profile page now
1191 setTimeout(checkCurrentProfile, 500); // Small delay to ensure DOM has updated
1192 }
1193 });
1194
1195 observer.observe(document, { subtree: true, childList: true });
1196 };
1197
1198 // Start observing for URL changes
1199 observeUrlChanges();
1200})();