From 92fbcfb17c92870aa1f0f7d0714b04690f19404f Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Sat, 15 Aug 2026 23:06:27 -0500 Subject: [PATCH] Refactor view lifecycle --- package.json | 2 +- src/js/router.js | 29 ++++---- src/js/views/bookmarks.view.js | 12 +++- src/js/views/chat.view.js | 12 +++- src/js/views/chatDetail.view.js | 32 +++++---- src/js/views/chatRequests.view.js | 7 +- src/js/views/communityPluginListing.view.js | 5 +- src/js/views/communityPlugins.view.js | 7 +- src/js/views/feedDetail.view.js | 7 +- src/js/views/feeds.view.js | 16 +++-- src/js/views/groupChatDetails.view.js | 7 +- src/js/views/hashtag.view.js | 7 +- src/js/views/home.view.js | 18 +++-- src/js/views/listDetail.view.js | 14 +++- src/js/views/lists.view.js | 12 +++- src/js/views/login.view.js | 10 ++- src/js/views/notifications.view.js | 14 ++-- src/js/views/postLikes.view.js | 7 +- src/js/views/postQuotes.view.js | 7 +- src/js/views/postReposts.view.js | 7 +- src/js/views/postThread.view.js | 29 ++++---- src/js/views/profile.view.js | 6 +- src/js/views/profileFollowers.view.js | 7 +- src/js/views/profileFollowing.view.js | 7 +- src/js/views/profileKnownFollowers.view.js | 7 +- src/js/views/search.view.js | 38 ++++++----- src/js/views/settings.view.js | 12 +++- src/js/views/settings/blockedAccounts.view.js | 7 +- src/js/views/settings/mutedAccounts.view.js | 7 +- tests/unit/specs/router.test.js | 67 +++++++------------ 30 files changed, 262 insertions(+), 157 deletions(-) diff --git a/package.json b/package.json index b01150b5..dc8794f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.196", + "version": "0.18.197", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/js/router.js b/src/js/router.js index 397aba7f..0f7f0d21 100644 --- a/src/js/router.js +++ b/src/js/router.js @@ -27,7 +27,17 @@ function bindMiddleClickRedispatch() { // the route cache) and detach when it's swapped out. function bindActive(root, attach, detach) { root.addEventListener("page-show", attach); - root.addEventListener("page-exit", detach); + root.addEventListener("page-hide", detach); +} + +// Lifecycle helpers + +export function onPageShow(root, handler) { + root.addEventListener("page-show", (event) => handler(event.detail)); +} + +export function onPageHide(root, handler) { + root.addEventListener("page-hide", (event) => handler(event.detail)); } export function bindToPage(root, source, event, handler) { @@ -265,7 +275,7 @@ export class Router extends EventEmitter { if (activeElement && outgoingPage.contains(activeElement)) { activeElement.blur(); } - outgoingPage.dispatchEvent(new CustomEvent("page-exit")); + outgoingPage.dispatchEvent(new CustomEvent("page-hide")); } if (this.pages.has(path)) { // Return to existing page @@ -299,13 +309,10 @@ export class Router extends EventEmitter { await raf(); if (this.currentPage !== page || this.currentPath !== path) return; this.currentPage.dispatchEvent( - new CustomEvent(isRestore ? "page-restore" : "page-enter", { - detail: { scrollY }, + new CustomEvent("page-show", { + detail: { scrollY, action: isRestore ? "restore" : "advance" }, }), ); - this.currentPage.dispatchEvent( - new CustomEvent("page-show", { detail: { scrollY } }), - ); return; } // First load of new page @@ -334,12 +341,10 @@ export class Router extends EventEmitter { this.currentPage.classList.add("page-visible"); outgoingPage?.classList.remove("page-visible"); outgoingPage?.classList.add("page-hidden"); - this.currentPage.dispatchEvent(new CustomEvent("page-create")); this.currentPage.dispatchEvent( - new CustomEvent("page-enter", { detail: { scrollY: 0 } }), - ); - this.currentPage.dispatchEvent( - new CustomEvent("page-show", { detail: { scrollY: 0 } }), + new CustomEvent("page-show", { + detail: { scrollY: 0, action: "advance" }, + }), ); } diff --git a/src/js/views/bookmarks.view.js b/src/js/views/bookmarks.view.js index 677f4431..af46ca65 100644 --- a/src/js/views/bookmarks.view.js +++ b/src/js/views/bookmarks.view.js @@ -2,7 +2,12 @@ import { html, render } from "/js/lib/lit-html.js"; import { postFeedTemplate } from "/js/templates/postFeed.template.js"; import { auth } from "/js/auth.js"; import { headerTemplate } from "/js/templates/header.template.js"; -import { bindToPage, pageEffect, bindPageTitle } from "/js/router.js"; +import { + bindToPage, + pageEffect, + bindPageTitle, + onPageShow, +} from "/js/router.js"; import { BOOKMARKS_PAGE_SIZE } from "/js/config.js"; export default async function bookmarksView({ @@ -14,7 +19,10 @@ export default async function bookmarksView({ const { postInteractionHandler } = interactionHandlers; - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); bindToPage(root, layout, "active-nav-click", () => { loadPageData(); diff --git a/src/js/views/chat.view.js b/src/js/views/chat.view.js index 563cc941..ccf9aa81 100644 --- a/src/js/views/chat.view.js +++ b/src/js/views/chat.view.js @@ -1,4 +1,9 @@ -import { bindToPage, pageEffect, bindPageTitle } from "/js/router.js"; +import { + bindToPage, + pageEffect, + bindPageTitle, + onPageShow, +} from "/js/router.js"; import { html, render } from "/js/lib/lit-html.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { auth } from "/js/auth.js"; @@ -201,7 +206,10 @@ export default async function chatView({ await loadConvoList({ reload: true }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); bindToPage(root, layout, "active-nav-click", () => { loadPageData(); diff --git a/src/js/views/chatDetail.view.js b/src/js/views/chatDetail.view.js index 77a38a2d..a5217317 100644 --- a/src/js/views/chatDetail.view.js +++ b/src/js/views/chatDetail.view.js @@ -1,4 +1,9 @@ -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { + pageEffect, + bindPageTitle, + onPageShow, + onPageHide, +} from "/js/router.js"; import { html, render, ref } from "/js/lib/lit-html.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { richTextTemplate } from "/js/templates/richText.template.js"; @@ -1691,22 +1696,23 @@ export default async function chatDetailView({ root.addEventListener("click", handleRootClick); - // The fetcher runs whenever the page is visible, in both restore directions - root.addEventListener("page-show", () => { - messageFetcher.start(); - }); - - root.addEventListener("page-exit", () => { - messageFetcher.stop(); - }); - async function loadPageData() { await dataLayer.declarative.ensureConvo(convoId); await loadMessages({ reload: true }); } - root.addEventListener("page-enter", () => { - scrollToBottom(); - loadPageData(); + onPageShow(root, ({ action, scrollY }) => { + if (action === "restore") { + window.scrollTo(0, scrollY); + } else { + scrollToBottom(); + loadPageData(); + } + // The fetcher runs whenever the page is visible, in both restore directions + messageFetcher.start(); + }); + + onPageHide(root, () => { + messageFetcher.stop(); }); } diff --git a/src/js/views/chatRequests.view.js b/src/js/views/chatRequests.view.js index 8b7ff82d..6dcb9cc2 100644 --- a/src/js/views/chatRequests.view.js +++ b/src/js/views/chatRequests.view.js @@ -1,4 +1,4 @@ -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { html, render } from "/js/lib/lit-html.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { auth } from "/js/auth.js"; @@ -252,5 +252,8 @@ export default async function chatRequestsView({ dataLayer.requests.loadConvoRequestList({ reload: true }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/communityPluginListing.view.js b/src/js/views/communityPluginListing.view.js index 1b8620fd..6a99b022 100644 --- a/src/js/views/communityPluginListing.view.js +++ b/src/js/views/communityPluginListing.view.js @@ -1,5 +1,5 @@ import { html, render } from "/js/lib/lit-html.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { linkToLogin, getPermalinkForCommunityPlugin } from "/js/navigation.js"; import { showToast } from "/js/toasts.js"; @@ -268,7 +268,8 @@ export default async function communityPluginListingView({ ); }); - root.addEventListener("page-enter", async () => { + onPageShow(root, async ({ action }) => { + if (action === "restore") return; await loadListings(); await loadDetails(); }); diff --git a/src/js/views/communityPlugins.view.js b/src/js/views/communityPlugins.view.js index e737ebc7..cd7e730d 100644 --- a/src/js/views/communityPlugins.view.js +++ b/src/js/views/communityPlugins.view.js @@ -1,5 +1,5 @@ import { html, render } from "/js/lib/lit-html.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { chevronRightIconTemplate } from "/js/templates/icons/chevronRight.template.js"; import { globeIconTemplate } from "/js/templates/icons/globeIcon.template.js"; @@ -139,5 +139,8 @@ export default async function communityPluginsView({ loadListings(); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/feedDetail.view.js b/src/js/views/feedDetail.view.js index 47e7ff99..9b7fe538 100644 --- a/src/js/views/feedDetail.view.js +++ b/src/js/views/feedDetail.view.js @@ -5,7 +5,7 @@ import { auth } from "/js/auth.js"; import "/js/components/infinite-scroll-container.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { pinIconTemplate } from "/js/templates/icons/pinIcon.template.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { FEED_PAGE_SIZE } from "/js/config.js"; import { showToast } from "/js/toasts.js"; import "/js/components/context-menu.js"; @@ -140,5 +140,8 @@ export default async function feedDetailView({ await loadFeed({ reload: true }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/feeds.view.js b/src/js/views/feeds.view.js index afd98b75..ba9d0b93 100644 --- a/src/js/views/feeds.view.js +++ b/src/js/views/feeds.view.js @@ -1,4 +1,9 @@ -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { + pageEffect, + bindPageTitle, + onPageShow, + onPageHide, +} from "/js/router.js"; import { html, render } from "/js/lib/lit-html.js"; import { auth } from "/js/auth.js"; import { headerTemplate } from "/js/templates/header.template.js"; @@ -291,12 +296,15 @@ export default async function feedsView({ root, context: { dataLayer } }) { } // Reset whenever the page changes visibility, unlike the data load below - root.addEventListener("page-show", resetEditingState); - root.addEventListener("page-exit", resetEditingState); + onPageShow(root, resetEditingState); + onPageHide(root, resetEditingState); function loadPageData() { dataLayer.requests.loadPinnedItems(); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/groupChatDetails.view.js b/src/js/views/groupChatDetails.view.js index 21862b5e..54be7aa3 100644 --- a/src/js/views/groupChatDetails.view.js +++ b/src/js/views/groupChatDetails.view.js @@ -1,6 +1,6 @@ import { html, render } from "/js/lib/lit-html.js"; import { auth } from "/js/auth.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { avatarGroupTemplate } from "/js/templates/avatarGroup.template.js"; import { profileFeedTemplate } from "/js/templates/profileFeed.template.js"; @@ -326,5 +326,8 @@ export default async function groupChatDetailsView({ }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/hashtag.view.js b/src/js/views/hashtag.view.js index 7668f3a5..5cec5459 100644 --- a/src/js/views/hashtag.view.js +++ b/src/js/views/hashtag.view.js @@ -4,7 +4,7 @@ import { headerTemplate } from "/js/templates/header.template.js"; import { auth } from "/js/auth.js"; import "/js/components/tab-bar.js"; import { HASHTAG_FEED_PAGE_SIZE } from "/js/config.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { Signal, ReactiveStore } from "/js/signals.js"; export default async function hashtagView({ @@ -118,5 +118,8 @@ export default async function hashtagView({ loadCurrentFeed({ reload: true }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/home.view.js b/src/js/views/home.view.js index 97f254bc..08088889 100644 --- a/src/js/views/home.view.js +++ b/src/js/views/home.view.js @@ -11,7 +11,13 @@ import { FOLLOWING_FEED_URI, LOGGED_OUT_FEED_URI, } from "/js/config.js"; -import { bindToPage, pageEffect, bindPageTitle } from "/js/router.js"; +import { + bindToPage, + pageEffect, + bindPageTitle, + onPageShow, + onPageHide, +} from "/js/router.js"; import { showToast } from "/js/toasts.js"; import { Signal, ReactiveStore, SignalSet } from "/js/signals.js"; import { WelcomeModal } from "/js/modals/welcome.modal.js"; @@ -391,7 +397,7 @@ export default async function homeView({ }); } - root.addEventListener("page-create", async () => { + async function loadPageData() { const currentFeedUri = state.$currentFeedUri.get(); const pinnedItems = await dataLayer.declarative.ensurePinnedItems(); if (!pinnedItems.some((item) => item.uri === currentFeedUri)) { @@ -400,15 +406,17 @@ export default async function homeView({ preloadHiddenFeeds(pinnedItems); initializePostSeenObservers(pinnedItems); await loadCurrentFeed({ reload: true }); - }); + } + + loadPageData().catch((error) => console.error(error)); - root.addEventListener("page-show", () => { + onPageShow(root, () => { for (const observer of postSeenObservers.values()) { observer.connect(); } }); - root.addEventListener("page-exit", () => { + onPageHide(root, () => { for (const observer of postSeenObservers.values()) { observer.disconnect(); } diff --git a/src/js/views/listDetail.view.js b/src/js/views/listDetail.view.js index f097bb75..daf6f419 100644 --- a/src/js/views/listDetail.view.js +++ b/src/js/views/listDetail.view.js @@ -10,7 +10,12 @@ import "/js/components/tab-bar.js"; import { pinIconTemplate } from "/js/templates/icons/pinIcon.template.js"; import { userPlusIconTemplate } from "/js/templates/icons/userPlusIcon.template.js"; import { richTextTemplate } from "/js/templates/richText.template.js"; -import { bindToPage, pageEffect, bindPageTitle } from "/js/router.js"; +import { + bindToPage, + pageEffect, + bindPageTitle, + onPageShow, +} from "/js/router.js"; import { FEED_PAGE_SIZE } from "/js/config.js"; import { showToast } from "/js/toasts.js"; import "/js/components/infinite-scroll-container.js"; @@ -415,7 +420,10 @@ export default async function listDetailView({ const resetUserScrolled = () => { userHasScrolled = false; }; - root.addEventListener("page-show", resetUserScrolled); + onPageShow(root, resetUserScrolled); - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/lists.view.js b/src/js/views/lists.view.js index c9aa1c07..17e7e894 100644 --- a/src/js/views/lists.view.js +++ b/src/js/views/lists.view.js @@ -2,7 +2,12 @@ import { html, render } from "/js/lib/lit-html.js"; import { auth } from "/js/auth.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { listFeedTemplate } from "/js/templates/listFeed.template.js"; -import { bindToPage, pageEffect, bindPageTitle } from "/js/router.js"; +import { + bindToPage, + pageEffect, + bindPageTitle, + onPageShow, +} from "/js/router.js"; import { showToast } from "/js/toasts.js"; import { parseUri } from "/js/dataHelpers.js"; import "/js/components/create-list-dialog.js"; @@ -14,7 +19,10 @@ export default async function listsView({ }) { await auth.requireAuth(); - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); bindToPage(root, layout, "active-nav-click", () => { loadPageData(); diff --git a/src/js/views/login.view.js b/src/js/views/login.view.js index c5cbc0fe..0d90c806 100644 --- a/src/js/views/login.view.js +++ b/src/js/views/login.view.js @@ -16,7 +16,12 @@ import { chevronRightIconTemplate } from "/js/templates/icons/chevronRight.templ import { userPlusIconTemplate } from "/js/templates/icons/userPlusIcon.template.js"; import { verificationBadgeTemplate } from "/js/templates/verificationBadge.template.js"; import { automatedAccountBadgeTemplate } from "/js/templates/automatedAccountBadge.template.js"; -import { pageEffect, bindToPage, bindPageTitle } from "/js/router.js"; +import { + pageEffect, + bindToPage, + bindPageTitle, + onPageShow, +} from "/js/router.js"; import { Signal, ReactiveStore } from "/js/signals.js"; export default async function loginView({ @@ -487,7 +492,8 @@ export default async function loginView({ ); }); - root.addEventListener("page-enter", async () => { + onPageShow(root, async ({ action }) => { + if (action === "restore") return; // this can happen when the oauth callback fails - see callback.html const params = new URLSearchParams(window.location.search); const errorMessage = params.get("error_message"); diff --git a/src/js/views/notifications.view.js b/src/js/views/notifications.view.js index 42749b22..c9562c52 100644 --- a/src/js/views/notifications.view.js +++ b/src/js/views/notifications.view.js @@ -7,7 +7,12 @@ import { smallPostTemplate } from "/js/templates/smallPost.template.js"; import { postSkeletonTemplate } from "/js/templates/postSkeleton.template.js"; import { displayRelativeTime, batch } from "/js/utils.js"; import { Signal, ReactiveStore } from "/js/signals.js"; -import { bindToPage, pageEffect, bindPageTitle } from "/js/router.js"; +import { + bindToPage, + pageEffect, + bindPageTitle, + onPageShow, +} from "/js/router.js"; import { userIconTemplate } from "/js/templates/icons/userIcon.template.js"; import { userPlusIconTemplate } from "/js/templates/icons/userPlusIcon.template.js"; import { repostIconTemplate } from "/js/templates/icons/repostIcon.template.js"; @@ -820,12 +825,7 @@ export default async function notificationsView({ }); } - root.addEventListener("page-enter", async () => { - await loadNotifications({ reload: true }); - }); - - root.addEventListener("page-restore", async (e) => { - const scrollY = e.detail?.scrollY ?? 0; + onPageShow(root, async ({ scrollY }) => { if (scrollY <= 200) { window.scrollTo(0, 0); await loadNotifications({ reload: true }); diff --git a/src/js/views/postLikes.view.js b/src/js/views/postLikes.view.js index d17d6ba6..1638bb5b 100644 --- a/src/js/views/postLikes.view.js +++ b/src/js/views/postLikes.view.js @@ -1,5 +1,5 @@ import { html, render } from "/js/lib/lit-html.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { profileFeedTemplate } from "/js/templates/profileFeed.template.js"; import { formatLargeNumber } from "/js/utils.js"; @@ -96,5 +96,8 @@ export default async function postLikesView({ loadLikes({ reload: true }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/postQuotes.view.js b/src/js/views/postQuotes.view.js index 493b4c94..83811f81 100644 --- a/src/js/views/postQuotes.view.js +++ b/src/js/views/postQuotes.view.js @@ -2,7 +2,7 @@ import { html, render } from "/js/lib/lit-html.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { formatLargeNumber } from "/js/utils.js"; import { postFeedTemplate } from "/js/templates/postFeed.template.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; export default async function postQuotesView({ root, @@ -98,5 +98,8 @@ export default async function postQuotesView({ loadQuotes({ reload: true }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/postReposts.view.js b/src/js/views/postReposts.view.js index f9abee74..7909485b 100644 --- a/src/js/views/postReposts.view.js +++ b/src/js/views/postReposts.view.js @@ -1,5 +1,5 @@ import { html, render } from "/js/lib/lit-html.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { profileFeedTemplate } from "/js/templates/profileFeed.template.js"; import { formatLargeNumber } from "/js/utils.js"; @@ -96,5 +96,8 @@ export default async function postRepostsView({ loadReposts({ reload: true }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/postThread.view.js b/src/js/views/postThread.view.js index 04cf85d7..721ea909 100644 --- a/src/js/views/postThread.view.js +++ b/src/js/views/postThread.view.js @@ -1,7 +1,12 @@ import { html, render } from "/js/lib/lit-html.js"; import { avatarTemplate } from "/js/templates/avatar.template.js"; import { sortBy, maxBy, pinScrollPosition } from "/js/utils.js"; -import { bindToPage, pageEffect, bindPageTitle } from "/js/router.js"; +import { + bindToPage, + pageEffect, + bindPageTitle, + onPageShow, +} from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { smallPostTemplate } from "/js/templates/smallPost.template.js"; import { mutedParentToggleTemplate } from "/js/templates/mutedParentToggle.template.js"; @@ -624,20 +629,18 @@ export default async function postThreadView({ }); } - root.addEventListener("page-enter", async () => { + onPageShow(root, async ({ action, scrollY }) => { userHasScrolled = false; - // On a revisit the thread is already rendered, so pin it under the header - const largePost = root.querySelector(".large-post"); - const header = root.querySelector("header"); - if (largePost && header) { - scrollToLargePost(largePost, header); + if (action === "restore") { + window.scrollTo(0, scrollY); + } else { + // On a revisit the thread is already rendered, so pin it under the header + const largePost = root.querySelector(".large-post"); + const header = root.querySelector("header"); + if (largePost && header) { + scrollToLargePost(largePost, header); + } } - await dataLayer.requests.loadPostThread(postUri); - }); - - root.addEventListener("page-restore", async (e) => { - userHasScrolled = false; - window.scrollTo(0, e.detail?.scrollY ?? 0); // Revalidate await dataLayer.requests.loadPostThread(postUri); }); diff --git a/src/js/views/profile.view.js b/src/js/views/profile.view.js index 2b578606..ed3ef459 100644 --- a/src/js/views/profile.view.js +++ b/src/js/views/profile.view.js @@ -542,7 +542,7 @@ export default async function profileView({ return dataLayer.derived.$hydratedDetailedProfiles.get(profileDid); } - root.addEventListener("page-create", async () => { + async function loadPageData() { const profile = await loadProfile(); if (!profile) { return; @@ -565,5 +565,7 @@ export default async function profileView({ ) { dataLayer.requests.loadProfileChatStatus(profile.did); } - }); + } + + loadPageData().catch((error) => console.error(error)); } diff --git a/src/js/views/profileFollowers.view.js b/src/js/views/profileFollowers.view.js index 611115f5..a11bfd95 100644 --- a/src/js/views/profileFollowers.view.js +++ b/src/js/views/profileFollowers.view.js @@ -1,6 +1,6 @@ import { html, render } from "/js/lib/lit-html.js"; import { auth } from "/js/auth.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { profileFeedTemplate } from "/js/templates/profileFeed.template.js"; import { getDisplayName } from "/js/dataHelpers.js"; @@ -104,5 +104,8 @@ export default async function profileFollowersView({ loadFollowers({ reload: true }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/profileFollowing.view.js b/src/js/views/profileFollowing.view.js index b96a51fe..0b8e2ee9 100644 --- a/src/js/views/profileFollowing.view.js +++ b/src/js/views/profileFollowing.view.js @@ -1,6 +1,6 @@ import { html, render } from "/js/lib/lit-html.js"; import { auth } from "/js/auth.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { profileFeedTemplate } from "/js/templates/profileFeed.template.js"; import { getDisplayName } from "/js/dataHelpers.js"; @@ -101,5 +101,8 @@ export default async function profileFollowingView({ loadFollowing({ reload: true }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/profileKnownFollowers.view.js b/src/js/views/profileKnownFollowers.view.js index 6ed5426a..c49ebeb3 100644 --- a/src/js/views/profileKnownFollowers.view.js +++ b/src/js/views/profileKnownFollowers.view.js @@ -1,6 +1,6 @@ import { html, render } from "/js/lib/lit-html.js"; import { auth } from "/js/auth.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { profileFeedTemplate } from "/js/templates/profileFeed.template.js"; import { getDisplayName } from "/js/dataHelpers.js"; @@ -93,5 +93,8 @@ export default async function profileKnownFollowersView({ loadKnownFollowers({ reload: true }); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/search.view.js b/src/js/views/search.view.js index 6befec32..8e8ec761 100644 --- a/src/js/views/search.view.js +++ b/src/js/views/search.view.js @@ -199,6 +199,24 @@ export default async function searchView({ } } + function loadPageData() { + const query = new URLSearchParams(window.location.search); + if (query.get("tab")) { + const tab = query.get("tab"); + state.$activeTab.set(tab === "posts" ? "top" : tab); + } + const q = getUrlQuery(); + if (q) { + state.$inputValue.set(q); + state.$showTypeahead.set(false); + state.$committedQuery.set(q); + loadedTabs.clear(); + tabScrollState.clear(); + loadTabIfNeeded(state.$activeTab.get()); + } + hydrateAndPruneRecentProfiles(); + } + function handleTabChange(tab) { if (tab === state.$activeTab.get()) { if (window.scrollY > 0) { @@ -611,6 +629,8 @@ export default async function searchView({ } } + loadPageData(); + bindPageTitle(root, () => "Search"); pageEffect(root, () => { @@ -748,22 +768,4 @@ export default async function searchView({ root, ); }); - - root.addEventListener("page-create", () => { - const query = new URLSearchParams(window.location.search); - if (query.get("tab")) { - const tab = query.get("tab"); - state.$activeTab.set(tab === "posts" ? "top" : tab); - } - const q = getUrlQuery(); - if (q) { - state.$inputValue.set(q); - state.$showTypeahead.set(false); - state.$committedQuery.set(q); - loadedTabs.clear(); - tabScrollState.clear(); - loadTabIfNeeded(state.$activeTab.get()); - } - hydrateAndPruneRecentProfiles(); - }); } diff --git a/src/js/views/settings.view.js b/src/js/views/settings.view.js index a3a0a385..f5246fc0 100644 --- a/src/js/views/settings.view.js +++ b/src/js/views/settings.view.js @@ -1,4 +1,9 @@ -import { pageEffect, bindToPage, bindPageTitle } from "/js/router.js"; +import { + pageEffect, + bindToPage, + bindPageTitle, + onPageShow, +} from "/js/router.js"; import { html, render } from "/js/lib/lit-html.js"; import { eyeIconTemplate } from "/js/templates/icons/eyeIcon.template.js"; import { notificationsIconTemplate } from "/js/templates/icons/notificationsIcon.template.js"; @@ -389,7 +394,10 @@ export default async function settingsView({ root, context: { dataLayer } }) { loadOtherAccounts(); } - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); // Account actions navigate away with the pending spinner showing; if the // user comes back via the back/forward cache the document is restored diff --git a/src/js/views/settings/blockedAccounts.view.js b/src/js/views/settings/blockedAccounts.view.js index 5f8bd2f9..08b04857 100644 --- a/src/js/views/settings/blockedAccounts.view.js +++ b/src/js/views/settings/blockedAccounts.view.js @@ -1,5 +1,5 @@ import { html, render } from "/js/lib/lit-html.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { auth } from "/js/auth.js"; import { profileFeedTemplate } from "/js/templates/profileFeed.template.js"; @@ -73,5 +73,8 @@ export default async function settingsBlockedAccountsView({ ); }); - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/src/js/views/settings/mutedAccounts.view.js b/src/js/views/settings/mutedAccounts.view.js index d038b9b4..a5a25935 100644 --- a/src/js/views/settings/mutedAccounts.view.js +++ b/src/js/views/settings/mutedAccounts.view.js @@ -1,5 +1,5 @@ import { html, render } from "/js/lib/lit-html.js"; -import { pageEffect, bindPageTitle } from "/js/router.js"; +import { pageEffect, bindPageTitle, onPageShow } from "/js/router.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { auth } from "/js/auth.js"; import { profileFeedTemplate } from "/js/templates/profileFeed.template.js"; @@ -73,5 +73,8 @@ export default async function settingsMutedAccountsView({ ); }); - root.addEventListener("page-enter", () => loadPageData()); + onPageShow(root, ({ action }) => { + if (action === "restore") return; + loadPageData(); + }); } diff --git a/tests/unit/specs/router.test.js b/tests/unit/specs/router.test.js index 2f401f1d..7bbcf2d8 100644 --- a/tests/unit/specs/router.test.js +++ b/tests/unit/specs/router.test.js @@ -1270,60 +1270,43 @@ describe("page event dispatch", () => { return router; } - async function eventsFor(loadOptions) { + async function showDetailsFor(loadOptions) { const router = createRouter(); await router.load("/a"); const pageA = router.pages.get("/a").el; - const seen = []; - for (const name of ["page-enter", "page-restore", "page-show"]) { - pageA.addEventListener(name, () => seen.push(name)); - } + const details = []; + pageA.addEventListener("page-show", (event) => details.push(event.detail)); await router.load("/b"); await router.load("/a", loadOptions); - return seen; + return details; } - it("dispatches page-enter then page-show when a cached page is revisited forwards", async () => { - assert.deepEqual(await eventsFor({}), ["page-enter", "page-show"]); - }); - - it("dispatches page-restore then page-show on a history traversal", async () => { - assert.deepEqual(await eventsFor({ isRestore: true }), [ - "page-restore", - "page-show", + it("reports an advance action when a cached page is revisited forwards", async () => { + assert.deepEqual(await showDetailsFor({}), [ + { scrollY: 0, action: "advance" }, ]); }); - it("dispatches page-create, page-enter then page-show on first load", async () => { - const router = new Router(); - router.addRoute("/a", () => () => {}); - mountRouter(router); - const seen = []; - router.renderRoute(({ container }) => { - for (const name of ["page-create", "page-enter", "page-show"]) { - container.addEventListener(name, () => seen.push(name)); - } - }); - - await router.load("/a"); - - assert.deepEqual(seen, ["page-create", "page-enter", "page-show"]); + it("reports a restore action on a history traversal", async () => { + assert.deepEqual(await showDetailsFor({ isRestore: true }), [ + { scrollY: 0, action: "restore" }, + ]); }); - it("reports a zero scroll position on a first load", async () => { + it("dispatches a single page-show with an advance action on first load", async () => { const router = new Router(); router.addRoute("/a", () => () => {}); mountRouter(router); const details = []; router.renderRoute(({ container }) => { - for (const name of ["page-enter", "page-show"]) { - container.addEventListener(name, (event) => details.push(event.detail)); - } + container.addEventListener("page-show", (event) => + details.push(event.detail), + ); }); await router.load("/a"); - assert.deepEqual(details, [{ scrollY: 0 }, { scrollY: 0 }]); + assert.deepEqual(details, [{ scrollY: 0, action: "advance" }]); }); it("reports the restored scroll position on page-show", async () => { @@ -1339,19 +1322,21 @@ describe("page event dispatch", () => { await router.load("/a", { isRestore: true }); }); - assert.deepEqual(details, [{ scrollY: 120 }]); + assert.deepEqual(details, [{ scrollY: 120, action: "restore" }]); }); - it("does not dispatch page-create when a cached page is revisited", async () => { - const router = createRouter(); - await router.load("/a"); - const pageA = router.pages.get("/a").el; - const seen = []; - pageA.addEventListener("page-create", () => seen.push("page-create")); + it("does not re-run a view when its cached page is revisited", async () => { + const router = new Router(); + router.addRoute("/a", () => () => {}); + router.addRoute("/b", () => () => {}); + mountRouter(router); + const rendered = []; + router.renderRoute(({ container }) => rendered.push(container)); + await router.load("/a"); await router.load("/b"); await router.load("/a"); - assert.deepEqual(seen, []); + assert.deepEqual(rendered.length, 2); }); }); -- 2.51.2