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