[READ-ONLY] Mirror of https://github.com/improsocial/impro An extensible Bluesky client for web impro.social
6

Configure Feed

Select the types of activity you want to include in your feed.

Add verification badges

Grace Kind (Apr 4, 2026, 3:44 PM -0500) b757fb51 601b4b9e

+357 -5
+1 -1
package.json
··· 1 1 { 2 2 "name": "impro", 3 - "version": "0.9.31", 3 + "version": "0.9.32", 4 4 "type": "module", 5 5 "scripts": { 6 6 "start": "rm -rf build && NODE_ENV=development eleventy --serve",
+30
src/css/style.css
··· 3605 3605 line-height: 1.2; 3606 3606 } 3607 3607 3608 + .verification-badge { 3609 + display: inline-flex; 3610 + align-items: center; 3611 + vertical-align: middle; 3612 + margin-left: 1px; 3613 + margin-bottom: -1px; 3614 + flex-shrink: 0; 3615 + color: var(--highlight-color); 3616 + } 3617 + 3618 + .verification-badge svg { 3619 + width: 12px; 3620 + height: 12px; 3621 + } 3622 + 3623 + .profile-info .verification-badge { 3624 + margin-left: 6px; 3625 + margin-bottom: 2px; 3626 + } 3627 + 3628 + .profile-list-item .verification-badge { 3629 + margin-left: 4px; 3630 + margin-bottom: 1px; 3631 + } 3632 + 3633 + .profile-name .verification-badge svg { 3634 + width: 18px; 3635 + height: 18px; 3636 + } 3637 + 3608 3638 .profile-handle { 3609 3639 color: var(--text-color-muted); 3610 3640 font-size: 15px;
+17
src/js/templates/icons/verifiedCheckIcon.template.js
··· 1 + import { html } from "/js/lib/lit-html.js"; 2 + 3 + export function verifiedCheckIconTemplate() { 4 + return html`<svg 5 + xmlns="http://www.w3.org/2000/svg" 6 + viewBox="0 0 24 24" 7 + fill="none" 8 + > 9 + <circle cx="12" cy="12" r="11.5" fill="currentColor" /> 10 + <path 11 + fill="#fff" 12 + fill-rule="evenodd" 13 + clip-rule="evenodd" 14 + d="M17.659 8.175a1.361 1.361 0 0 1 0 1.925l-6.224 6.223a1.361 1.361 0 0 1-1.925 0L6.4 13.212a1.361 1.361 0 0 1 1.925-1.925l2.149 2.148 5.26-5.26a1.361 1.361 0 0 1 1.925 0Z" 15 + /> 16 + </svg>`; 17 + }
+22
src/js/templates/icons/verifierCheckIcon.template.js
··· 1 + import { html } from "/js/lib/lit-html.js"; 2 + 3 + export function verifierCheckIconTemplate() { 4 + return html`<svg 5 + xmlns="http://www.w3.org/2000/svg" 6 + viewBox="0 0 24 24" 7 + fill="none" 8 + > 9 + <path 10 + fill="currentColor" 11 + fill-rule="evenodd" 12 + clip-rule="evenodd" 13 + d="M8.792 1.615a4.154 4.154 0 0 1 6.416 0 4.154 4.154 0 0 0 3.146 1.515 4.154 4.154 0 0 1 4 5.017 4.154 4.154 0 0 0 .777 3.404 4.154 4.154 0 0 1-1.427 6.255 4.153 4.153 0 0 0-2.177 2.73 4.154 4.154 0 0 1-5.781 2.784 4.154 4.154 0 0 0-3.492 0 4.154 4.154 0 0 1-5.78-2.784 4.154 4.154 0 0 0-2.178-2.73A4.154 4.154 0 0 1 .87 11.551a4.154 4.154 0 0 0 .776-3.404A4.154 4.154 0 0 1 5.646 3.13a4.154 4.154 0 0 0 3.146-1.515Z" 14 + /> 15 + <path 16 + fill="#fff" 17 + fill-rule="evenodd" 18 + clip-rule="evenodd" 19 + d="M17.861 8.26a1.438 1.438 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L5.97 13.58a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z" 20 + /> 21 + </svg>`; 22 + }
+2 -1
src/js/templates/postHeaderText.template.js
··· 1 1 import { html } from "/js/lib/lit-html.js"; 2 2 import { displayRelativeTime } from "/js/utils.js"; 3 3 import { linkToProfile } from "/js/navigation.js"; 4 + import { verificationBadgeTemplate } from "/js/templates/verificationBadge.template.js"; 4 5 5 6 export function postHeaderTextTemplate({ 6 7 author, ··· 19 20 >` 20 21 : html`<span class="post-name" data-testid="post-author-name" 21 22 >${author.displayName || author.handle}</span 22 - >`} 23 + >`}${verificationBadgeTemplate({ profile: author })} 23 24 ${includeHandle 24 25 ? html`<span class="post-username" data-testid="post-author-handle" 25 26 >@${author.handle}</span
+2 -1
src/js/templates/profileCard.template.js
··· 13 13 import { formatLargeNumber, classnames, noop, sortBy } from "/js/utils.js"; 14 14 import { showSignInModal } from "/js/modals.js"; 15 15 import { richTextTemplate } from "/js/templates/richText.template.js"; 16 + import { verificationBadgeTemplate } from "/js/templates/verificationBadge.template.js"; 16 17 import "/js/components/context-menu.js"; 17 18 import "/js/components/context-menu-item.js"; 18 19 import "/js/components/context-menu-item-group.js"; ··· 286 287 </div> 287 288 <div class="profile-info"> 288 289 <h1 class="profile-name" data-testid="profile-name"> 289 - ${getDisplayName(profile)} 290 + ${getDisplayName(profile)}${verificationBadgeTemplate({ profile })} 290 291 </h1> 291 292 <div class="profile-handle-row"> 292 293 ${profile.viewer?.followedBy && !isBlocking && !isBlockedBy
+2 -1
src/js/templates/profileListItem.template.js
··· 1 1 import { html } from "/js/lib/lit-html.js"; 2 2 import { avatarTemplate } from "/js/templates/avatar.template.js"; 3 3 import { linkToProfile } from "/js/navigation.js"; 4 + import { verificationBadgeTemplate } from "/js/templates/verificationBadge.template.js"; 4 5 5 6 export function profileListItemTemplate({ actor }) { 6 7 const displayName = actor.displayName || actor.handle; ··· 15 16 class="profile-list-item-display-name" 16 17 data-testid="profile-list-item-display-name" 17 18 > 18 - ${displayName} 19 + ${displayName}${verificationBadgeTemplate({ profile: actor })} 19 20 </span> 20 21 </a> 21 22 <div
+33
src/js/templates/verificationBadge.template.js
··· 1 + import { html } from "/js/lib/lit-html.js"; 2 + import { verifiedCheckIconTemplate } from "/js/templates/icons/verifiedCheckIcon.template.js"; 3 + import { verifierCheckIconTemplate } from "/js/templates/icons/verifierCheckIcon.template.js"; 4 + 5 + export function getVerificationState(profile) { 6 + if (!profile?.verification) { 7 + return { role: "default", isVerified: false }; 8 + } 9 + const { verifiedStatus, trustedVerifierStatus } = profile.verification; 10 + const isVerifierUser = ["valid", "invalid"].includes(trustedVerifierStatus); 11 + const isVerified = 12 + (["valid", "invalid"].includes(verifiedStatus) && 13 + verifiedStatus === "valid") || 14 + (isVerifierUser && trustedVerifierStatus === "valid"); 15 + return { 16 + role: isVerifierUser ? "verifier" : "default", 17 + isVerified, 18 + }; 19 + } 20 + 21 + export function verificationBadgeTemplate({ profile }) { 22 + const { role, isVerified } = getVerificationState(profile); 23 + if (!isVerified) return ""; 24 + 25 + const isVerifier = role === "verifier"; 26 + return html`<span 27 + class="verification-badge" 28 + title="${isVerifier ? "Trusted Verifier" : "Verified"}" 29 + >${isVerifier 30 + ? verifierCheckIconTemplate() 31 + : verifiedCheckIconTemplate()}</span 32 + >`; 33 + }
+4 -1
src/js/views/search.view.js
··· 12 12 import { FeedInteractionHandler } from "/js/feedInteractionHandler.js"; 13 13 import { pinIconTemplate } from "/js/templates/icons/pinIcon.template.js"; 14 14 import { tabBarTemplate } from "/js/templates/tabBar.template.js"; 15 + import { verificationBadgeTemplate } from "/js/templates/verificationBadge.template.js"; 15 16 16 17 class SearchView extends View { 17 18 async render({ ··· 150 151 <div class="profile-list-item-body"> 151 152 <div class="profile-list-item-name"> 152 153 <span class="profile-list-item-display-name"> 153 - ${displayName || profile.handle} 154 + ${displayName || profile.handle}${verificationBadgeTemplate({ 155 + profile, 156 + })} 154 157 </span> 155 158 </div> 156 159 <div class="profile-list-item-handle">@${profile.handle}</div>
+45
tests/unit/specs/templates/postHeaderText.template.test.js
··· 118 118 ); 119 119 assertEquals(nameElement.tagName.toLowerCase(), "span"); 120 120 }); 121 + 122 + it("should render verification badge for verified author", () => { 123 + const verifiedAuthor = { 124 + ...post.author, 125 + verification: { verifiedStatus: "valid", trustedVerifierStatus: "none" }, 126 + }; 127 + const result = postHeaderTextTemplate({ 128 + author: verifiedAuthor, 129 + timestamp: post.indexedAt, 130 + }); 131 + const container = document.createElement("div"); 132 + render(result, container); 133 + const badge = container.querySelector(".verification-badge"); 134 + assert(badge !== null); 135 + assertEquals(badge.getAttribute("title"), "Verified"); 136 + }); 137 + 138 + it("should not render verification badge for non-verified author", () => { 139 + const result = postHeaderTextTemplate({ 140 + author: post.author, 141 + timestamp: post.indexedAt, 142 + }); 143 + const container = document.createElement("div"); 144 + render(result, container); 145 + assertEquals(container.querySelector(".verification-badge"), null); 146 + }); 147 + 148 + it("should render verifier badge for trusted verifier author", () => { 149 + const verifierAuthor = { 150 + ...post.author, 151 + verification: { 152 + verifiedStatus: "none", 153 + trustedVerifierStatus: "valid", 154 + }, 155 + }; 156 + const result = postHeaderTextTemplate({ 157 + author: verifierAuthor, 158 + timestamp: post.indexedAt, 159 + }); 160 + const container = document.createElement("div"); 161 + render(result, container); 162 + const badge = container.querySelector(".verification-badge"); 163 + assert(badge !== null); 164 + assertEquals(badge.getAttribute("title"), "Trusted Verifier"); 165 + }); 121 166 }); 122 167 123 168 await t.run();
+56
tests/unit/specs/templates/profileCard.template.test.js
··· 157 157 }); 158 158 }); 159 159 160 + t.describe("profileCardTemplate - verification badge", (it) => { 161 + it("should render verification badge for verified profile", () => { 162 + const profile = { 163 + ...mockProfile, 164 + verification: { verifiedStatus: "valid", trustedVerifierStatus: "none" }, 165 + }; 166 + const result = profileCardTemplate({ 167 + profile, 168 + onClickFollow: () => {}, 169 + }); 170 + const container = document.createElement("div"); 171 + render(result, container); 172 + const badge = container.querySelector( 173 + "[data-testid='profile-name'] .verification-badge", 174 + ); 175 + assert(badge !== null); 176 + assertEquals(badge.getAttribute("title"), "Verified"); 177 + }); 178 + 179 + it("should not render verification badge for non-verified profile", () => { 180 + const result = profileCardTemplate({ 181 + profile: mockProfile, 182 + onClickFollow: () => {}, 183 + }); 184 + const container = document.createElement("div"); 185 + render(result, container); 186 + assertEquals( 187 + container.querySelector( 188 + "[data-testid='profile-name'] .verification-badge", 189 + ), 190 + null, 191 + ); 192 + }); 193 + 194 + it("should render verifier badge for trusted verifier profile", () => { 195 + const profile = { 196 + ...mockProfile, 197 + verification: { 198 + verifiedStatus: "none", 199 + trustedVerifierStatus: "valid", 200 + }, 201 + }; 202 + const result = profileCardTemplate({ 203 + profile, 204 + onClickFollow: () => {}, 205 + }); 206 + const container = document.createElement("div"); 207 + render(result, container); 208 + const badge = container.querySelector( 209 + "[data-testid='profile-name'] .verification-badge", 210 + ); 211 + assert(badge !== null); 212 + assertEquals(badge.getAttribute("title"), "Trusted Verifier"); 213 + }); 214 + }); 215 + 160 216 t.describe("profileCardTemplate - labeler support", (it) => { 161 217 it("should render subscribe button for labeler profile when not subscribed", () => { 162 218 const profile = {
+38
tests/unit/specs/templates/profileListItem.template.test.js
··· 55 55 }); 56 56 }); 57 57 58 + t.describe("profileListItemTemplate - verification badge", (it) => { 59 + it("should render verification badge for verified actor", () => { 60 + const verifiedActor = { 61 + ...mockActor, 62 + verification: { verifiedStatus: "valid", trustedVerifierStatus: "none" }, 63 + }; 64 + const result = profileListItemTemplate({ actor: verifiedActor }); 65 + const container = document.createElement("div"); 66 + render(result, container); 67 + const badge = container.querySelector(".verification-badge"); 68 + assert(badge !== null); 69 + assertEquals(badge.getAttribute("title"), "Verified"); 70 + }); 71 + 72 + it("should not render verification badge for non-verified actor", () => { 73 + const result = profileListItemTemplate({ actor: mockActor }); 74 + const container = document.createElement("div"); 75 + render(result, container); 76 + assertEquals(container.querySelector(".verification-badge"), null); 77 + }); 78 + 79 + it("should render verifier badge for trusted verifier actor", () => { 80 + const verifierActor = { 81 + ...mockActor, 82 + verification: { 83 + verifiedStatus: "none", 84 + trustedVerifierStatus: "valid", 85 + }, 86 + }; 87 + const result = profileListItemTemplate({ actor: verifierActor }); 88 + const container = document.createElement("div"); 89 + render(result, container); 90 + const badge = container.querySelector(".verification-badge"); 91 + assert(badge !== null); 92 + assertEquals(badge.getAttribute("title"), "Trusted Verifier"); 93 + }); 94 + }); 95 + 58 96 t.describe("profileListItemTemplate - no display name", (it) => { 59 97 it("should use handle as display name when displayName is missing", () => { 60 98 const actorWithoutDisplayName = {
+105
tests/unit/specs/templates/verificationBadge.template.test.js
··· 1 + import { TestSuite } from "../../testSuite.js"; 2 + import { assert, assertEquals } from "../../testHelpers.js"; 3 + import { verificationBadgeTemplate } from "/js/templates/verificationBadge.template.js"; 4 + import { render } from "/js/lib/lit-html.js"; 5 + 6 + const t = new TestSuite("verificationBadgeTemplate"); 7 + 8 + t.describe("verificationBadgeTemplate", (it) => { 9 + it("should render nothing for profile without verification", () => { 10 + const profile = { did: "did:plc:123", handle: "user.bsky.social" }; 11 + const result = verificationBadgeTemplate({ profile }); 12 + const container = document.createElement("div"); 13 + render(result, container); 14 + assertEquals(container.querySelector(".verification-badge"), null); 15 + }); 16 + 17 + it("should render nothing for profile with invalid verification", () => { 18 + const profile = { 19 + did: "did:plc:123", 20 + verification: { 21 + verifiedStatus: "invalid", 22 + trustedVerifierStatus: "none", 23 + }, 24 + }; 25 + const result = verificationBadgeTemplate({ profile }); 26 + const container = document.createElement("div"); 27 + render(result, container); 28 + assertEquals(container.querySelector(".verification-badge"), null); 29 + }); 30 + 31 + it("should render verified badge with correct title", () => { 32 + const profile = { 33 + did: "did:plc:123", 34 + verification: { verifiedStatus: "valid", trustedVerifierStatus: "none" }, 35 + }; 36 + const result = verificationBadgeTemplate({ profile }); 37 + const container = document.createElement("div"); 38 + render(result, container); 39 + const badge = container.querySelector(".verification-badge"); 40 + assert(badge !== null); 41 + assertEquals(badge.getAttribute("title"), "Verified"); 42 + }); 43 + 44 + it("should render verified badge with circle SVG", () => { 45 + const profile = { 46 + did: "did:plc:123", 47 + verification: { verifiedStatus: "valid", trustedVerifierStatus: "none" }, 48 + }; 49 + const result = verificationBadgeTemplate({ profile }); 50 + const container = document.createElement("div"); 51 + render(result, container); 52 + const svg = container.querySelector(".verification-badge svg"); 53 + assert(svg !== null); 54 + assert(svg.querySelector("circle") !== null); 55 + }); 56 + 57 + it("should render verifier badge with correct title", () => { 58 + const profile = { 59 + did: "did:plc:123", 60 + verification: { 61 + verifiedStatus: "none", 62 + trustedVerifierStatus: "valid", 63 + }, 64 + }; 65 + const result = verificationBadgeTemplate({ profile }); 66 + const container = document.createElement("div"); 67 + render(result, container); 68 + const badge = container.querySelector(".verification-badge"); 69 + assert(badge !== null); 70 + assertEquals(badge.getAttribute("title"), "Trusted Verifier"); 71 + }); 72 + 73 + it("should render verifier badge with shield SVG (no circle)", () => { 74 + const profile = { 75 + did: "did:plc:123", 76 + verification: { 77 + verifiedStatus: "none", 78 + trustedVerifierStatus: "valid", 79 + }, 80 + }; 81 + const result = verificationBadgeTemplate({ profile }); 82 + const container = document.createElement("div"); 83 + render(result, container); 84 + const svg = container.querySelector(".verification-badge svg"); 85 + assert(svg !== null); 86 + assertEquals(svg.querySelector("circle"), null); 87 + }); 88 + 89 + it("should render verifier badge when both statuses are valid", () => { 90 + const profile = { 91 + did: "did:plc:123", 92 + verification: { 93 + verifiedStatus: "valid", 94 + trustedVerifierStatus: "valid", 95 + }, 96 + }; 97 + const result = verificationBadgeTemplate({ profile }); 98 + const container = document.createElement("div"); 99 + render(result, container); 100 + const badge = container.querySelector(".verification-badge"); 101 + assertEquals(badge.getAttribute("title"), "Trusted Verifier"); 102 + }); 103 + }); 104 + 105 + await t.run();