Persist sidebar across navigations to preserve scroll position

Move AppSidebar out of per-route view components into App.svelte so it
is never unmounted on navigation. This fixes the sidebar scroll position
resetting to the top whenever the user opens a repository or navigates
between Issues, Patches and the repo home view.

Also removes the redundant invoke<Config>("config") call from each repo
route loader — the value is now taken from sidebarData.config, which
loadSidebarData() already fetches.
This commit is contained in:
Rūdolfs Ošiņš
2026-03-23 22:51:59 -06:00
parent d0763b3ab0
commit 7b396ff72d
11 changed files with 88 additions and 93 deletions
+37 -15
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import type { Config } from "@bindings/config/Config";
import type { ErrorWrapper } from "@bindings/error/ErrorWrapper";
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
@@ -20,12 +21,14 @@
import { invoke } from "@app/lib/invoke";
import { hide } from "@app/lib/modal";
import * as router from "@app/lib/router";
import { isLoadedRepoRoute } from "@app/lib/router/definitions";
import {
setUnlistenNodeEvents,
unlistenNodeEvents,
} from "@app/lib/startup.svelte";
import { isMac, unreachable } from "@app/lib/utils";
import AppSidebar from "@app/components/AppSidebar.svelte";
import { codeFont } from "@app/components/CodeFontSwitch.svelte";
import {
followSystemTheme,
@@ -50,6 +53,11 @@
const activeRouteStore = router.activeRouteStore;
const activeRepo = $derived.by((): RepoInfo | undefined => {
const route = $activeRouteStore;
return isLoadedRepoRoute(route) ? route.params.repo : undefined;
});
const DRAG_REGION_HEIGHT = 32;
const INTERACTIVE_TAGS = new Set([
"a",
@@ -144,6 +152,13 @@
align-items: center;
height: 100%;
}
.layout {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 100%;
height: 100%;
overflow: hidden;
}
</style>
<svelte:document
@@ -194,20 +209,27 @@
{:else if showSpinner}
<div class="spinner"><Spinner /></div>
{/if}
{:else if $activeRouteStore.resource === "inbox"}
<InboxView {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "guide"}
<GuideView {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.home"}
<RepoHome {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issue"}
<Issue {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issues"}
<Issues {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.patch"}
<Patch {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.patches"}
<Patches {...$activeRouteStore.params} />
{:else}
{unreachable($activeRouteStore)}
<div class="layout">
<AppSidebar
sidebarData={$activeRouteStore.params.sidebarData}
{activeRepo} />
{#if $activeRouteStore.resource === "inbox"}
<InboxView {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "guide"}
<GuideView {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.home"}
<RepoHome {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issue"}
<Issue {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issues"}
<Issues {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.patch"}
<Patch {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.patches"}
<Patches {...$activeRouteStore.params} />
{:else}
{unreachable($activeRouteStore)}
{/if}
</div>
{/if}
+12
View File
@@ -52,6 +52,18 @@ export type LoadedRoute =
| LoadedInboxRoute
| LoadedGuideRoute;
export function isLoadedRepoRoute(
route: LoadedRoute,
): route is LoadedRepoRoute {
return (
route.resource === "repo.home" ||
route.resource === "repo.issue" ||
route.resource === "repo.issues" ||
route.resource === "repo.patch" ||
route.resource === "repo.patches"
);
}
export async function loadSidebarData(): Promise<SidebarData> {
const [config, repos, notificationCount, seededNotReplicated] =
await Promise.all([
+1 -1
View File
@@ -81,7 +81,7 @@
}
</style>
<Layout {sidebarData}>
<Layout>
<div class="page">
<div class="hero">
<img src="/flower.png" alt="" />
+1 -1
View File
@@ -107,7 +107,7 @@
}
</style>
<Layout {sidebarData} selfScroll>
<Layout selfScroll>
{#if notificationCount.value === 0}
<div class="inbox-zero">
<div
+1 -4
View File
@@ -16,7 +16,6 @@
import { show } from "@app/lib/modal";
import * as roles from "@app/lib/roles";
import * as router from "@app/lib/router";
import type { SidebarData } from "@app/lib/router/definitions";
import {
explorerUrl,
issueStatusBackgroundColor,
@@ -49,7 +48,6 @@
config: Config;
threads: Thread[];
status: IssueStatus;
sidebarData: SidebarData;
}
/* eslint-disable prefer-const */
@@ -61,7 +59,6 @@
config,
threads,
status: initialStatus,
sidebarData,
}: Props = $props();
/* eslint-enable prefer-const */
@@ -346,7 +343,7 @@
}
</style>
<Layout {sidebarData} activeRepo={repo}>
<Layout>
<div class="page">
<div class="topbar">
<Icon name={issue.state.status === "open" ? "issue" : "issue-closed"} />
+2 -4
View File
@@ -15,7 +15,6 @@
} from "@app/lib/issueCounts.svelte";
import { show } from "@app/lib/modal";
import * as router from "@app/lib/router";
import type { SidebarData } from "@app/lib/router/definitions";
import { modifierKey } from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
@@ -32,11 +31,10 @@
repo: RepoInfo;
issues: Issue[];
status: IssueStatus;
sidebarData: SidebarData;
}
/* eslint-disable prefer-const */
let { repo, issues, status, sidebarData }: Props = $props();
let { repo, issues, status }: Props = $props();
/* eslint-enable prefer-const */
let cacheState: CacheEvent | undefined = $state();
@@ -156,7 +154,7 @@
}
</style>
<Layout {sidebarData} activeRepo={repo} selfScroll>
<Layout selfScroll>
<div class="page">
<div class="topbar">
<span class="topbar-title">Issues</span>
+18 -39
View File
@@ -1,24 +1,16 @@
<script lang="ts">
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import type { Snippet } from "svelte";
import type { SidebarData } from "@app/lib/router/definitions";
import AppSidebar from "@app/components/AppSidebar.svelte";
import ScrollArea from "@app/components/ScrollArea.svelte";
interface Props {
children: Snippet;
sidebarData: SidebarData;
activeRepo?: RepoInfo;
loadMoreContent?: () => Promise<void>;
selfScroll?: boolean;
}
const {
children,
sidebarData,
activeRepo = undefined,
loadMoreContent = undefined,
selfScroll = false,
}: Props = $props();
@@ -26,35 +18,22 @@
let loadingContent = false;
</script>
<style>
.layout {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 100%;
height: 100%;
overflow: hidden;
}
</style>
<div class="layout">
<AppSidebar {sidebarData} {activeRepo} />
{#if selfScroll}
<div
style="height: 100%; overflow: hidden; min-width: 0; background-color: var(--color-surface-canvas);">
{@render children()}
</div>
{:else}
<ScrollArea
style="height: 100%; width: 100%; background-color: var(--color-surface-canvas);"
onScrollHalf={loadMoreContent
? () => {
if (!loadingContent) {
loadingContent = true;
void loadMoreContent().finally(() => (loadingContent = false));
}
{#if selfScroll}
<div
style="height: 100%; overflow: hidden; min-width: 0; background-color: var(--color-surface-canvas);">
{@render children()}
</div>
{:else}
<ScrollArea
style="height: 100%; width: 100%; background-color: var(--color-surface-canvas);"
onScrollHalf={loadMoreContent
? () => {
if (!loadingContent) {
loadingContent = true;
void loadMoreContent().finally(() => (loadingContent = false));
}
: undefined}>
{@render children()}
</ScrollArea>
{/if}
</div>
}
: undefined}>
{@render children()}
</ScrollArea>
{/if}
+1 -4
View File
@@ -13,7 +13,6 @@
import { nodeRunning } from "@app/lib/events";
import { invoke } from "@app/lib/invoke";
import * as router from "@app/lib/router";
import type { SidebarData } from "@app/lib/router/definitions";
import {
didFromPublicKey,
explorerUrl,
@@ -46,7 +45,6 @@
activity: Operation<Action>[];
status: PatchStatus | undefined;
review: Review | DraftReview | undefined;
sidebarData: SidebarData;
}
/* eslint-disable prefer-const */
@@ -58,7 +56,6 @@
status: initialStatus,
activity,
review,
sidebarData,
}: Props = $props();
/* eslint-enable prefer-const */
@@ -249,7 +246,7 @@
}
</style>
<Layout {sidebarData} activeRepo={repo}>
<Layout>
{#if review}
<ReviewComponent
{config}
+2 -4
View File
@@ -17,7 +17,6 @@
updatePatchCounts,
} from "@app/lib/patchCounts.svelte";
import * as router from "@app/lib/router";
import type { SidebarData } from "@app/lib/router/definitions";
import { modifierKey } from "@app/lib/utils";
import CobCacheWarning from "@app/components/CobCacheWarning.svelte";
@@ -33,10 +32,9 @@
repo: RepoInfo;
patches: PaginatedQuery<Patch[]>;
status: PatchStatus | undefined;
sidebarData: SidebarData;
}
const { repo, patches, status, sidebarData }: Props = $props();
const { repo, patches, status }: Props = $props();
let items = $state(patches.content);
let cursor = patches.cursor;
@@ -225,7 +223,7 @@
}
</style>
<Layout {sidebarData} activeRepo={repo} selfScroll>
<Layout selfScroll>
<div class="page">
<div class="topbar">
<span class="topbar-title">Patches</span>
+2 -4
View File
@@ -9,7 +9,6 @@
import { useOverlayScrollbars } from "overlayscrollbars-svelte";
import { invoke, InvokeError } from "@app/lib/invoke";
import type { SidebarData } from "@app/lib/router/definitions";
import { highlight } from "@app/lib/syntax";
import FileBlock from "@app/components/FileBlock.svelte";
@@ -27,11 +26,10 @@
tree: Tree;
repo: RepoInfo;
readme: Readme | null;
sidebarData: SidebarData;
}
/* eslint-disable prefer-const */
let { tree, readme, repo, sidebarData }: Props = $props();
let { tree, readme, repo }: Props = $props();
/* eslint-enable prefer-const */
let currentPath = $state("");
@@ -120,7 +118,7 @@
}
</style>
<Layout {sidebarData} activeRepo={repo} selfScroll>
<Layout selfScroll>
<div
class="content"
style:display="flex"
+11 -17
View File
@@ -42,7 +42,6 @@ export interface LoadedRepoHomeRoute {
repo: RepoInfo;
sha?: string;
tree: Tree;
config: Config;
readme: Readme | null;
sidebarData: SidebarData;
};
@@ -72,7 +71,6 @@ export interface LoadedRepoIssuesRoute {
resource: "repo.issues";
params: {
repo: RepoInfo;
config: Config;
issues: Issue[];
status: IssueStatus;
sidebarData: SidebarData;
@@ -114,7 +112,6 @@ export interface LoadedRepoPatchesRoute {
resource: "repo.patches";
params: {
repo: RepoInfo;
config: Config;
patches: PaginatedQuery<Patch[]>;
status: PatchStatus | undefined;
sidebarData: SidebarData;
@@ -137,10 +134,9 @@ export type LoadedRepoRoute =
export async function loadPatch(
route: RepoPatchRoute,
): Promise<LoadedRepoPatchRoute> {
const [sidebarData, config, repo, patches, patch, revisions, activity] =
const [sidebarData, repo, patches, patch, revisions, activity] =
await Promise.all([
loadSidebarData(),
invoke<Config>("config"),
invoke<RepoInfo>("repo_by_id", {
rid: route.rid,
}),
@@ -163,6 +159,8 @@ export async function loadPatch(
}),
]);
const config = sidebarData.config;
const draftReview =
route.reviewId !== undefined &&
draftReviewStorage.get(route.reviewId, {
@@ -195,9 +193,8 @@ export async function loadPatch(
export async function loadPatches(
route: RepoPatchesRoute,
): Promise<LoadedRepoPatchesRoute> {
const [sidebarData, config, repo, patches] = await Promise.all([
const [sidebarData, repo, patches] = await Promise.all([
loadSidebarData(),
invoke<Config>("config"),
invoke<RepoInfo>("repo_by_id", {
rid: route.rid,
}),
@@ -210,16 +207,15 @@ export async function loadPatches(
return {
resource: "repo.patches",
params: { sidebarData, repo, config, patches, status: route.status },
params: { sidebarData, repo, patches, status: route.status },
};
}
export async function loadRepoHome(
route: RepoHomeRoute,
): Promise<LoadedRepoHomeRoute> {
const [sidebarData, config, repo, readme, tree] = await Promise.all([
const [sidebarData, repo, readme, tree] = await Promise.all([
loadSidebarData(),
invoke<Config>("config"),
invoke<RepoInfo>("repo_by_id", {
rid: route.rid,
}),
@@ -235,17 +231,16 @@ export async function loadRepoHome(
return {
resource: "repo.home",
params: { sidebarData, repo, sha: route.sha, config, readme, tree },
params: { sidebarData, repo, sha: route.sha, readme, tree },
};
}
export async function loadIssue(
route: RepoIssueRoute,
): Promise<LoadedRepoIssueRoute> {
const [sidebarData, config, repo, issue, activity, issues, threads] =
const [sidebarData, repo, issue, activity, issues, threads] =
await Promise.all([
loadSidebarData(),
invoke<Config>("config"),
invoke<RepoInfo>("repo_by_id", {
rid: route.rid,
}),
@@ -272,7 +267,7 @@ export async function loadIssue(
params: {
sidebarData,
repo,
config,
config: sidebarData.config,
issue,
activity,
issues,
@@ -285,9 +280,8 @@ export async function loadIssue(
export async function loadIssues(
route: RepoIssuesRoute,
): Promise<LoadedRepoIssuesRoute> {
const [sidebarData, config, repo, issues] = await Promise.all([
const [sidebarData, repo, issues] = await Promise.all([
loadSidebarData(),
invoke<Config>("config"),
invoke<RepoInfo>("repo_by_id", {
rid: route.rid,
}),
@@ -299,7 +293,7 @@ export async function loadIssues(
return {
resource: "repo.issues",
params: { sidebarData, repo, config, issues, status: route.status },
params: { sidebarData, repo, issues, status: route.status },
};
}