From 54b6bc6ef0c44850ef4a23fae3d2e6d3f20e950f Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Thu, 13 Aug 2026 18:46:42 -0500 Subject: [PATCH 01/19] Shorten delay for showing global loading spinner --- package.json | 2 +- src/index.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e5dd42a8..52552395 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.178", + "version": "0.18.179", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/index.html b/src/index.html index 669f8079..2a9860c6 100644 --- a/src/index.html +++ b/src/index.html @@ -34,13 +34,13 @@ `; } - // Show loading state if the app hasn't loaded within 2 seconds, and show an error state if it hasn't loaded within 10 seconds. + // Show loading state if the app hasn't loaded within 1 second, and show an error state if it hasn't loaded within 10 seconds. setTimeout(() => { if (window.hasLoaded || window.hasErrored) { return; } showGlobalLoadingState(); - }, 2000); + }, 1000); setTimeout(() => { if (window.hasLoaded || window.hasErrored) { -- 2.51.2 From f9f342d18322cc6b07e0a59734875c2973307076 Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Thu, 13 Aug 2026 18:51:45 -0500 Subject: [PATCH 02/19] Update plugin input styles --- package.json | 2 +- src/css/style.css | 40 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 52552395..87fd2c8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.179", + "version": "0.18.180", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/css/style.css b/src/css/style.css index e2b6c029..79b97ee3 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -10751,17 +10751,49 @@ h2.setting-item-name { } .setting-item-dropdown, -.plugin-content textarea { +.setting-item-text-input, +.setting-item-textarea, +.plugin-content textarea, +.plugin-content + :is( + input:not([type]), + input[type="text"], + input[type="search"], + input[type="email"], + input[type="url"], + input[type="tel"], + input[type="password"], + input[type="number"] + ) { font-size: 16px; - padding: 8px 10px; + font-family: inherit; + padding: 10px 12px; border-radius: var(--text-input-border-radius); - border: var(--hair) solid var(--generic-border-color); + border: var(--hair) solid var(--post-border-color); background: var(--text-input-background-color); color: var(--text-color); } +.setting-item-text-input:focus, +.setting-item-textarea:focus, +.plugin-content textarea:focus, +.plugin-content + :is( + input:not([type]), + input[type="text"], + input[type="search"], + input[type="email"], + input[type="url"], + input[type="tel"], + input[type="password"], + input[type="number"] + ):focus { + outline: 2px solid var(--highlight-color); + outline-offset: -1px; +} + +.setting-item-textarea, .plugin-content textarea { - font-family: inherit; resize: vertical; } -- 2.51.2 From 9d59916d3f8e513a4da4e890a22f14a05aaf8705 Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Thu, 13 Aug 2026 23:41:53 -0500 Subject: [PATCH 03/19] Update label styles --- package.json | 2 +- src/css/style.css | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 87fd2c8c..3da236ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.180", + "version": "0.18.181", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/css/style.css b/src/css/style.css index 79b97ee3..8683cc89 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1864,6 +1864,8 @@ container-link { color: var(--text-color-muted); width: fit-content; border-radius: var(--label-badge-border-radius); + border: none; + background-color: inherit; } .label-badge-image { -- 2.51.2 From 7fcee921965c4c71ae962496cf77d53eadff10ae Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Thu, 13 Aug 2026 23:47:01 -0500 Subject: [PATCH 04/19] Remove home view opacity hack --- package.json | 2 +- src/js/views/home.view.js | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/package.json b/package.json index 3da236ad..949411a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.181", + "version": "0.18.182", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/js/views/home.view.js b/src/js/views/home.view.js index 42e9f4e7..1cf4bade 100644 --- a/src/js/views/home.view.js +++ b/src/js/views/home.view.js @@ -416,13 +416,7 @@ class HomeView extends View { root.addEventListener("page-restore", (e) => { const scrollY = e.detail?.scrollY ?? 0; - root.style.opacity = "0"; window.scrollTo(0, scrollY); - requestAnimationFrame(() => { - requestAnimationFrame(() => { - root.style.opacity = "1"; - }); - }); for (const observer of postSeenObservers.values()) { observer.connect(); } -- 2.51.2 From dc732d6e450ca32a4b5b48346e02622133250a85 Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Fri, 14 Aug 2026 00:16:41 -0500 Subject: [PATCH 05/19] Update icons --- package.json | 2 +- src/css/style.css | 4 ++-- src/img/icons/custom/inbox.svg | 2 +- src/img/icons/custom/message-plus-line.svg | 2 +- src/img/icons/custom/reply.svg | 2 +- src/img/icons/custom/repost.svg | 2 +- src/js/templates/icons/inboxIcon.template.js | 16 ++++++++++++---- .../templates/icons/messagePlusIcon.template.js | 16 ++++++++++++---- src/js/templates/icons/replyIcon.template.js | 9 +++++++-- src/js/templates/icons/repostIcon.template.js | 16 ++++++++++++++-- 10 files changed, 52 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 949411a4..b582033e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.182", + "version": "0.18.183", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/css/style.css b/src/css/style.css index 8683cc89..870ec5f8 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1950,7 +1950,7 @@ container-link { } .small-post .repost-label .icon path { - fill: var(--text-color-muted); + stroke: var(--text-color-muted); } .small-post .missing-post-indicator { @@ -3346,7 +3346,7 @@ image-carousel { } .notification-icon .repost-icon path { - fill: var(--highlight-color); + stroke: var(--highlight-color); } .notification-icon.verified-icon svg { diff --git a/src/img/icons/custom/inbox.svg b/src/img/icons/custom/inbox.svg index 888a6bdc..0b8665d3 100644 --- a/src/img/icons/custom/inbox.svg +++ b/src/img/icons/custom/inbox.svg @@ -1 +1 @@ - + diff --git a/src/img/icons/custom/message-plus-line.svg b/src/img/icons/custom/message-plus-line.svg index d8e4c8f1..69134f19 100644 --- a/src/img/icons/custom/message-plus-line.svg +++ b/src/img/icons/custom/message-plus-line.svg @@ -1 +1 @@ - + diff --git a/src/img/icons/custom/reply.svg b/src/img/icons/custom/reply.svg index d5a509aa..a8a3b16d 100644 --- a/src/img/icons/custom/reply.svg +++ b/src/img/icons/custom/reply.svg @@ -1 +1 @@ - + diff --git a/src/img/icons/custom/repost.svg b/src/img/icons/custom/repost.svg index f3620b1d..e759385d 100644 --- a/src/img/icons/custom/repost.svg +++ b/src/img/icons/custom/repost.svg @@ -1 +1 @@ - + diff --git a/src/js/templates/icons/inboxIcon.template.js b/src/js/templates/icons/inboxIcon.template.js index b9adaef8..2a7c12f7 100644 --- a/src/js/templates/icons/inboxIcon.template.js +++ b/src/js/templates/icons/inboxIcon.template.js @@ -11,10 +11,18 @@ export function inboxIconTemplate() { fill="none" > + `; diff --git a/src/js/templates/icons/messagePlusIcon.template.js b/src/js/templates/icons/messagePlusIcon.template.js index 9d8c9eea..97e939ac 100644 --- a/src/js/templates/icons/messagePlusIcon.template.js +++ b/src/js/templates/icons/messagePlusIcon.template.js @@ -11,10 +11,18 @@ export function messagePlusIconTemplate() { fill="none" > + `; diff --git a/src/js/templates/icons/replyIcon.template.js b/src/js/templates/icons/replyIcon.template.js index 5ab7cba5..397ecee4 100644 --- a/src/js/templates/icons/replyIcon.template.js +++ b/src/js/templates/icons/replyIcon.template.js @@ -1,16 +1,21 @@ import { html } from "/js/lib/lit-html.js"; +// Source: src/img/icons/custom/reply.svg export function replyIconTemplate() { return html` `; } diff --git a/src/js/templates/icons/repostIcon.template.js b/src/js/templates/icons/repostIcon.template.js index bd5fe3a2..97e98f0c 100644 --- a/src/js/templates/icons/repostIcon.template.js +++ b/src/js/templates/icons/repostIcon.template.js @@ -1,16 +1,28 @@ import { html } from "/js/lib/lit-html.js"; +// Source: src/img/icons/custom/repost.svg export function repostIconTemplate() { return html` + `; } -- 2.51.2 From c77316365c563c83aa38aa5ed58e82261b2c5e7c Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Fri, 14 Aug 2026 00:24:40 -0500 Subject: [PATCH 06/19] Share scroll restore logic --- package.json | 2 +- src/js/app.js | 8 +- src/js/router.js | 14 ++ src/js/views/bookmarks.view.js | 10 +- src/js/views/chat.view.js | 10 +- src/js/views/chatRequests.view.js | 10 +- src/js/views/communityPluginListing.view.js | 4 - src/js/views/communityPlugins.view.js | 1 - src/js/views/feedDetail.view.js | 10 +- src/js/views/feeds.view.js | 10 +- src/js/views/groupChatDetails.view.js | 12 +- src/js/views/hashtag.view.js | 5 - src/js/views/home.view.js | 4 +- src/js/views/installedPlugins.view.js | 12 -- src/js/views/listDetail.view.js | 10 +- src/js/views/lists.view.js | 10 +- src/js/views/notifications.view.js | 1 - src/js/views/pluginPage.view.js | 4 - src/js/views/pluginSettings.view.js | 4 - src/js/views/postLikes.view.js | 12 +- src/js/views/postQuotes.view.js | 12 +- src/js/views/postReposts.view.js | 12 +- src/js/views/profile.view.js | 9 -- src/js/views/profileFollowers.view.js | 12 +- src/js/views/profileFollowing.view.js | 12 +- src/js/views/profileKnownFollowers.view.js | 12 +- src/js/views/settings.view.js | 4 - src/js/views/settings/advanced.view.js | 4 - src/js/views/settings/appearance.view.js | 4 - src/js/views/settings/blockedAccounts.view.js | 4 - src/js/views/settings/mutedAccounts.view.js | 4 - src/js/views/settings/mutedWords.view.js | 4 - src/js/views/settings/notifications.view.js | 4 - tests/e2e/specs/concerns/scroll.test.js | 60 ++++++++ tests/unit/specs/router.test.js | 133 +++++++++++++++--- 35 files changed, 223 insertions(+), 220 deletions(-) diff --git a/package.json b/package.json index b582033e..cc75a890 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.183", + "version": "0.18.184", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/js/app.js b/src/js/app.js index 9adceb0d..28d79496 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -255,10 +255,12 @@ export async function main() { router.addRoute(["/", "/intent/compose"], () => homeView, { layoutOptions: { activeNavItem: "home" }, + scrollRestore: "always", }); router.addRoute("/login", () => loginView, { layout: false }); router.addRoute("/notifications", () => notificationsView, { layoutOptions: { activeNavItem: "notifications" }, + scrollRestore: "always", }); router.addRoute("/messages/inbox", () => chatRequestsView, { layoutOptions: { activeNavItem: "chat" }, @@ -268,6 +270,7 @@ export async function main() { }); router.addRoute("/messages/:convoId", () => chatDetailView, { layoutOptions: { activeNavItem: "chat" }, + scrollRestore: "manual", }); router.addRoute("/messages", () => chatView, { layoutOptions: { activeNavItem: "chat" }, @@ -283,6 +286,7 @@ export async function main() { }); router.addRoute("/search", () => searchView, { layoutOptions: { activeNavItem: "search" }, + scrollRestore: "manual", }); router.addRoute("/hashtag/:tag", () => hashtagView); router.addRoute("/profile/:handleOrDid/feed/:rkey", () => feedDetailView); @@ -299,7 +303,9 @@ export async function main() { "/profile/:handleOrDid/post/:rkey/reposts", () => postRepostsView, ); - router.addRoute("/profile/:handleOrDid/post/:rkey", () => postThreadView); + router.addRoute("/profile/:handleOrDid/post/:rkey", () => postThreadView, { + scrollRestore: "manual", + }); router.addRoute( "/profile/:handleOrDid/known-followers", () => profileKnownFollowersView, diff --git a/src/js/router.js b/src/js/router.js index 08250ced..9f273eeb 100644 --- a/src/js/router.js +++ b/src/js/router.js @@ -272,6 +272,20 @@ export class Router extends EventEmitter { const scrollY = this.scrollStates.get(path) ?? 0; this.currentPage.classList.remove("page-hidden"); this.currentPage.classList.add("page-visible"); + // Scroll before dispatching so a "manual" view's own scroll wins + const scrollRestore = routeInfo.options.scrollRestore ?? "back"; + switch (scrollRestore) { + case "always": + window.scrollTo(0, scrollY); + break; + case "back": + window.scrollTo(0, isBack ? scrollY : 0); + break; + case "manual": + break; + default: + console.warn(`unknown scrollRestore type: ${scrollRestore}`); + } this.currentPage.dispatchEvent( new CustomEvent("page-restore", { detail: { diff --git a/src/js/views/bookmarks.view.js b/src/js/views/bookmarks.view.js index 81a3db6a..03f4e102 100644 --- a/src/js/views/bookmarks.view.js +++ b/src/js/views/bookmarks.view.js @@ -66,14 +66,8 @@ class BookmarksView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await loadBookmarks({ reload: true }); - } + if (e.detail?.isBack) return; + await loadBookmarks({ reload: true }); }); } } diff --git a/src/js/views/chat.view.js b/src/js/views/chat.view.js index 6aef6ced..0f218525 100644 --- a/src/js/views/chat.view.js +++ b/src/js/views/chat.view.js @@ -283,14 +283,8 @@ class ChatView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await loadConvoList({ reload: true }); - } + if (e.detail?.isBack) return; + await loadConvoList({ reload: true }); }); } } diff --git a/src/js/views/chatRequests.view.js b/src/js/views/chatRequests.view.js index 7fc4530c..631c35f5 100644 --- a/src/js/views/chatRequests.view.js +++ b/src/js/views/chatRequests.view.js @@ -261,14 +261,8 @@ class ChatRequestsView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadConvoRequestList({ reload: true }); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadConvoRequestList({ reload: true }); }); } } diff --git a/src/js/views/communityPluginListing.view.js b/src/js/views/communityPluginListing.view.js index c76ff5ff..e167eaa6 100644 --- a/src/js/views/communityPluginListing.view.js +++ b/src/js/views/communityPluginListing.view.js @@ -285,10 +285,6 @@ class CommunityPluginListingView extends View { await loadListings(); await loadDetails(); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/communityPlugins.view.js b/src/js/views/communityPlugins.view.js index 9f61c77b..239d9dd3 100644 --- a/src/js/views/communityPlugins.view.js +++ b/src/js/views/communityPlugins.view.js @@ -152,7 +152,6 @@ class CommunityPluginsView extends View { }); root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); loadListings(); }); } diff --git a/src/js/views/feedDetail.view.js b/src/js/views/feedDetail.view.js index eb02ec45..5cd446e8 100644 --- a/src/js/views/feedDetail.view.js +++ b/src/js/views/feedDetail.view.js @@ -145,14 +145,8 @@ class FeedDetailView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await loadFeed({ reload: true }); - } + if (e.detail?.isBack) return; + await loadFeed({ reload: true }); }); } } diff --git a/src/js/views/feeds.view.js b/src/js/views/feeds.view.js index 9ce672d6..460b46a6 100644 --- a/src/js/views/feeds.view.js +++ b/src/js/views/feeds.view.js @@ -303,14 +303,8 @@ class FeedsView extends View { root.addEventListener("page-restore", async (e) => { resetEditingState(); - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadPinnedItems(); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadPinnedItems(); }); } } diff --git a/src/js/views/groupChatDetails.view.js b/src/js/views/groupChatDetails.view.js index e424fee7..adec59be 100644 --- a/src/js/views/groupChatDetails.view.js +++ b/src/js/views/groupChatDetails.view.js @@ -340,16 +340,8 @@ class GroupChatDetailsView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await loadConvoDetails({ reload: true }); - } + if (e.detail?.isBack) return; + await loadConvoDetails({ reload: true }); }); } } diff --git a/src/js/views/hashtag.view.js b/src/js/views/hashtag.view.js index bde96bcd..20ddada4 100644 --- a/src/js/views/hashtag.view.js +++ b/src/js/views/hashtag.view.js @@ -119,11 +119,6 @@ class HashtagView extends View { root.addEventListener("page-enter", async () => { await loadCurrentFeed(); }); - - root.addEventListener("page-restore", (e) => { - const scrollY = e.detail?.scrollY ?? 0; - window.scrollTo(0, scrollY); - }); } } diff --git a/src/js/views/home.view.js b/src/js/views/home.view.js index 1cf4bade..fd933638 100644 --- a/src/js/views/home.view.js +++ b/src/js/views/home.view.js @@ -414,9 +414,7 @@ class HomeView extends View { await loadCurrentFeed(); }); - root.addEventListener("page-restore", (e) => { - const scrollY = e.detail?.scrollY ?? 0; - window.scrollTo(0, scrollY); + root.addEventListener("page-restore", () => { for (const observer of postSeenObservers.values()) { observer.connect(); } diff --git a/src/js/views/installedPlugins.view.js b/src/js/views/installedPlugins.view.js index d5f572b1..993758b8 100644 --- a/src/js/views/installedPlugins.view.js +++ b/src/js/views/installedPlugins.view.js @@ -344,18 +344,6 @@ class InstalledPluginsView extends View { root, ); }); - - root.addEventListener("page-restore", (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - } - }); } } diff --git a/src/js/views/listDetail.view.js b/src/js/views/listDetail.view.js index 474d0dfd..23c1a1fa 100644 --- a/src/js/views/listDetail.view.js +++ b/src/js/views/listDetail.view.js @@ -431,14 +431,8 @@ class ListDetailView extends View { root.addEventListener("page-restore", async (e) => { userHasScrolled = false; - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await loadListAndFeeds({ reload: true }); - } + if (e.detail?.isBack) return; + await loadListAndFeeds({ reload: true }); }); } } diff --git a/src/js/views/lists.view.js b/src/js/views/lists.view.js index 22d9a000..479c8ee5 100644 --- a/src/js/views/lists.view.js +++ b/src/js/views/lists.view.js @@ -92,14 +92,8 @@ class ListsView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - await loadLists({ reload: true }); - } + if (e.detail?.isBack) return; + await loadLists({ reload: true }); }); } } diff --git a/src/js/views/notifications.view.js b/src/js/views/notifications.view.js index ea657d15..9bc506b5 100644 --- a/src/js/views/notifications.view.js +++ b/src/js/views/notifications.view.js @@ -841,7 +841,6 @@ class NotificationsView extends View { root.addEventListener("page-restore", async (e) => { const scrollY = e.detail?.scrollY ?? 0; - window.scrollTo(0, scrollY); if (scrollY <= 200) { await loadNotifications({ reload: true }); } diff --git a/src/js/views/pluginPage.view.js b/src/js/views/pluginPage.view.js index 9ff70f50..4b327c57 100644 --- a/src/js/views/pluginPage.view.js +++ b/src/js/views/pluginPage.view.js @@ -100,10 +100,6 @@ class PluginPageView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/pluginSettings.view.js b/src/js/views/pluginSettings.view.js index fd7502b0..b87169b7 100644 --- a/src/js/views/pluginSettings.view.js +++ b/src/js/views/pluginSettings.view.js @@ -99,10 +99,6 @@ class PluginSettingsView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/postLikes.view.js b/src/js/views/postLikes.view.js index 07b5ce07..923e8e0a 100644 --- a/src/js/views/postLikes.view.js +++ b/src/js/views/postLikes.view.js @@ -100,16 +100,8 @@ class PostLikesView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadPostLikes(postUri); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadPostLikes(postUri); }); } } diff --git a/src/js/views/postQuotes.view.js b/src/js/views/postQuotes.view.js index 8ab37509..f2d9268c 100644 --- a/src/js/views/postQuotes.view.js +++ b/src/js/views/postQuotes.view.js @@ -103,16 +103,8 @@ class PostQuotesView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadPostQuotes(postUri); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadPostQuotes(postUri); }); } } diff --git a/src/js/views/postReposts.view.js b/src/js/views/postReposts.view.js index 2320b86b..2ecfd88f 100644 --- a/src/js/views/postReposts.view.js +++ b/src/js/views/postReposts.view.js @@ -99,16 +99,8 @@ class PostRepostsView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadPostReposts(postUri); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadPostReposts(postUri); }); } } diff --git a/src/js/views/profile.view.js b/src/js/views/profile.view.js index 5d8c7b1b..ed329c12 100644 --- a/src/js/views/profile.view.js +++ b/src/js/views/profile.view.js @@ -581,15 +581,6 @@ class ProfileView extends View { dataLayer.requests.loadProfileChatStatus(profile.did); } }); - - root.addEventListener("page-restore", (e) => { - const { isBack, scrollY } = e.detail; - if (isBack) { - window.scrollTo(0, scrollY); - } else { - window.scrollTo(0, 0); - } - }); } } diff --git a/src/js/views/profileFollowers.view.js b/src/js/views/profileFollowers.view.js index 89cfaaad..76430194 100644 --- a/src/js/views/profileFollowers.view.js +++ b/src/js/views/profileFollowers.view.js @@ -110,16 +110,8 @@ class ProfileFollowersView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadProfileFollowers(profileDid); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadProfileFollowers(profileDid); }); } } diff --git a/src/js/views/profileFollowing.view.js b/src/js/views/profileFollowing.view.js index ad0bb90b..368f05ef 100644 --- a/src/js/views/profileFollowing.view.js +++ b/src/js/views/profileFollowing.view.js @@ -108,16 +108,8 @@ class ProfileFollowingView extends View { }); root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; - const isBack = e.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadProfileFollows(profileDid); - } + if (e.detail?.isBack) return; + await dataLayer.requests.loadProfileFollows(profileDid); }); } } diff --git a/src/js/views/profileKnownFollowers.view.js b/src/js/views/profileKnownFollowers.view.js index 52848b11..1ddcd33f 100644 --- a/src/js/views/profileKnownFollowers.view.js +++ b/src/js/views/profileKnownFollowers.view.js @@ -96,16 +96,8 @@ class ProfileKnownFollowersView extends View { }); root.addEventListener("page-restore", async (event) => { - const scrollY = event.detail?.scrollY ?? 0; - const isBack = event.detail?.isBack ?? false; - if (isBack) { - if (scrollY > 0) { - window.scrollTo(0, scrollY); - } - } else { - window.scrollTo(0, 0); - await dataLayer.requests.loadKnownFollowers(profileDid); - } + if (event.detail?.isBack) return; + await dataLayer.requests.loadKnownFollowers(profileDid); }); } } diff --git a/src/js/views/settings.view.js b/src/js/views/settings.view.js index b8291fa0..a2792b79 100644 --- a/src/js/views/settings.view.js +++ b/src/js/views/settings.view.js @@ -396,10 +396,6 @@ class SettingsView extends View { loadOtherAccounts(); }); - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); - // Account actions navigate away with the pending spinner showing; if the // user comes back via the back/forward cache the document is restored // as-is, so reset the stuck pending state. diff --git a/src/js/views/settings/advanced.view.js b/src/js/views/settings/advanced.view.js index bc3c3007..2f0ac0b4 100644 --- a/src/js/views/settings/advanced.view.js +++ b/src/js/views/settings/advanced.view.js @@ -277,10 +277,6 @@ class SettingsAdvancedView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/settings/appearance.view.js b/src/js/views/settings/appearance.view.js index d22a0385..d2bbc1ce 100644 --- a/src/js/views/settings/appearance.view.js +++ b/src/js/views/settings/appearance.view.js @@ -147,10 +147,6 @@ class SettingsAppearanceView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/settings/blockedAccounts.view.js b/src/js/views/settings/blockedAccounts.view.js index 01737c2d..89c599d5 100644 --- a/src/js/views/settings/blockedAccounts.view.js +++ b/src/js/views/settings/blockedAccounts.view.js @@ -81,10 +81,6 @@ class SettingsBlockedAccountsView extends View { root.addEventListener("page-enter", async () => { await loadMore(); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/settings/mutedAccounts.view.js b/src/js/views/settings/mutedAccounts.view.js index 708f8dce..f469709b 100644 --- a/src/js/views/settings/mutedAccounts.view.js +++ b/src/js/views/settings/mutedAccounts.view.js @@ -78,10 +78,6 @@ class SettingsMutedAccountsView extends View { root.addEventListener("page-enter", async () => { await loadMore(); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/settings/mutedWords.view.js b/src/js/views/settings/mutedWords.view.js index f3ef7c0a..c571705b 100644 --- a/src/js/views/settings/mutedWords.view.js +++ b/src/js/views/settings/mutedWords.view.js @@ -358,10 +358,6 @@ class SettingsMutedWordsView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/src/js/views/settings/notifications.view.js b/src/js/views/settings/notifications.view.js index 88799b20..6d560de5 100644 --- a/src/js/views/settings/notifications.view.js +++ b/src/js/views/settings/notifications.view.js @@ -103,10 +103,6 @@ class SettingsNotificationsView extends View { root, ); }); - - root.addEventListener("page-restore", () => { - window.scrollTo(0, 0); - }); } } diff --git a/tests/e2e/specs/concerns/scroll.test.js b/tests/e2e/specs/concerns/scroll.test.js index cf5e17b2..a99add73 100644 --- a/tests/e2e/specs/concerns/scroll.test.js +++ b/tests/e2e/specs/concerns/scroll.test.js @@ -55,4 +55,64 @@ test.describe("Scroll position restoration", () => { // Verify the post we scrolled to is still visible (scroll position restored) await expect(targetPost).toBeVisible({ timeout: 10000 }); }); + + test.describe("on a settings subpage", () => { + test.use({ viewport: { width: 375, height: 667 } }); + + test("should restore on back and reset to the top on a forward visit", async ({ + page, + }) => { + const mockServer = new MockServer(); + mockServer.mutedWords = Array.from({ length: 40 }, (_, i) => ({ + value: `mutedword${i + 1}`, + targets: ["content"], + })); + await mockServer.setup(page); + + await login(page); + await page.goto("/settings"); + + const openMutedWords = () => + page.locator('[data-testid="settings-nav-muted-words"]').click(); + const view = page.locator("#settings-muted-words-view"); + + await openMutedWords(); + await expect(view.locator('[data-testid="muted-word-list"]')).toBeVisible( + { + timeout: 10000, + }, + ); + + await view + .locator('[data-testid="muted-word-item"]') + .last() + .scrollIntoViewIfNeeded(); + const scrollY = await page.evaluate(() => window.scrollY); + expect(scrollY).toBeGreaterThan(0); + + await page.goBack(); + await expect(page.locator("#settings-view")).toBeVisible({ + timeout: 10000, + }); + + // Forward navigation to the cached page starts at the top + await openMutedWords(); + await expect(view).toBeVisible({ timeout: 10000 }); + await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0); + + // ...and the position saved for that visit is restored on back + await view + .locator('[data-testid="muted-word-item"]') + .last() + .scrollIntoViewIfNeeded(); + await page.goBack(); + await expect(page.locator("#settings-view")).toBeVisible({ + timeout: 10000, + }); + await page.goForward(); + await expect + .poll(() => page.evaluate(() => window.scrollY)) + .toBe(scrollY); + }); + }); }); diff --git a/tests/unit/specs/router.test.js b/tests/unit/specs/router.test.js index cb417aa6..92af5eaa 100644 --- a/tests/unit/specs/router.test.js +++ b/tests/unit/specs/router.test.js @@ -28,6 +28,27 @@ function mountRouter(router, { layout = null } = {}) { }; } +// JSDOM's window.scrollY is a read-only getter, so temporarily override it to +// simulate the page being scrolled before we navigate away. +function withScrollY(value, callback) { + const original = Object.getOwnPropertyDescriptor(window, "scrollY"); + Object.defineProperty(window, "scrollY", { + value, + configurable: true, + }); + return (async () => { + try { + return await callback(); + } finally { + if (original) { + Object.defineProperty(window, "scrollY", original); + } else { + delete window.scrollY; + } + } + })(); +} + describe("constructor and initialization", () => { it("should initialize with empty routes", () => { const router = new Router(); @@ -1088,27 +1109,6 @@ describe("page cache", () => { }); describe("scroll position persistence", () => { - // JSDOM's window.scrollY is a read-only getter, so temporarily override it to - // simulate the page being scrolled before we navigate away. - function withScrollY(value, callback) { - const original = Object.getOwnPropertyDescriptor(window, "scrollY"); - Object.defineProperty(window, "scrollY", { - value, - configurable: true, - }); - return (async () => { - try { - return await callback(); - } finally { - if (original) { - Object.defineProperty(window, "scrollY", original); - } else { - delete window.scrollY; - } - } - })(); - } - function createRouter() { const router = new Router(); mountRouter(router); @@ -1168,3 +1168,94 @@ describe("scroll position persistence", () => { assert.deepEqual(restoredScrollY, 175); }); }); + +describe("scrollRestore", () => { + let scrollTo; + + beforeEach(() => { + scrollTo = mock.method(window, "scrollTo", () => {}); + }); + + afterEach(() => { + scrollTo.mock.restore(); + }); + + function scrollCalls() { + return scrollTo.mock.calls.map((call) => call.arguments); + } + + function createRouter(scrollRestore) { + const router = new Router(); + mountRouter(router); + router.addRoute( + "/a", + () => Promise.resolve({}), + scrollRestore ? { scrollRestore } : {}, + ); + router.addRoute("/b", () => Promise.resolve({})); + router.renderRoute(() => {}); + return router; + } + + // Returns to /a after leaving it scrolled to 175 + async function revisit(router, { isBack }) { + await router.load("/a"); + await withScrollY(175, () => router.load("/b")); + scrollTo.mock.resetCalls(); + await router.load("/a", { isBack }); + } + + it("restores the saved position on back navigation by default", async () => { + const router = createRouter(); + + await revisit(router, { isBack: true }); + + assert.deepEqual(scrollCalls(), [[0, 175]]); + }); + + it("scrolls to the top on forward navigation by default", async () => { + const router = createRouter(); + + await revisit(router, { isBack: false }); + + assert.deepEqual(scrollCalls(), [[0, 0]]); + }); + + it("restores the saved position on forward navigation when set to always", async () => { + const router = createRouter("always"); + + await revisit(router, { isBack: false }); + + assert.deepEqual(scrollCalls(), [[0, 175]]); + }); + + it("still restores the saved position on back navigation when set to always", async () => { + const router = createRouter("always"); + + await revisit(router, { isBack: true }); + + assert.deepEqual(scrollCalls(), [[0, 175]]); + }); + + it("does not touch the scroll position when set to manual", async () => { + const router = createRouter("manual"); + + await revisit(router, { isBack: false }); + await revisit(router, { isBack: true }); + + assert.deepEqual(scrollCalls(), []); + }); + + it("scrolls before dispatching page-restore so a view can override it", async () => { + const router = createRouter(); + await router.load("/a"); + let scrolledBeforeDispatch = null; + router.pages.get("/a").el.addEventListener("page-restore", () => { + scrolledBeforeDispatch = scrollCalls(); + }); + + await revisit(router, { isBack: true }); + + assert.deepEqual(scrolledBeforeDispatch, [[0, 175]]); + }); +}); -- 2.51.2 From 1020db31977c75eb3149734b6a1d7c9724ab2237 Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Fri, 14 Aug 2026 00:57:22 -0500 Subject: [PATCH 07/19] Update avatar styles --- package.json | 2 +- src/css/style.css | 29 +++++++------ src/js/components/account-switcher-dialog.js | 4 +- src/js/templates/avatarGroup.template.js | 4 +- src/js/templates/footer.template.js | 2 +- src/js/views/chatDetail.view.js | 1 - .../templates/avatarGroup.template.test.js | 43 +++++++++++++++++++ 7 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 tests/unit/specs/templates/avatarGroup.template.test.js diff --git a/package.json b/package.json index cc75a890..e8628ecc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.184", + "version": "0.18.185", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/css/style.css b/src/css/style.css index 870ec5f8..2febc4ff 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -874,7 +874,8 @@ input[type="color"]::-moz-color-swatch { width: var(--avatar-size-post); height: var(--avatar-size-post); border-radius: var(--avatar-border-radius); - background-color: var(--generic-border-color); + border: var(--hair) solid var(--post-border-color); + background-color: var(--skeleton-color); } header { @@ -1216,12 +1217,6 @@ tab-bar:not([full-width]) { border: var(--hair) solid var(--post-border-color); } -.sidebar-profile-avatar .avatar-placeholder { - border-radius: var(--avatar-border-radius); - background-color: var(--skeleton-color); - border: var(--hair) solid var(--post-border-color); -} - .sidebar-profile-info { margin-bottom: 12px; } @@ -1556,12 +1551,9 @@ button.sidebar-plugin-nav-item { height: var(--avatar-size-footer-nav); } -.footer-nav-item .avatar-image-placeholder { +.footer-nav-item .avatar-placeholder { width: var(--avatar-size-footer-nav); height: var(--avatar-size-footer-nav); - border-radius: var(--avatar-border-radius); - background-color: var(--skeleton-color); - border: var(--hair) solid var(--post-border-color); } .footer-nav-item[disabled] { @@ -3568,7 +3560,8 @@ image-carousel { flex-shrink: 0; } -.convo-avatar .avatar-image-frame { +.convo-avatar .avatar-image-frame, +.convo-avatar .avatar-placeholder { width: var(--avatar-size-convo); height: var(--avatar-size-convo); } @@ -3579,10 +3572,16 @@ image-carousel { height: var(--avatar-size-convo); } +.avatar-group > .avatar-placeholder { + width: 100%; + height: 100%; +} + .avatar-group-item { position: absolute; } +.avatar-group-item .avatar, .avatar-group-item .avatar-image-frame { display: block; width: 100%; @@ -4122,7 +4121,8 @@ image-carousel { align-self: flex-start; } -.message-avatar .avatar-image-frame { +.message-avatar .avatar-image-frame, +.message-avatar .avatar-placeholder { width: var(--avatar-size-message); height: var(--avatar-size-message); } @@ -5071,6 +5071,7 @@ chat-input .rich-text-input-placeholder { .reaction-row-avatar .avatar, .reaction-row-avatar .avatar-image-frame, +.reaction-row-avatar .avatar-placeholder, .reaction-row-avatar .avatar-image { width: 42px; height: 42px; @@ -8486,7 +8487,7 @@ context-menu-item-group > context-menu-item > button { } .account-switcher-item .avatar, -.account-switcher-item .avatar-image-placeholder, +.account-switcher-item .avatar-placeholder, .account-switcher-item .skeleton-avatar { width: 40px; height: 40px; diff --git a/src/js/components/account-switcher-dialog.js b/src/js/components/account-switcher-dialog.js index 5042448c..9da2ba3f 100644 --- a/src/js/components/account-switcher-dialog.js +++ b/src/js/components/account-switcher-dialog.js @@ -181,9 +181,7 @@ class AccountSwitcherDialog extends Component { author: profile, clickAction: "none", }) - : html`
`} + : html`
`}