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