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