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})();