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