[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 more icons

Grace Kind (Jul 9, 2026, 3:43 PM -0500) 678cf4ac de3060ed

+130 -33
+1 -1
package.json
··· 1 1 { 2 2 "name": "impro", 3 - "version": "0.17.122", 3 + "version": "0.17.123", 4 4 "type": "module", 5 5 "scripts": { 6 6 "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve",
+36 -9
src/css/style.css
··· 401 401 cursor: default; 402 402 } 403 403 404 + .rounded-button .plus-icon { 405 + width: 14px; 406 + height: 14px; 407 + } 408 + 404 409 .rounded-button .loading-spinner { 405 410 border-left-color: transparent; 406 411 border-right-color: transparent; ··· 570 575 align-items: flex-start; 571 576 justify-content: center; 572 577 width: 100%; 578 + } 579 + 580 + .rounded-button .icon { 581 + display: flex; 573 582 } 574 583 575 584 .button-group { ··· 2091 2100 margin-top: 4px; 2092 2101 } 2093 2102 2103 + .reply-to-author .icon { 2104 + display: inline-block; 2105 + width: 13px; 2106 + height: 13px; 2107 + vertical-align: -2px; 2108 + } 2109 + 2094 2110 .icon { 2095 2111 position: relative; 2096 2112 } ··· 2462 2478 .alt-indicator { 2463 2479 pointer-events: none; 2464 2480 position: absolute; 2481 + display: flex; 2482 + align-items: center; 2483 + gap: 3px; 2465 2484 top: 8px; 2466 2485 left: 8px; 2467 2486 background: var(--overlay-color-medium); ··· 2473 2492 letter-spacing: 0.5px; 2474 2493 } 2475 2494 2495 + .alt-indicator .icon { 2496 + width: 11px; 2497 + height: 11px; 2498 + } 2499 + 2476 2500 .external-link { 2477 2501 overflow: hidden; 2478 2502 } ··· 3928 3952 } 3929 3953 3930 3954 .message-reply-caption-arrow { 3931 - font-size: 14px; 3932 - line-height: 1; 3955 + display: flex; 3956 + } 3957 + 3958 + .message-reply-caption-arrow .icon { 3959 + width: 13px; 3960 + height: 13px; 3933 3961 } 3934 3962 3935 3963 .message-reply-caption-sent { ··· 7032 7060 cursor: default; 7033 7061 } 7034 7062 7035 - .button-plus-icon { 7036 - position: relative; 7037 - bottom: 1px; 7063 + .settings-button .plus-icon { 7064 + width: 14px; 7065 + height: 14px; 7038 7066 } 7039 7067 7040 7068 .settings-button .loading-spinner { ··· 7326 7354 flex-shrink: 0; 7327 7355 } 7328 7356 7329 - .settings-account-ellipsis span { 7330 - position: relative; 7331 - top: -4px; 7332 - font-weight: 600; 7357 + .settings-account-ellipsis .icon { 7358 + width: 16px; 7359 + height: 16px; 7333 7360 } 7334 7361 7335 7362 .account-spinner {
+1
src/img/icons/custom/corner-down-right-line.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 5v2.5a5.5 5.5 0 0 0 5.5 5.5H19m0 0-5-5m5 5-5 5"/></svg>
+8 -2
src/js/components/post-composer.js
··· 17 17 import { imageIconTemplate } from "/js/templates/icons/imageIcon.template.js"; 18 18 import { emojiIconTemplate } from "/js/templates/icons/emojiIcon.template.js"; 19 19 import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js"; 20 + import { checkIconTemplate } from "/js/templates/icons/checkIcon.template.js"; 21 + import { plusIconTemplate } from "/js/templates/icons/plusIcon.template.js"; 20 22 import { showToast } from "/js/toasts.js"; 21 23 import { 22 24 validateVideoFile, ··· 91 93 `; 92 94 } 93 95 96 + function altIndicatorContentTemplate(hasAlt) { 97 + return html`${hasAlt ? checkIconTemplate() : plusIconTemplate()} ALT`; 98 + } 99 + 94 100 function videoPreviewTemplate({ video, onRemove, onEditAltText }) { 95 101 const isReady = video.status === "done"; 96 102 const isError = video.status === "error"; ··· 131 137 onEditAltText(); 132 138 }} 133 139 > 134 - ${video.alt ? "✓ ALT" : "+ ALT"} 140 + ${altIndicatorContentTemplate(!!video.alt)} 135 141 </button> 136 142 </div> 137 143 </div> ··· 161 167 ${closeIconTemplate()} 162 168 </button> 163 169 <div class="alt-indicator ${img.alt ? "has-alt" : "no-alt"}"> 164 - ${img.alt ? "✓ ALT" : "+ ALT"} 170 + ${altIndicatorContentTemplate(!!img.alt)} 165 171 </div> 166 172 </div> 167 173 `,
+17
src/js/templates/icons/cornerDownRightIcon.template.js
··· 1 + import { html } from "/js/lib/lit-html.js"; 2 + 3 + //src/img/icons/custom/corner-down-right-line.svg 4 + 5 + export function cornerDownRightIconTemplate() { 6 + return html`<div class="icon corner-down-right-icon"> 7 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"> 8 + <path 9 + stroke="currentColor" 10 + stroke-linecap="round" 11 + stroke-linejoin="round" 12 + stroke-width="2.5" 13 + d="M5 5v2.5a5.5 5.5 0 0 0 5.5 5.5H19m0 0-5-5m5 5-5 5" 14 + /> 15 + </svg> 16 + </div>`; 17 + }
+17
src/js/templates/icons/moreMenuIcon.template.js
··· 1 + import { html } from "/js/lib/lit-html.js"; 2 + 3 + //github.com/halfmage/majesticons/blob/main/line/more-menu-line.svg 4 + 5 + export function moreMenuIconTemplate() { 6 + return html`<div class="icon more-menu-icon"> 7 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"> 8 + <path 9 + stroke="currentColor" 10 + stroke-linecap="round" 11 + stroke-linejoin="round" 12 + stroke-width="2" 13 + d="M12 12h.01M8 12h.01M16 12h.01" 14 + /> 15 + </svg> 16 + </div>`; 17 + }
+17
src/js/templates/icons/plusIcon.template.js
··· 1 + import { html } from "/js/lib/lit-html.js"; 2 + 3 + //github.com/halfmage/majesticons/blob/main/line/plus-line.svg 4 + 5 + export function plusIconTemplate() { 6 + return html`<div class="icon plus-icon"> 7 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"> 8 + <path 9 + stroke="currentColor" 10 + stroke-linecap="round" 11 + stroke-linejoin="round" 12 + stroke-width="2" 13 + d="M5 12h7m7 0h-7m0 0V5m0 7v7" 14 + /> 15 + </svg> 16 + </div>`; 17 + }
+6 -3
src/js/templates/profileCard.template.js
··· 10 10 import { showToast } from "/js/toasts.js"; 11 11 import { avatarTemplate } from "/js/templates/avatar.template.js"; 12 12 import { chatIconTemplate } from "/js/templates/icons/chatIcon.template.js"; 13 + import { plusIconTemplate } from "/js/templates/icons/plusIcon.template.js"; 13 14 import { notificationsIconTemplate } from "/js/templates/icons/notificationsIcon.template.js"; 14 15 import { 15 16 formatLargeNumber, ··· 346 347 data-testid="subscribe-button" 347 348 data-teststate=${isSubscribed ? "subscribed" : "not-subscribed"} 348 349 > 349 - ${isSubscribed ? "Subscribed" : "+ Subscribe"} 350 + ${isSubscribed 351 + ? "Subscribed" 352 + : html`${plusIconTemplate()} Subscribe`} 350 353 </button>`; 351 354 } else { 352 355 return null; ··· 372 375 ${isFollowing 373 376 ? "Following" 374 377 : isFollowedBy 375 - ? "+ Follow back" 376 - : "+ Follow"} 378 + ? html`${plusIconTemplate()} Follow back` 379 + : html`${plusIconTemplate()} Follow`} 377 380 </button>`; 378 381 })()} 379 382 <button
+3 -2
src/js/templates/profileFeed.template.js
··· 1 1 import { html } from "/js/lib/lit-html.js"; 2 2 import { avatarTemplate } from "/js/templates/avatar.template.js"; 3 + import { plusIconTemplate } from "/js/templates/icons/plusIcon.template.js"; 3 4 import { linkToProfile } from "/js/navigation.js"; 4 5 import { verificationBadgeTemplate } from "/js/templates/verificationBadge.template.js"; 5 6 import { automatedAccountBadgeTemplate } from "/js/templates/automatedAccountBadge.template.js"; ··· 80 81 ${isFollowing 81 82 ? "Following" 82 83 : isFollowedBy 83 - ? "+ Follow back" 84 - : "+ Follow"} 84 + ? html`${plusIconTemplate()} Follow back` 85 + : html`${plusIconTemplate()} Follow`} 85 86 </button>` 86 87 : ""} 87 88 </div>
+2 -1
src/js/templates/smallPost.template.js
··· 16 16 import { repostIconTemplate } from "/js/templates/icons/repostIcon.template.js"; 17 17 import { pinIconTemplate } from "/js/templates/icons/pinIcon.template.js"; 18 18 import { infoIconTemplate } from "/js/templates/icons/infoIcon.template.js"; 19 + import { cornerDownRightIconTemplate } from "/js/templates/icons/cornerDownRightIcon.template.js"; 19 20 import { postLabelsTemplate } from "/js/templates/postLabels.template.js"; 20 21 import { blockedPostTemplate } from "/js/templates/blockedPost.template.js"; 21 22 import { notFoundPostTemplate } from "/js/templates/notFoundPost.template.js"; ··· 140 141 : ""} 141 142 ${showReplyToLabel 142 143 ? html`<div class="reply-to-author"> 143 - ⤷ Replied to 144 + ${cornerDownRightIconTemplate()} Replied to 144 145 ${replyToAuthor 145 146 ? replyToAuthor.did === currentUser?.did 146 147 ? " you"
+4 -1
src/js/views/chatDetail.view.js
··· 36 36 import { getPermalinkForConvo } from "/js/navigation.js"; 37 37 import { emojiIconTemplate } from "/js/templates/icons/emojiIcon.template.js"; 38 38 import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js"; 39 + import { cornerDownRightIconTemplate } from "/js/templates/icons/cornerDownRightIcon.template.js"; 39 40 import "/js/components/infinite-scroll-container.js"; 40 41 import "/js/components/chat-input.js"; 41 42 import "/js/components/emoji-picker-dialog.js"; ··· 899 900 scrollToAndHighlightMessage(replyTo.id); 900 901 }} 901 902 > 902 - <span class="message-reply-caption-arrow" aria-hidden="true">⤷</span> 903 + <span class="message-reply-caption-arrow" aria-hidden="true" 904 + >${cornerDownRightIconTemplate()}</span 905 + > 903 906 <span>${captionText}</span> 904 907 </div>`; 905 908 }
+2 -1
src/js/views/settings.view.js
··· 7 7 import { restrictedIconTemplate } from "/js/templates/icons/restrictedIcon.template.js"; 8 8 import { codeIconTemplate } from "/js/templates/icons/codeIcon.template.js"; 9 9 import { boxIconTemplate } from "/js/templates/icons/boxIcon.template.js"; 10 + import { moreMenuIconTemplate } from "/js/templates/icons/moreMenuIcon.template.js"; 10 11 import { auth, getLoginErrorMessage } from "/js/auth.js"; 11 12 import { headerTemplate } from "/js/templates/header.template.js"; 12 13 import { chevronRightIconTemplate } from "/js/templates/icons/chevronRight.template.js"; ··· 140 141 ); 141 142 }} 142 143 > 143 - <span>⋯</span> 144 + ${moreMenuIconTemplate()} 144 145 </button> 145 146 <context-menu> 146 147 <context-menu-item
+2 -2
src/js/views/settings/mutedWords.view.js
··· 3 3 import { pageEffect } from "/js/router.js"; 4 4 import { headerTemplate } from "/js/templates/header.template.js"; 5 5 import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js"; 6 + import { plusIconTemplate } from "/js/templates/icons/plusIcon.template.js"; 6 7 import { auth } from "/js/auth.js"; 7 8 import { confirmModal } from "/js/modals/confirm.modal.js"; 8 9 import { differenceInHours, differenceInDays } from "/js/utils.js"; ··· 333 334 > 334 335 ${state.$isSaving.get() 335 336 ? html`<div class="loading-spinner"></div>` 336 - : html`<span>Add</span 337 - ><span class="button-plus-icon">+</span>`} 337 + : html`<span>Add</span>${plusIconTemplate()}`} 338 338 </button> 339 339 ${state.$error.get() 340 340 ? html`<div
+9 -5
tests/e2e/specs/flows/followUser.test.js
··· 27 27 28 28 const profileView = page.locator("#profile-view"); 29 29 await expect( 30 - profileView.locator('[data-testid="follow-button"]'), 31 - ).toContainText("+ Follow", { timeout: 10000 }); 30 + profileView.locator( 31 + '[data-testid="follow-button"][data-teststate="follow"]', 32 + ), 33 + ).toBeVisible({ timeout: 10000 }); 32 34 await expect( 33 35 profileView.locator('[data-testid="profile-stats"]'), 34 36 ).toContainText("120 followers"); ··· 106 108 // Click the follow button to unfollow 107 109 await profileView.locator('[data-testid="follow-button"]').click(); 108 110 109 - // Verify the button changes to "+ Follow" 111 + // Verify the button changes back to the follow state 110 112 await expect( 111 - profileView.locator('[data-testid="follow-button"]'), 112 - ).toContainText("+ Follow", { timeout: 10000 }); 113 + profileView.locator( 114 + '[data-testid="follow-button"][data-teststate="follow"]', 115 + ), 116 + ).toBeVisible({ timeout: 10000 }); 113 117 114 118 // Verify the success toast is shown 115 119 await expect(page.locator('[data-testid="toast"]')).toContainText(
+3 -4
tests/e2e/specs/views/profile.view.test.js
··· 238 238 await page.goto(`/profile/${otherUser.did}`); 239 239 240 240 const view = page.locator("#profile-view"); 241 - await expect(view.locator('[data-testid="follow-button"]')).toContainText( 242 - "+ Follow", 243 - { timeout: 10000 }, 244 - ); 241 + await expect( 242 + view.locator('[data-testid="follow-button"][data-teststate="follow"]'), 243 + ).toBeVisible({ timeout: 10000 }); 245 244 }); 246 245 247 246 test("should show 'Following' button for followed profiles", async ({
+1 -1
tests/unit/specs/templates/profileCard.template.test.js
··· 824 824 container 825 825 .querySelector("[data-testid='follow-button']") 826 826 .textContent.trim(), 827 - "+ Follow", 827 + "Follow", 828 828 ); 829 829 }); 830 830
+1 -1
tests/unit/specs/templates/smallPost.template.test.js
··· 322 322 const label = container.querySelector(".reply-to-author"); 323 323 assert(label !== null); 324 324 const text = label.textContent.replace(/\s+/g, " ").trim(); 325 - assertEquals(text, "⤷ Replied to user"); 325 + assertEquals(text, "Replied to user"); 326 326 }); 327 327 }); 328 328