Mirror of https://github.com/improsocial/impro An extensible Bluesky client for web impro.social
6

Configure Feed

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

Add layout to not found view

Grace Kind (Jul 13, 2026, 10:29 PM -0500) 6c2a362a da0855ee

+62 -103
+10 -1
src/css/style.css
··· 7865 7865 flex-direction: column; 7866 7866 align-items: center; 7867 7867 justify-content: center; 7868 - min-height: 100vh; 7868 + min-height: calc(100vh - 45px); 7869 + min-height: calc(100dvh - 45px); 7870 + } 7871 + 7872 + @media (max-width: 799px) { 7873 + #not-found-view main { 7874 + min-height: calc( 7875 + 100dvh - 45px - var(--footer-height) - var(--safe-area-inset-bottom) 7876 + ); 7877 + } 7869 7878 } 7870 7879 7871 7880 #not-found-view h1 {
+8 -1
src/index.html
··· 241 241 // This will save us from needing to resolve handles when navigating between pages. 242 242 setUpIdentityPrecaching(dataLayer, identityResolver); 243 243 244 + // Preload the current user for layout + views 245 + if (session) { 246 + dataLayer.declarative.ensureCurrentUser().catch((error) => { 247 + console.warn("Error preloading current user:", error); 248 + }); 249 + } 250 + 244 251 // Preload preferences - sometimes this fails, so try it twice. 245 252 try { 246 253 await dataLayer.initializePreferences(); ··· 442 449 () => settingsPluginDetailView, 443 450 settingsRouteOptions, 444 451 ); 445 - router.setNotFoundView(() => notFoundView, { layout: false }); 452 + router.setNotFoundView(() => notFoundView); 446 453 447 454 router.renderRoute(({ view, params, container, layout }) => { 448 455 return view.render({
-1
src/js/views/bookmarks.view.js
··· 60 60 61 61 root.addEventListener("page-enter", async () => { 62 62 window.scrollTo(0, 0); 63 - dataLayer.declarative.ensureCurrentUser(); 64 63 await loadBookmarks(); 65 64 }); 66 65
-1
src/js/views/chatRequests.view.js
··· 253 253 }); 254 254 255 255 root.addEventListener("page-enter", async () => { 256 - dataLayer.declarative.ensureCurrentUser(); 257 256 await dataLayer.declarative.ensureConvoRequestList(); 258 257 }); 259 258
-1
src/js/views/feedDetail.view.js
··· 130 130 } 131 131 132 132 root.addEventListener("page-enter", async () => { 133 - dataLayer.declarative.ensureCurrentUser(); 134 133 dataLayer.declarative.ensureFeedGenerator(feedUri); 135 134 await loadFeed(); 136 135 });
-1
src/js/views/feeds.view.js
··· 100 100 }); 101 101 102 102 root.addEventListener("page-enter", async () => { 103 - dataLayer.declarative.ensureCurrentUser(); 104 103 await dataLayer.declarative.ensurePinnedItems(); 105 104 }); 106 105
-1
src/js/views/hashtag.view.js
··· 115 115 } 116 116 117 117 root.addEventListener("page-enter", async () => { 118 - dataLayer.declarative.ensureCurrentUser(); 119 118 await loadCurrentFeed(); 120 119 }); 121 120
-1
src/js/views/listDetail.view.js
··· 299 299 } 300 300 301 301 root.addEventListener("page-enter", async () => { 302 - dataLayer.declarative.ensureCurrentUser(); 303 302 await Promise.all([ 304 303 dataLayer.declarative.ensureList(listUri), 305 304 loadFeed(),
+10 -7
src/js/views/notFound.view.js
··· 1 1 import { View } from "/js/views/view.js"; 2 2 import { html, render } from "/js/lib/lit-html.js"; 3 + import { headerTemplate } from "/js/templates/header.template.js"; 4 + import { pageEffect } from "/js/router.js"; 3 5 4 6 class NotFoundView extends View { 5 - async render({ root, context: { dataStore } }) { 6 - function renderPage() { 7 + async render({ root }) { 8 + pageEffect(root, () => { 7 9 render( 8 10 html`<div id="not-found-view"> 11 + ${headerTemplate({ 12 + title: "Not found", 13 + })} 9 14 <main> 10 - <h1>Not Found</h1> 11 - <a href="/">Go Home</a> 15 + <h1>Page not found</h1> 16 + <a href="/">Go home</a> 12 17 </main> 13 18 </div>`, 14 19 root, 15 20 ); 16 - } 17 - 18 - renderPage(); 21 + }); 19 22 } 20 23 } 21 24
-1
src/js/views/notifications.view.js
··· 793 793 } 794 794 795 795 root.addEventListener("page-enter", async () => { 796 - dataLayer.declarative.ensureCurrentUser(); 797 796 await loadNotifications({ reload: true }); 798 797 }); 799 798
-3
src/js/views/postLikes.view.js
··· 88 88 } 89 89 90 90 root.addEventListener("page-enter", async () => { 91 - if (isAuthenticated) { 92 - dataLayer.declarative.ensureCurrentUser(); 93 - } 94 91 // Load the post thread to get the post like count 95 92 dataLayer.declarative.ensurePostThread(postUri); 96 93 await loadLikes();
-3
src/js/views/postQuotes.view.js
··· 93 93 } 94 94 95 95 root.addEventListener("page-enter", async () => { 96 - if (isAuthenticated) { 97 - dataLayer.declarative.ensureCurrentUser(); 98 - } 99 96 // Load the post thread to get the post quote count 100 97 dataLayer.declarative.ensurePostThread(postUri); 101 98 await loadQuotes();
-3
src/js/views/postReposts.view.js
··· 87 87 } 88 88 89 89 root.addEventListener("page-enter", async () => { 90 - if (isAuthenticated) { 91 - dataLayer.declarative.ensureCurrentUser(); 92 - } 93 90 // Load the post thread to get the post repost count 94 91 dataLayer.declarative.ensurePostThread(postUri); 95 92 await loadReposts();
-3
src/js/views/postThread.view.js
··· 582 582 } 583 583 584 584 root.addEventListener("page-enter", async () => { 585 - if (isAuthenticated) { 586 - dataLayer.declarative.ensureCurrentUser(); 587 - } 588 585 try { 589 586 await dataLayer.declarative.ensurePostThread(postUri); 590 587 } catch (error) {
-1
src/js/views/profileFollowers.view.js
··· 93 93 } 94 94 95 95 root.addEventListener("page-enter", async () => { 96 - dataLayer.declarative.ensureCurrentUser(); 97 96 // Load the profile to get the follower count 98 97 dataLayer.declarative.ensureDetailedProfile(profileDid); 99 98 await loadFollowers();
-1
src/js/views/profileFollowing.view.js
··· 91 91 } 92 92 93 93 root.addEventListener("page-enter", async () => { 94 - dataLayer.declarative.ensureCurrentUser(); 95 94 // Load the profile to get the follows count 96 95 dataLayer.declarative.ensureDetailedProfile(profileDid); 97 96 await loadFollowing();
-1
src/js/views/profileKnownFollowers.view.js
··· 85 85 } 86 86 87 87 root.addEventListener("page-enter", async () => { 88 - dataLayer.declarative.ensureCurrentUser(); 89 88 dataLayer.declarative.ensureDetailedProfile(profileDid); 90 89 await loadKnownFollowers(); 91 90 });
-3
src/js/views/search.view.js
··· 463 463 if (state.$searchQuery.get()) { 464 464 loadSearchResults(); 465 465 } 466 - if (isAuthenticated) { 467 - dataLayer.declarative.ensureCurrentUser(); 468 - } 469 466 }); 470 467 471 468 root.addEventListener("page-restore", (event) => {
-1
src/js/views/settings.view.js
··· 395 395 }); 396 396 397 397 root.addEventListener("page-enter", async () => { 398 - dataLayer.declarative.ensureCurrentUser(); 399 398 loadOtherAccounts(); 400 399 }); 401 400
+1 -10
src/js/views/settings/advanced.view.js
··· 16 16 import { PermissionsDeclinedError } from "/js/plugins/pluginService.js"; 17 17 18 18 class SettingsAdvancedView extends View { 19 - async render({ 20 - root, 21 - router, 22 - layout, 23 - context: { dataLayer, pluginService }, 24 - }) { 19 + async render({ root, router, layout, context: { pluginService } }) { 25 20 await auth.requireAuth(); 26 21 27 22 const storedConfig = getAppViewConfig(); ··· 278 273 </div>`, 279 274 root, 280 275 ); 281 - }); 282 - 283 - root.addEventListener("page-enter", async () => { 284 - dataLayer.declarative.ensureCurrentUser(); 285 276 }); 286 277 287 278 root.addEventListener("page-restore", () => {
+1 -5
src/js/views/settings/appearance.view.js
··· 11 11 } from "/js/theme.js"; 12 12 13 13 class SettingsAppearanceView extends View { 14 - async render({ root, router, layout, context: { dataLayer } }) { 14 + async render({ root, router, layout }) { 15 15 await auth.requireAuth(); 16 16 17 17 function handleHighlightColorChange(newHighlightColor) { ··· 144 144 </div>`, 145 145 root, 146 146 ); 147 - }); 148 - 149 - root.addEventListener("page-enter", async () => { 150 - dataLayer.declarative.ensureCurrentUser(); 151 147 }); 152 148 153 149 root.addEventListener("page-restore", () => {
-1
src/js/views/settings/blockedAccounts.view.js
··· 69 69 }); 70 70 71 71 root.addEventListener("page-enter", async () => { 72 - dataLayer.declarative.ensureCurrentUser(); 73 72 await loadMore(); 74 73 }); 75 74
+1 -8
src/js/views/settings/communityPluginListing.view.js
··· 10 10 import "/js/components/rendered-markdown.js"; 11 11 12 12 class SettingsCommunityPluginListingView extends View { 13 - async render({ 14 - root, 15 - router, 16 - layout, 17 - params, 18 - context: { dataLayer, pluginService }, 19 - }) { 13 + async render({ root, router, layout, params, context: { pluginService } }) { 20 14 await auth.requireAuth(); 21 15 22 16 const { pluginId } = params; ··· 240 234 }); 241 235 242 236 root.addEventListener("page-enter", async () => { 243 - dataLayer.declarative.ensureCurrentUser(); 244 237 await loadListings(); 245 238 await loadDetails(); 246 239 });
+1 -7
src/js/views/settings/communityPlugins.view.js
··· 7 7 import { Signal, ReactiveStore } from "/js/signals.js"; 8 8 9 9 class SettingsCommunityPluginsView extends View { 10 - async render({ 11 - root, 12 - router, 13 - layout, 14 - context: { dataLayer, pluginService }, 15 - }) { 10 + async render({ root, router, layout, context: { pluginService } }) { 16 11 await auth.requireAuth(); 17 12 18 13 const state = new ReactiveStore("settingsCommunityPluginsView"); ··· 117 112 }); 118 113 119 114 root.addEventListener("page-enter", async () => { 120 - dataLayer.declarative.ensureCurrentUser(); 121 115 await loadListings(); 122 116 }); 123 117
-1
src/js/views/settings/mutedAccounts.view.js
··· 65 65 }); 66 66 67 67 root.addEventListener("page-enter", async () => { 68 - dataLayer.declarative.ensureCurrentUser(); 69 68 await loadMore(); 70 69 }); 71 70
-4
src/js/views/settings/mutedWords.view.js
··· 357 357 ); 358 358 }); 359 359 360 - root.addEventListener("page-enter", async () => { 361 - dataLayer.declarative.ensureCurrentUser(); 362 - }); 363 - 364 360 root.addEventListener("page-restore", () => { 365 361 window.scrollTo(0, 0); 366 362 });
+1 -11
src/js/views/settings/pluginDetail.view.js
··· 6 6 import { Signal, ReactiveStore } from "/js/signals.js"; 7 7 8 8 class SettingsPluginDetailView extends View { 9 - async render({ 10 - root, 11 - router, 12 - layout, 13 - params, 14 - context: { dataLayer, pluginService }, 15 - }) { 9 + async render({ root, router, layout, params, context: { pluginService } }) { 16 10 await auth.requireAuth(); 17 11 18 12 const { pluginId } = params; ··· 125 119 </div>`, 126 120 root, 127 121 ); 128 - }); 129 - 130 - root.addEventListener("page-enter", () => { 131 - dataLayer.declarative.ensureCurrentUser(); 132 122 }); 133 123 134 124 root.addEventListener("page-restore", () => {
+1 -10
src/js/views/settings/plugins.view.js
··· 15 15 import "/js/components/toggle-switch.js"; 16 16 17 17 class SettingsPluginsView extends View { 18 - async render({ 19 - root, 20 - router, 21 - layout, 22 - context: { dataLayer, pluginService }, 23 - }) { 18 + async render({ root, router, layout, context: { pluginService } }) { 24 19 await auth.requireAuth(); 25 20 26 21 const state = new ReactiveStore("settingsPluginsView"); ··· 332 327 </div>`, 333 328 root, 334 329 ); 335 - }); 336 - 337 - root.addEventListener("page-enter", async () => { 338 - dataLayer.declarative.ensureCurrentUser(); 339 330 }); 340 331 341 332 root.addEventListener("page-restore", () => {
+5 -7
tests/e2e/specs/concerns/layoutNavigation.test.js
··· 13 13 await expect(page.locator("#home-view")).toBeVisible({ timeout: 10000 }); 14 14 }); 15 15 16 - test("keeps a single layout whose active nav item follows navigation and hides on bare routes", async ({ 16 + test("keeps a single layout whose active nav item follows navigation, including the not-found page", async ({ 17 17 page, 18 18 }) => { 19 19 const layoutRoot = page.locator("#main-layout"); ··· 53 53 notificationsNavItem.locator(".icon.filled"), 54 54 ).not.toBeVisible(); 55 55 56 - // Bare routes (layout: false, e.g. the not-found page) hide the layout 57 - // entirely. /login can't be used here: it redirects authenticated users. 56 + // The not-found page keeps the layout, with no active nav item 58 57 await page.goto("/this-route-does-not-exist"); 59 58 await expect(page.locator("#not-found-view")).toBeVisible({ 60 59 timeout: 10000, 61 60 }); 62 - await expect(layoutRoot).toHaveClass(/layout-hidden/); 63 - await expect(layoutRoot).not.toBeVisible(); 61 + await expect(layoutRoot).toBeVisible(); 62 + await expect(layoutRoot.locator(".icon.filled")).not.toBeVisible(); 64 63 65 - // Returning to a layout route shows the chrome again 64 + // Returning to a layout route restores the active nav item 66 65 await page.goto("/"); 67 66 await expect(page.locator("#home-view")).toBeVisible({ timeout: 10000 }); 68 - await expect(layoutRoot).not.toHaveClass(/layout-hidden/); 69 67 await expect(layoutRoot).toBeVisible(); 70 68 await expect(homeNavItem.locator(".icon.filled")).toBeVisible(); 71 69 });
+23 -4
tests/e2e/specs/views/notFound.view.test.js
··· 8 8 }) => { 9 9 await page.goto("/some/nonexistent/route"); 10 10 11 - const notFoundView = page.locator("#not-found-view"); 11 + const notFoundView = page.locator( 12 + '[data-testid="view-column-center"] #not-found-view', 13 + ); 12 14 await expect( 13 - notFoundView.getByRole("heading", { name: "Not Found" }), 15 + notFoundView.getByRole("heading", { name: "Page not found" }), 14 16 ).toBeVisible(); 15 17 await expect( 16 - notFoundView.getByRole("link", { name: "Go Home" }), 18 + notFoundView.getByRole("link", { name: "Go home" }), 17 19 ).toBeVisible(); 18 20 }); 19 21 22 + test("should load the current user into the layout when authenticated", async ({ 23 + page, 24 + }) => { 25 + const mockServer = new MockServer(); 26 + await mockServer.setup(page); 27 + 28 + await login(page); 29 + await page.goto("/some/nonexistent/route"); 30 + 31 + const profileNavItem = page.locator('[data-testid="sidebar-nav-profile"]'); 32 + await expect(profileNavItem).toHaveAttribute( 33 + "href", 34 + "/profile/did:plc:testuser123", 35 + ); 36 + await expect(profileNavItem).not.toHaveClass(/disabled/); 37 + }); 38 + 20 39 test("should navigate home when clicking Go Home", async ({ page }) => { 21 40 const mockServer = new MockServer(); 22 41 await mockServer.setup(page); ··· 26 45 27 46 await page 28 47 .locator("#not-found-view") 29 - .getByRole("link", { name: "Go Home" }) 48 + .getByRole("link", { name: "Go home" }) 30 49 .click(); 31 50 32 51 await expect(page).toHaveURL("/");