[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.

Update muted small post rendering

Grace Kind (May 2, 2026, 2:34 PM -0500) eaea5b32 161459d2

+432 -301
+1 -1
package.json
··· 1 1 { 2 2 "name": "impro", 3 - "version": "0.11.37", 3 + "version": "0.11.38", 4 4 "type": "module", 5 5 "scripts": { 6 6 "start": "rm -rf build && NODE_ENV=development eleventy --serve",
+4 -4
src/css/style.css
··· 2706 2706 } 2707 2707 } 2708 2708 2709 - .muted-reply-toggle-button-icon { 2709 + .muted-parent-toggle-button-icon { 2710 2710 width: 20px; 2711 2711 height: 20px; 2712 2712 } 2713 2713 2714 - .muted-reply-toggle-button { 2714 + .muted-parent-toggle-button { 2715 2715 display: flex; 2716 2716 align-items: center; 2717 2717 gap: 16px; ··· 2724 2724 } 2725 2725 2726 2726 /* i don't know why the hidden attribute isn't working, but this hides it */ 2727 - .muted-reply-toggle.expanded .muted-reply-toggle-button { 2727 + .muted-parent-toggle.expanded .muted-parent-toggle-button { 2728 2728 display: none; 2729 2729 } 2730 2730 2731 2731 @media (min-width: 800px) { 2732 - .muted-reply-toggle-button:hover { 2732 + .muted-parent-toggle-button:hover { 2733 2733 background-color: var(--post-hover-color); 2734 2734 } 2735 2735 }
+6 -6
src/js/components/muted-reply-toggle.js src/js/components/muted-parent-toggle.js
··· 3 3 import { eyeSlashIconTemplate } from "/js/templates/icons/eyeSlashIcon.template.js"; 4 4 import { classnames } from "/js/utils.js"; 5 5 6 - class MutedReplyToggle extends Component { 6 + class MutedParentToggle extends Component { 7 7 connectedCallback() { 8 8 if (this.initialized) { 9 9 return; 10 10 } 11 - this.label = this.getAttribute("label") ?? "Muted reply"; 11 + this.label = this.getAttribute("label") ?? "Muted parent"; 12 12 this.expanded = false; 13 13 this._children = getChildrenFragment(this); 14 14 this.innerHTML = ""; ··· 20 20 render( 21 21 html` 22 22 <div 23 - class=${classnames("muted-reply-toggle", { 23 + class=${classnames("muted-parent-toggle", { 24 24 expanded: this.expanded, 25 25 })} 26 26 aria-expanded=${this.expanded} 27 27 > 28 28 <div 29 - class="muted-reply-toggle-button" 29 + class="muted-parent-toggle-button" 30 30 tabindex="0" 31 31 role="button" 32 32 ?hidden=${this.expanded} ··· 39 39 } 40 40 }} 41 41 > 42 - <div class="muted-reply-toggle-button-icon"> 42 + <div class="muted-parent-toggle-button-icon"> 43 43 ${eyeSlashIconTemplate()} 44 44 </div> 45 45 <span>${this.label}</span> ··· 63 63 } 64 64 } 65 65 66 - MutedReplyToggle.register(); 66 + MutedParentToggle.register();
+21
src/js/templates/mutedParentToggle.template.js
··· 1 + import { html } from "/js/lib/lit-html.js"; 2 + import "/js/components/muted-parent-toggle.js"; 3 + 4 + export function mutedParentToggleTemplate({ post, children }) { 5 + if (post.author?.viewer?.muted) { 6 + return html`<muted-parent-toggle label="Muted account"> 7 + ${children} 8 + </muted-parent-toggle>`; 9 + } 10 + if (post.viewer?.hasMutedWord) { 11 + return html`<muted-parent-toggle label="Hidden by muted word"> 12 + ${children} 13 + </muted-parent-toggle>`; 14 + } 15 + if (post.viewer?.isHidden) { 16 + return html`<muted-parent-toggle label="Post hidden by you"> 17 + ${children} 18 + </muted-parent-toggle>`; 19 + } 20 + return children; 21 + }
+13 -8
src/js/templates/postFeed.template.js
··· 1 1 import { html, keyed } from "/js/lib/lit-html.js"; 2 2 import { smallPostTemplate } from "/js/templates/smallPost.template.js"; 3 + import { mutedParentToggleTemplate } from "/js/templates/mutedParentToggle.template.js"; 3 4 import { postSkeletonTemplate } from "/js/templates/postSkeleton.template.js"; 4 5 import { linkToPost } from "/js/navigation.js"; 5 6 ··· 18 19 `; 19 20 } 20 21 21 - function postTemplate({ post, hiddenPostUris, ...props }) { 22 + function postTemplate({ post, hiddenPostUris, isParent = false, ...props }) { 22 23 if (hiddenPostUris.includes(post.uri)) { 23 24 return feedFeedbackMessageTemplate({ post }); 24 - } else { 25 - return smallPostTemplate({ 26 - post, 27 - ...props, 28 - }); 25 + } 26 + const rendered = smallPostTemplate({ 27 + post, 28 + ...props, 29 + ignoreMuteWarning: isParent || props.ignoreMuteWarning, 30 + }); 31 + if (isParent) { 32 + return mutedParentToggleTemplate({ post, children: rendered }); 29 33 } 34 + return rendered; 30 35 } 31 36 32 37 function replyContextTemplate({ ··· 61 66 onClickShowLess, 62 67 onClickShowMore, 63 68 enableFeedFeedback, 64 - hideMutedAccount: true, 69 + isParent: true, 65 70 })} 66 71 ` 67 72 : ""} ··· 90 95 onClickShowLess, 91 96 onClickShowMore, 92 97 enableFeedFeedback, 93 - hideMutedAccount: true, 98 + isParent: true, 94 99 })} 95 100 ` 96 101 : ""}
+26 -22
src/js/templates/smallPost.template.js
··· 22 22 import { unavailablePostTemplate } from "/js/templates/unavailablePost.template.js"; 23 23 import { moderationWarningTemplate } from "/js/templates/moderationWarning.template.js"; 24 24 import "/js/components/lightbox-image-group.js"; 25 - import "/js/components/muted-reply-toggle.js"; 26 25 27 - function contentWarningTemplate({ post, contentLabel, children }) { 26 + function contentWarningTemplate({ 27 + post, 28 + ignoreContentWarning, 29 + ignoreMuteWarning, 30 + children, 31 + }) { 32 + const contentLabel = ignoreContentWarning ? null : post.contentLabel; 28 33 if (contentLabel && contentLabel.visibility !== "ignore") { 29 34 const isAuthorLabel = contentLabel.label.uri === post?.author?.did; 30 35 return moderationWarningTemplate({ ··· 35 40 children, 36 41 }); 37 42 } 38 - 43 + if (!ignoreMuteWarning) { 44 + if (post.viewer?.hasMutedWord) { 45 + return html`<moderation-warning 46 + label="Hidden by muted word" 47 + icon-style="closed-eye" 48 + >${children}</moderation-warning 49 + >`; 50 + } 51 + if (post.viewer?.isHidden) { 52 + return html`<moderation-warning 53 + label="Post hidden by you" 54 + icon-style="closed-eye" 55 + >${children}</moderation-warning 56 + >`; 57 + } 58 + } 39 59 return children; 40 60 } 41 61 ··· 48 68 replyContext, 49 69 repostAuthor, 50 70 ignoreContentWarning = false, 71 + ignoreMuteWarning = false, 51 72 isPinned = false, 52 73 onClickShowLess = noop, 53 74 onClickShowMore = noop, 54 75 enableFeedFeedback = false, 55 - hideMutedAccount = false, 56 - overrideMutedWords = false, 57 76 showReplyToLabel = false, 58 77 replyToAuthor = null, 59 78 lazyLoadImages = false, ··· 138 157 : ""} 139 158 ${contentWarningTemplate({ 140 159 post, 141 - contentLabel: ignoreContentWarning ? null : post.contentLabel, 142 - isAuthenticated, 160 + ignoreContentWarning, 161 + ignoreMuteWarning, 143 162 children: html` <div class="post-body"> 144 163 ${hideUnauthenticated 145 164 ? html`<div class="missing-post-indicator no-unauthenticated"> ··· 201 220 </div> 202 221 `; 203 222 204 - if (hideMutedAccount && post.author.viewer?.muted) { 205 - return html`<muted-reply-toggle label="Muted account"> 206 - ${content} 207 - </muted-reply-toggle>`; 208 - } 209 - if (post.viewer?.hasMutedWord && !overrideMutedWords) { 210 - return html`<muted-reply-toggle label="Hidden by muted word"> 211 - ${content} 212 - </muted-reply-toggle>`; 213 - } 214 - if (post.viewer?.isHidden) { 215 - return html`<muted-reply-toggle label="Post hidden by you"> 216 - ${content} 217 - </muted-reply-toggle>`; 218 - } 219 223 return content; 220 224 }
+1 -1
src/js/views/notifications.view.js
··· 416 416 showReplyToLabel: !!replyToAuthor, 417 417 replyToAuthor, 418 418 postInteractionHandler, 419 - overrideMutedWords: true, 419 + ignoreMuteWarning: true, 420 420 })} 421 421 </div> 422 422 `;
+12 -8
src/js/views/postThread.view.js
··· 3 3 import { sortBy } from "/js/utils.js"; 4 4 import { textHeaderTemplate } from "/js/templates/textHeader.template.js"; 5 5 import { smallPostTemplate } from "/js/templates/smallPost.template.js"; 6 + import { mutedParentToggleTemplate } from "/js/templates/mutedParentToggle.template.js"; 6 7 import { largePostTemplate } from "/js/templates/largePost.template.js"; 7 8 import { postSkeletonTemplate } from "/js/templates/postSkeleton.template.js"; 8 9 import { mainLayoutTemplate } from "/js/templates/mainLayout.template.js"; ··· 275 276 isUserPost: currentUser?.did === reply.post?.author?.did, 276 277 postInteractionHandler, 277 278 ignoreContentWarning: true, 278 - overrideMutedWords: true, 279 + ignoreMuteWarning: true, 279 280 lazyLoadImages: true, 280 281 }), 281 282 )} ··· 365 366 ) { 366 367 return noUnauthenticatedSmallPostTemplate({ replyContext }); 367 368 } 368 - return smallPostTemplate({ 369 + return mutedParentToggleTemplate({ 369 370 post: parentPost, 370 - currentUser, 371 - isAuthenticated, 372 - isUserPost: currentUser?.did === parentPost.author?.did, 373 - postInteractionHandler, 374 - replyContext, 375 - hideMutedAccount: true, 371 + children: smallPostTemplate({ 372 + post: parentPost, 373 + currentUser, 374 + isAuthenticated, 375 + isUserPost: currentUser?.did === parentPost.author?.did, 376 + postInteractionHandler, 377 + replyContext, 378 + ignoreMuteWarning: true, 379 + }), 376 380 }); 377 381 })} 378 382 ${hiddenUnauthenticated
+8 -8
tests/e2e/specs/flows/hidePost.test.js
··· 115 115 await expect(confirmButton).toBeVisible({ timeout: 5000 }); 116 116 await confirmButton.click(); 117 117 118 - // The reply should show "Post hidden by you" wrapper 119 - const mutedToggle = view.locator("muted-reply-toggle"); 120 - await expect(mutedToggle).toBeVisible({ timeout: 10000 }); 121 - await expect(mutedToggle).toContainText("Post hidden by you"); 118 + // The reply should show a "Post hidden by you" moderation warning 119 + const warning = view.locator("moderation-warning"); 120 + await expect(warning).toBeVisible({ timeout: 10000 }); 121 + await expect(warning).toContainText("Post hidden by you"); 122 122 123 - // Expanding the wrapper should reveal the content 124 - await mutedToggle.locator(".muted-reply-toggle-button").click(); 125 - await expect(mutedToggle.locator(".toggle-content")).toBeVisible({ 123 + // Expanding the warning should reveal the content 124 + await warning.locator(".top-bar").click(); 125 + await expect(warning.locator(".toggle-content")).toBeVisible({ 126 126 timeout: 5000, 127 127 }); 128 - await expect(mutedToggle).toContainText("Reply to hide in thread"); 128 + await expect(warning).toContainText("Reply to hide in thread"); 129 129 }); 130 130 });
+4 -4
tests/e2e/specs/views/postThread.view.test.js
··· 1190 1190 await page.goto("/profile/author1.bsky.social/post/abc123"); 1191 1191 1192 1192 const view = page.locator("#post-detail-view"); 1193 - const toggle = view.locator("muted-reply-toggle"); 1193 + const toggle = view.locator("muted-parent-toggle"); 1194 1194 await expect(toggle).toBeVisible({ timeout: 10000 }); 1195 1195 1196 1196 // Content should be hidden initially 1197 1197 await expect(toggle.locator(".toggle-content")).toBeHidden(); 1198 1198 1199 1199 // Click to expand 1200 - await toggle.locator(".muted-reply-toggle-button").click(); 1200 + await toggle.locator(".muted-parent-toggle-button").click(); 1201 1201 await expect(toggle.locator(".toggle-content")).toBeVisible(); 1202 1202 await expect(toggle).toContainText("This is a muted parent post"); 1203 1203 }); ··· 1243 1243 await page.goto("/profile/author1.bsky.social/post/abc123"); 1244 1244 1245 1245 const view = page.locator("#post-detail-view"); 1246 - const toggle = view.locator("muted-reply-toggle"); 1246 + const toggle = view.locator("muted-parent-toggle"); 1247 1247 await expect(toggle).toBeVisible({ timeout: 10000 }); 1248 1248 await expect(toggle).toContainText("Hidden by muted word"); 1249 1249 }); ··· 1289 1289 await page.goto("/profile/author1.bsky.social/post/abc123"); 1290 1290 1291 1291 const view = page.locator("#post-detail-view"); 1292 - const toggle = view.locator("muted-reply-toggle"); 1292 + const toggle = view.locator("muted-parent-toggle"); 1293 1293 await expect(toggle).toBeVisible({ timeout: 10000 }); 1294 1294 await expect(toggle).toContainText("Post hidden by you"); 1295 1295 });
+218
tests/unit/specs/components/muted-parent-toggle.test.js
··· 1 + import { TestSuite } from "../../testSuite.js"; 2 + import { assert, assertEquals } from "../../testHelpers.js"; 3 + import "/js/components/muted-parent-toggle.js"; 4 + 5 + const t = new TestSuite("MutedParentToggle"); 6 + 7 + t.beforeEach(() => { 8 + document.body.innerHTML = ""; 9 + }); 10 + 11 + t.describe("MutedParentToggle - rendering", (it) => { 12 + it("should render muted-parent-toggle div", () => { 13 + const element = document.createElement("muted-parent-toggle"); 14 + document.body.appendChild(element); 15 + const toggle = element.querySelector(".muted-parent-toggle"); 16 + assert(toggle !== null); 17 + }); 18 + 19 + it("should render muted-parent-toggle-button", () => { 20 + const element = document.createElement("muted-parent-toggle"); 21 + document.body.appendChild(element); 22 + const button = element.querySelector(".muted-parent-toggle-button"); 23 + assert(button !== null); 24 + }); 25 + 26 + it("should render toggle-content div", () => { 27 + const element = document.createElement("muted-parent-toggle"); 28 + document.body.appendChild(element); 29 + const content = element.querySelector(".toggle-content"); 30 + assert(content !== null); 31 + }); 32 + 33 + it("should display default label 'Muted parent'", () => { 34 + const element = document.createElement("muted-parent-toggle"); 35 + document.body.appendChild(element); 36 + const button = element.querySelector(".muted-parent-toggle-button"); 37 + assert(button.textContent.includes("Muted parent")); 38 + }); 39 + 40 + it("should display custom label when provided", () => { 41 + const element = document.createElement("muted-parent-toggle"); 42 + element.setAttribute("label", "Hidden by mute list"); 43 + document.body.appendChild(element); 44 + const button = element.querySelector(".muted-parent-toggle-button"); 45 + assert(button.textContent.includes("Hidden by mute list")); 46 + }); 47 + 48 + it("should display 'Show' text", () => { 49 + const element = document.createElement("muted-parent-toggle"); 50 + document.body.appendChild(element); 51 + const showMore = element.querySelector(".muted-account-show-more"); 52 + assert(showMore !== null); 53 + assertEquals(showMore.textContent, "Show"); 54 + }); 55 + 56 + it("should preserve children in toggle-content", () => { 57 + const element = document.createElement("muted-parent-toggle"); 58 + element.innerHTML = "<div class='test-child'>Muted Parent Content</div>"; 59 + document.body.appendChild(element); 60 + const child = element.querySelector(".toggle-content .test-child"); 61 + assert(child !== null); 62 + assertEquals(child.textContent, "Muted Parent Content"); 63 + }); 64 + 65 + it("should render icon", () => { 66 + const element = document.createElement("muted-parent-toggle"); 67 + document.body.appendChild(element); 68 + const icon = element.querySelector(".muted-parent-toggle-button-icon"); 69 + assert(icon !== null); 70 + }); 71 + }); 72 + 73 + t.describe("MutedParentToggle - initial state", (it) => { 74 + it("should start with expanded set to false", () => { 75 + const element = document.createElement("muted-parent-toggle"); 76 + document.body.appendChild(element); 77 + assertEquals(element.expanded, false); 78 + }); 79 + 80 + it("should have aria-expanded set to false initially", () => { 81 + const element = document.createElement("muted-parent-toggle"); 82 + document.body.appendChild(element); 83 + const toggle = element.querySelector(".muted-parent-toggle"); 84 + assertEquals(toggle.getAttribute("aria-expanded"), "false"); 85 + }); 86 + 87 + it("should show button initially", () => { 88 + const element = document.createElement("muted-parent-toggle"); 89 + document.body.appendChild(element); 90 + const button = element.querySelector(".muted-parent-toggle-button"); 91 + assert(!button.hidden); 92 + }); 93 + 94 + it("should hide toggle-content initially", () => { 95 + const element = document.createElement("muted-parent-toggle"); 96 + document.body.appendChild(element); 97 + const content = element.querySelector(".toggle-content"); 98 + assert(content.hidden); 99 + }); 100 + 101 + it("should not have expanded class initially", () => { 102 + const element = document.createElement("muted-parent-toggle"); 103 + document.body.appendChild(element); 104 + const toggle = element.querySelector(".muted-parent-toggle"); 105 + assert(!toggle.classList.contains("expanded")); 106 + }); 107 + }); 108 + 109 + t.describe("MutedParentToggle - toggle", (it) => { 110 + it("should set expanded to true when toggle() is called", () => { 111 + const element = document.createElement("muted-parent-toggle"); 112 + document.body.appendChild(element); 113 + element.toggle(); 114 + assertEquals(element.expanded, true); 115 + }); 116 + 117 + it("should update aria-expanded when toggled", () => { 118 + const element = document.createElement("muted-parent-toggle"); 119 + document.body.appendChild(element); 120 + element.toggle(); 121 + const toggle = element.querySelector(".muted-parent-toggle"); 122 + assertEquals(toggle.getAttribute("aria-expanded"), "true"); 123 + }); 124 + 125 + it("should show toggle-content when expanded", () => { 126 + const element = document.createElement("muted-parent-toggle"); 127 + document.body.appendChild(element); 128 + element.toggle(); 129 + const content = element.querySelector(".toggle-content"); 130 + assert(!content.hidden); 131 + }); 132 + 133 + it("should hide button when expanded", () => { 134 + const element = document.createElement("muted-parent-toggle"); 135 + document.body.appendChild(element); 136 + element.toggle(); 137 + const button = element.querySelector(".muted-parent-toggle-button"); 138 + assert(button.hidden); 139 + }); 140 + 141 + it("should add expanded class when expanded", () => { 142 + const element = document.createElement("muted-parent-toggle"); 143 + document.body.appendChild(element); 144 + element.toggle(); 145 + const toggle = element.querySelector(".muted-parent-toggle"); 146 + assert(toggle.classList.contains("expanded")); 147 + }); 148 + 149 + it("should toggle back to collapsed state", () => { 150 + const element = document.createElement("muted-parent-toggle"); 151 + document.body.appendChild(element); 152 + element.toggle(); 153 + element.toggle(); 154 + assertEquals(element.expanded, false); 155 + }); 156 + }); 157 + 158 + t.describe("MutedParentToggle - click interaction", (it) => { 159 + it("should toggle when button is clicked", () => { 160 + const element = document.createElement("muted-parent-toggle"); 161 + document.body.appendChild(element); 162 + const button = element.querySelector(".muted-parent-toggle-button"); 163 + button.click(); 164 + assertEquals(element.expanded, true); 165 + }); 166 + }); 167 + 168 + t.describe("MutedParentToggle - keyboard interaction", (it) => { 169 + it("should toggle when Enter is pressed on button", () => { 170 + const element = document.createElement("muted-parent-toggle"); 171 + document.body.appendChild(element); 172 + const button = element.querySelector(".muted-parent-toggle-button"); 173 + const event = new window.KeyboardEvent("keydown", { key: "Enter" }); 174 + button.dispatchEvent(event); 175 + assertEquals(element.expanded, true); 176 + }); 177 + 178 + it("should toggle when Space is pressed on button", () => { 179 + const element = document.createElement("muted-parent-toggle"); 180 + document.body.appendChild(element); 181 + const button = element.querySelector(".muted-parent-toggle-button"); 182 + const event = new window.KeyboardEvent("keydown", { key: " " }); 183 + button.dispatchEvent(event); 184 + assertEquals(element.expanded, true); 185 + }); 186 + }); 187 + 188 + t.describe("MutedParentToggle - accessibility", (it) => { 189 + it("should have tabindex on button", () => { 190 + const element = document.createElement("muted-parent-toggle"); 191 + document.body.appendChild(element); 192 + const button = element.querySelector(".muted-parent-toggle-button"); 193 + assertEquals(button.getAttribute("tabindex"), "0"); 194 + }); 195 + 196 + it("should have role button", () => { 197 + const element = document.createElement("muted-parent-toggle"); 198 + document.body.appendChild(element); 199 + const button = element.querySelector(".muted-parent-toggle-button"); 200 + assertEquals(button.getAttribute("role"), "button"); 201 + }); 202 + }); 203 + 204 + t.describe("MutedParentToggle - reinitialization protection", (it) => { 205 + it("should not reinitialize when connectedCallback is called multiple times", () => { 206 + const element = document.createElement("muted-parent-toggle"); 207 + element.innerHTML = "<span class='test'>Original</span>"; 208 + document.body.appendChild(element); 209 + 210 + element.connectedCallback(); 211 + 212 + const child = element.querySelector(".toggle-content .test"); 213 + assert(child !== null); 214 + assertEquals(child.textContent, "Original"); 215 + }); 216 + }); 217 + 218 + await t.run();
-218
tests/unit/specs/components/muted-reply-toggle.test.js
··· 1 - import { TestSuite } from "../../testSuite.js"; 2 - import { assert, assertEquals } from "../../testHelpers.js"; 3 - import "/js/components/muted-reply-toggle.js"; 4 - 5 - const t = new TestSuite("MutedReplyToggle"); 6 - 7 - t.beforeEach(() => { 8 - document.body.innerHTML = ""; 9 - }); 10 - 11 - t.describe("MutedReplyToggle - rendering", (it) => { 12 - it("should render muted-reply-toggle div", () => { 13 - const element = document.createElement("muted-reply-toggle"); 14 - document.body.appendChild(element); 15 - const toggle = element.querySelector(".muted-reply-toggle"); 16 - assert(toggle !== null); 17 - }); 18 - 19 - it("should render muted-reply-toggle-button", () => { 20 - const element = document.createElement("muted-reply-toggle"); 21 - document.body.appendChild(element); 22 - const button = element.querySelector(".muted-reply-toggle-button"); 23 - assert(button !== null); 24 - }); 25 - 26 - it("should render toggle-content div", () => { 27 - const element = document.createElement("muted-reply-toggle"); 28 - document.body.appendChild(element); 29 - const content = element.querySelector(".toggle-content"); 30 - assert(content !== null); 31 - }); 32 - 33 - it("should display default label 'Muted reply'", () => { 34 - const element = document.createElement("muted-reply-toggle"); 35 - document.body.appendChild(element); 36 - const button = element.querySelector(".muted-reply-toggle-button"); 37 - assert(button.textContent.includes("Muted reply")); 38 - }); 39 - 40 - it("should display custom label when provided", () => { 41 - const element = document.createElement("muted-reply-toggle"); 42 - element.setAttribute("label", "Hidden by mute list"); 43 - document.body.appendChild(element); 44 - const button = element.querySelector(".muted-reply-toggle-button"); 45 - assert(button.textContent.includes("Hidden by mute list")); 46 - }); 47 - 48 - it("should display 'Show' text", () => { 49 - const element = document.createElement("muted-reply-toggle"); 50 - document.body.appendChild(element); 51 - const showMore = element.querySelector(".muted-account-show-more"); 52 - assert(showMore !== null); 53 - assertEquals(showMore.textContent, "Show"); 54 - }); 55 - 56 - it("should preserve children in toggle-content", () => { 57 - const element = document.createElement("muted-reply-toggle"); 58 - element.innerHTML = "<div class='test-child'>Muted Reply Content</div>"; 59 - document.body.appendChild(element); 60 - const child = element.querySelector(".toggle-content .test-child"); 61 - assert(child !== null); 62 - assertEquals(child.textContent, "Muted Reply Content"); 63 - }); 64 - 65 - it("should render icon", () => { 66 - const element = document.createElement("muted-reply-toggle"); 67 - document.body.appendChild(element); 68 - const icon = element.querySelector(".muted-reply-toggle-button-icon"); 69 - assert(icon !== null); 70 - }); 71 - }); 72 - 73 - t.describe("MutedReplyToggle - initial state", (it) => { 74 - it("should start with expanded set to false", () => { 75 - const element = document.createElement("muted-reply-toggle"); 76 - document.body.appendChild(element); 77 - assertEquals(element.expanded, false); 78 - }); 79 - 80 - it("should have aria-expanded set to false initially", () => { 81 - const element = document.createElement("muted-reply-toggle"); 82 - document.body.appendChild(element); 83 - const toggle = element.querySelector(".muted-reply-toggle"); 84 - assertEquals(toggle.getAttribute("aria-expanded"), "false"); 85 - }); 86 - 87 - it("should show button initially", () => { 88 - const element = document.createElement("muted-reply-toggle"); 89 - document.body.appendChild(element); 90 - const button = element.querySelector(".muted-reply-toggle-button"); 91 - assert(!button.hidden); 92 - }); 93 - 94 - it("should hide toggle-content initially", () => { 95 - const element = document.createElement("muted-reply-toggle"); 96 - document.body.appendChild(element); 97 - const content = element.querySelector(".toggle-content"); 98 - assert(content.hidden); 99 - }); 100 - 101 - it("should not have expanded class initially", () => { 102 - const element = document.createElement("muted-reply-toggle"); 103 - document.body.appendChild(element); 104 - const toggle = element.querySelector(".muted-reply-toggle"); 105 - assert(!toggle.classList.contains("expanded")); 106 - }); 107 - }); 108 - 109 - t.describe("MutedReplyToggle - toggle", (it) => { 110 - it("should set expanded to true when toggle() is called", () => { 111 - const element = document.createElement("muted-reply-toggle"); 112 - document.body.appendChild(element); 113 - element.toggle(); 114 - assertEquals(element.expanded, true); 115 - }); 116 - 117 - it("should update aria-expanded when toggled", () => { 118 - const element = document.createElement("muted-reply-toggle"); 119 - document.body.appendChild(element); 120 - element.toggle(); 121 - const toggle = element.querySelector(".muted-reply-toggle"); 122 - assertEquals(toggle.getAttribute("aria-expanded"), "true"); 123 - }); 124 - 125 - it("should show toggle-content when expanded", () => { 126 - const element = document.createElement("muted-reply-toggle"); 127 - document.body.appendChild(element); 128 - element.toggle(); 129 - const content = element.querySelector(".toggle-content"); 130 - assert(!content.hidden); 131 - }); 132 - 133 - it("should hide button when expanded", () => { 134 - const element = document.createElement("muted-reply-toggle"); 135 - document.body.appendChild(element); 136 - element.toggle(); 137 - const button = element.querySelector(".muted-reply-toggle-button"); 138 - assert(button.hidden); 139 - }); 140 - 141 - it("should add expanded class when expanded", () => { 142 - const element = document.createElement("muted-reply-toggle"); 143 - document.body.appendChild(element); 144 - element.toggle(); 145 - const toggle = element.querySelector(".muted-reply-toggle"); 146 - assert(toggle.classList.contains("expanded")); 147 - }); 148 - 149 - it("should toggle back to collapsed state", () => { 150 - const element = document.createElement("muted-reply-toggle"); 151 - document.body.appendChild(element); 152 - element.toggle(); 153 - element.toggle(); 154 - assertEquals(element.expanded, false); 155 - }); 156 - }); 157 - 158 - t.describe("MutedReplyToggle - click interaction", (it) => { 159 - it("should toggle when button is clicked", () => { 160 - const element = document.createElement("muted-reply-toggle"); 161 - document.body.appendChild(element); 162 - const button = element.querySelector(".muted-reply-toggle-button"); 163 - button.click(); 164 - assertEquals(element.expanded, true); 165 - }); 166 - }); 167 - 168 - t.describe("MutedReplyToggle - keyboard interaction", (it) => { 169 - it("should toggle when Enter is pressed on button", () => { 170 - const element = document.createElement("muted-reply-toggle"); 171 - document.body.appendChild(element); 172 - const button = element.querySelector(".muted-reply-toggle-button"); 173 - const event = new window.KeyboardEvent("keydown", { key: "Enter" }); 174 - button.dispatchEvent(event); 175 - assertEquals(element.expanded, true); 176 - }); 177 - 178 - it("should toggle when Space is pressed on button", () => { 179 - const element = document.createElement("muted-reply-toggle"); 180 - document.body.appendChild(element); 181 - const button = element.querySelector(".muted-reply-toggle-button"); 182 - const event = new window.KeyboardEvent("keydown", { key: " " }); 183 - button.dispatchEvent(event); 184 - assertEquals(element.expanded, true); 185 - }); 186 - }); 187 - 188 - t.describe("MutedReplyToggle - accessibility", (it) => { 189 - it("should have tabindex on button", () => { 190 - const element = document.createElement("muted-reply-toggle"); 191 - document.body.appendChild(element); 192 - const button = element.querySelector(".muted-reply-toggle-button"); 193 - assertEquals(button.getAttribute("tabindex"), "0"); 194 - }); 195 - 196 - it("should have role button", () => { 197 - const element = document.createElement("muted-reply-toggle"); 198 - document.body.appendChild(element); 199 - const button = element.querySelector(".muted-reply-toggle-button"); 200 - assertEquals(button.getAttribute("role"), "button"); 201 - }); 202 - }); 203 - 204 - t.describe("MutedReplyToggle - reinitialization protection", (it) => { 205 - it("should not reinitialize when connectedCallback is called multiple times", () => { 206 - const element = document.createElement("muted-reply-toggle"); 207 - element.innerHTML = "<span class='test'>Original</span>"; 208 - document.body.appendChild(element); 209 - 210 - element.connectedCallback(); 211 - 212 - const child = element.querySelector(".toggle-content .test"); 213 - assert(child !== null); 214 - assertEquals(child.textContent, "Original"); 215 - }); 216 - }); 217 - 218 - await t.run();
+93
tests/unit/specs/templates/mutedParentToggle.template.test.js
··· 1 + import { TestSuite } from "../../testSuite.js"; 2 + import { assert, assertEquals } from "../../testHelpers.js"; 3 + import { mutedParentToggleTemplate } from "/js/templates/mutedParentToggle.template.js"; 4 + import { render, html } from "/js/lib/lit-html.js"; 5 + 6 + const t = new TestSuite("mutedParentToggleTemplate"); 7 + 8 + const basePost = { 9 + uri: "at://did:plc:author/app.bsky.feed.post/abc", 10 + author: { did: "did:plc:author", viewer: {} }, 11 + viewer: {}, 12 + }; 13 + 14 + const children = html`<div class="parent-content">Parent post</div>`; 15 + 16 + t.describe("mutedParentToggleTemplate - muted account", (it) => { 17 + it("should wrap in muted-parent-toggle with 'Muted account' label", () => { 18 + const post = { 19 + ...basePost, 20 + author: { ...basePost.author, viewer: { muted: true } }, 21 + }; 22 + const container = document.createElement("div"); 23 + render(mutedParentToggleTemplate({ post, children }), container); 24 + const toggle = container.querySelector("muted-parent-toggle"); 25 + assert(toggle !== null); 26 + assertEquals(toggle.getAttribute("label"), "Muted account"); 27 + assert(toggle.querySelector(".parent-content") !== null); 28 + }); 29 + }); 30 + 31 + t.describe("mutedParentToggleTemplate - muted word", (it) => { 32 + it("should wrap in muted-parent-toggle with 'Hidden by muted word' label", () => { 33 + const post = { 34 + ...basePost, 35 + viewer: { hasMutedWord: true }, 36 + }; 37 + const container = document.createElement("div"); 38 + render(mutedParentToggleTemplate({ post, children }), container); 39 + const toggle = container.querySelector("muted-parent-toggle"); 40 + assert(toggle !== null); 41 + assertEquals(toggle.getAttribute("label"), "Hidden by muted word"); 42 + }); 43 + }); 44 + 45 + t.describe("mutedParentToggleTemplate - hidden post", (it) => { 46 + it("should wrap in muted-parent-toggle with 'Post hidden by you' label", () => { 47 + const post = { 48 + ...basePost, 49 + viewer: { isHidden: true }, 50 + }; 51 + const container = document.createElement("div"); 52 + render(mutedParentToggleTemplate({ post, children }), container); 53 + const toggle = container.querySelector("muted-parent-toggle"); 54 + assert(toggle !== null); 55 + assertEquals(toggle.getAttribute("label"), "Post hidden by you"); 56 + }); 57 + }); 58 + 59 + t.describe("mutedParentToggleTemplate - normal post", (it) => { 60 + it("should render children directly without wrapping", () => { 61 + const container = document.createElement("div"); 62 + render(mutedParentToggleTemplate({ post: basePost, children }), container); 63 + assertEquals(container.querySelector("muted-parent-toggle"), null); 64 + assert(container.querySelector(".parent-content") !== null); 65 + }); 66 + }); 67 + 68 + t.describe("mutedParentToggleTemplate - precedence", (it) => { 69 + it("should prefer muted account over muted word and hidden", () => { 70 + const post = { 71 + ...basePost, 72 + author: { ...basePost.author, viewer: { muted: true } }, 73 + viewer: { hasMutedWord: true, isHidden: true }, 74 + }; 75 + const container = document.createElement("div"); 76 + render(mutedParentToggleTemplate({ post, children }), container); 77 + const toggle = container.querySelector("muted-parent-toggle"); 78 + assertEquals(toggle.getAttribute("label"), "Muted account"); 79 + }); 80 + 81 + it("should prefer muted word over hidden", () => { 82 + const post = { 83 + ...basePost, 84 + viewer: { hasMutedWord: true, isHidden: true }, 85 + }; 86 + const container = document.createElement("div"); 87 + render(mutedParentToggleTemplate({ post, children }), container); 88 + const toggle = container.querySelector("muted-parent-toggle"); 89 + assertEquals(toggle.getAttribute("label"), "Hidden by muted word"); 90 + }); 91 + }); 92 + 93 + await t.run();
+25 -21
tests/unit/specs/templates/smallPost.template.test.js
··· 354 354 }); 355 355 356 356 t.describe("smallPostTemplate - moderation", (it) => { 357 - it("should wrap in muted-reply-toggle for muted account when hideMutedAccount is true", () => { 358 - const mutedAccountPost = { 359 - ...post, 360 - author: { ...post.author, viewer: { muted: true } }, 361 - }; 362 - const result = smallPostTemplate({ 363 - post: mutedAccountPost, 364 - ...baseProps, 365 - hideMutedAccount: true, 366 - }); 367 - const container = document.createElement("div"); 368 - render(result, container); 369 - assert(container.querySelector("muted-reply-toggle") !== null); 370 - }); 371 - 372 - it("should wrap in muted-reply-toggle for post with muted word", () => { 357 + it("should show a moderation warning for post with muted word", () => { 373 358 const mutedWordPost = { 374 359 ...post, 375 360 viewer: { ...post.viewer, hasMutedWord: true }, ··· 380 365 }); 381 366 const container = document.createElement("div"); 382 367 render(result, container); 383 - assert(container.querySelector("muted-reply-toggle") !== null); 368 + const warning = container.querySelector("moderation-warning"); 369 + assert(warning !== null); 370 + assertEquals(warning.getAttribute("label"), "Hidden by muted word"); 384 371 }); 385 372 386 - it("should wrap in muted-reply-toggle for hidden post", () => { 373 + it("should show a moderation warning for hidden post", () => { 387 374 const hiddenPost = { 388 375 ...post, 389 376 viewer: { ...post.viewer, isHidden: true }, ··· 394 381 }); 395 382 const container = document.createElement("div"); 396 383 render(result, container); 397 - assert(container.querySelector("muted-reply-toggle") !== null); 384 + const warning = container.querySelector("moderation-warning"); 385 + assert(warning !== null); 386 + assertEquals(warning.getAttribute("label"), "Post hidden by you"); 398 387 }); 399 388 400 - it("should not wrap in muted-reply-toggle for normal post", () => { 389 + it("should not show a moderation warning for normal post", () => { 401 390 const normalPost = { 402 391 ...post, 403 392 viewer: { ...post.viewer, hasMutedWord: false, isHidden: false }, ··· 409 398 }); 410 399 const container = document.createElement("div"); 411 400 render(result, container); 412 - assertEquals(container.querySelector("muted-reply-toggle"), null); 401 + assertEquals(container.querySelector("moderation-warning"), null); 402 + }); 403 + 404 + it("should suppress mute warning when ignoreMuteWarning is true", () => { 405 + const mutedWordPost = { 406 + ...post, 407 + viewer: { ...post.viewer, hasMutedWord: true }, 408 + }; 409 + const result = smallPostTemplate({ 410 + post: mutedWordPost, 411 + ...baseProps, 412 + ignoreMuteWarning: true, 413 + }); 414 + const container = document.createElement("div"); 415 + render(result, container); 416 + assertEquals(container.querySelector("moderation-warning"), null); 413 417 }); 414 418 it("should show author info and lock message for !no-unauthenticated posts when logged out", () => { 415 419 const restrictedPost = {