csr tangled client in solid-js
15

Configure Feed

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

improve repo prefetching

dawn (May 31, 2026, 2:05 AM +0300) 67f29a35 00a96cb1

+336 -206
-20
src/components/common.tsx
··· 254 254 255 255 export const textareaStyles = (): string => 256 256 'untangled-textarea w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-gray-100'; 257 - 258 - export const usePreloader = () => { 259 - const queryClient = useQueryClient(); 260 - 261 - return (queryKey: readonly unknown[], queryFn: () => Promise<any>, delayMs = 30) => { 262 - let hoverTimer: ReturnType<typeof setTimeout> | undefined; 263 - 264 - const onMouseEnter = () => { 265 - hoverTimer = setTimeout(() => { 266 - queryClient.prefetchQuery({ queryKey, queryFn }); 267 - }, delayMs); 268 - }; 269 - 270 - const onMouseLeave = () => { 271 - clearTimeout(hoverTimer); 272 - }; 273 - 274 - return { onMouseEnter, onMouseLeave }; 275 - }; 276 - };
+7 -6
src/components/repo.tsx
··· 32 32 import { getRepoPullCount } from '../lib/api/pulls'; 33 33 import { formatRelativeTime, languageColor, encodePath, formatLanguagePercent, blobHref, treeHref } from '../lib/repo-utils'; 34 34 import { getTangledAppviewService } from '../lib/settings'; 35 - import { Avatar, SkeletonBlock, StateBadge, PlaceholderAvatar, buttonStyles, cardStyles, usePreloader } from './common'; 36 - import { repoQueryKey } from '../pages/repo/shared'; 35 + import { Avatar, SkeletonBlock, StateBadge, PlaceholderAvatar, buttonStyles, cardStyles } from './common'; 36 + import { useRepoPreloader } from '../lib/preloading'; 37 + 37 38 38 39 marked.setOptions({ 39 40 gfm: true, ··· 113 114 </div> 114 115 ); 115 116 116 - const TreeRowsSkeleton: Component = () => ( 117 + export const TreeRowsSkeleton: Component = () => ( 117 118 <div class="space-y-3"> 118 119 <For each={skeletonRows}> 119 120 {(_, index) => ( ··· 129 130 </div> 130 131 ); 131 132 132 - const RepoOverviewSidebarSkeleton: Component = () => ( 133 + export const RepoOverviewSidebarSkeleton: Component = () => ( 133 134 <div class="min-w-0 md:pt-0 md:pl-6 border-gray-100 dark:border-gray-700 md:border-t-0 md:border-l"> 134 135 <div class="flex items-center gap-2 pb-3 font-bold"> 135 136 <SkeletonBlock class="size-4" /> ··· 1235 1236 1236 1237 export const RepoCard: Component<RepoCardProps> = (props) => { 1237 1238 const linkHref = () => props.href || `/${props.owner}/${props.name}`; 1238 - const preload = usePreloader(); 1239 + const preloadRepo = useRepoPreloader(); 1239 1240 1240 1241 return ( 1241 1242 <div ··· 1259 1260 'truncate min-w-0 no-underline hover:underline text-black dark:text-white font-bold', 1260 1261 props.compact && 'focus:outline-none' 1261 1262 )} 1262 - {...preload(repoQueryKey(props.owner, props.name), () => getRepo(props.owner, props.name))} 1263 + {...preloadRepo(props.owner, props.name)} 1263 1264 > 1264 1265 <Show when={props.showOwner !== false} fallback={props.name}> 1265 1266 {props.owner}/{props.name}
+2
src/index.css
··· 3874 3874 } 3875 3875 3876 3876 3877 + 3878 +
+135 -6
src/lib/preloading.tsx
··· 1 1 import { useQueryClient } from '@tanstack/solid-query'; 2 - import { usePreloader } from '../components/common'; 3 - import { getRepo, type RepoContext } from './api/repos'; 4 - import { getIssue } from './api/issues'; 5 - import { fetchPullRoundPatch, getPull } from './api/pulls'; 6 - import { repoQueryKey, issueQueryKey, pullQueryKey, pullPatchQueryKey } from './api/keys'; 2 + import { useAuth } from './auth'; 3 + import { 4 + getRepo, 5 + getRepoBranches, 6 + getRepoDefaultBranch, 7 + getRepoTags, 8 + getRepoForkCount, 9 + getRepoTree, 10 + getRepoLog, 11 + getRepoLanguages, 12 + type RepoContext, 13 + } from './api/repos'; 14 + import { getIssue, getRepoIssueCount } from './api/issues'; 15 + import { fetchPullRoundPatch, getPull, getRepoPullCount } from './api/pulls'; 16 + import { getString } from './api/strings'; 17 + import { getRepoStarSummary } from './api/stars'; 18 + import { repoQueryKey, issueQueryKey, pullQueryKey, pullPatchQueryKey, issuesQueryKey, pullsQueryKey } from './api/keys'; 19 + import { resolveDefaultBranchName } from '../pages/repo/code-helpers'; 20 + 21 + export const PRELOAD_HOVER_TIME = 30; 22 + 23 + export const usePreloader = () => { 24 + const queryClient = useQueryClient(); 25 + 26 + return (queryKey: readonly unknown[], queryFn: () => Promise<any>, delayMs = PRELOAD_HOVER_TIME) => { 27 + let hoverTimer: ReturnType<typeof setTimeout> | undefined; 28 + 29 + const onMouseEnter = () => { 30 + hoverTimer = setTimeout(() => { 31 + queryClient.prefetchQuery({ queryKey, queryFn }); 32 + }, delayMs); 33 + }; 34 + 35 + const onMouseLeave = () => { 36 + clearTimeout(hoverTimer); 37 + }; 38 + 39 + return { onMouseEnter, onMouseLeave }; 40 + }; 41 + }; 42 + 43 + export const useRepoPreloader = () => { 44 + const queryClient = useQueryClient(); 45 + const auth = useAuth(); 46 + const preload = usePreloader(); 47 + 48 + return (owner: string, name: string) => { 49 + return preload( 50 + ['repo-prefetch', owner, name], 51 + async () => { 52 + const repo = await queryClient.fetchQuery({ 53 + queryKey: repoQueryKey(owner, name), 54 + queryFn: () => getRepo(owner, name), 55 + }); 56 + 57 + if (!repo) { 58 + return repo; 59 + } 60 + 61 + const currentDid = auth.currentDid(); 62 + 63 + // Fetch branches and default-branch, and prefetch counts and tags concurrently 64 + const [branches, defaultBranchRes] = await Promise.all([ 65 + queryClient.fetchQuery({ 66 + queryKey: ['repo-branches', repo.repoDid], 67 + queryFn: () => getRepoBranches(repo), 68 + }), 69 + queryClient.fetchQuery({ 70 + queryKey: ['repo-default-branch', repo.repoDid], 71 + queryFn: () => getRepoDefaultBranch(repo), 72 + }), 73 + queryClient.prefetchQuery({ 74 + queryKey: ['repo-star-summary', repo.repoDid, currentDid], 75 + queryFn: () => getRepoStarSummary(repo, currentDid), 76 + }), 77 + queryClient.prefetchQuery({ 78 + queryKey: ['repo-fork-count', repo.repoDid], 79 + queryFn: () => getRepoForkCount(repo), 80 + }), 81 + queryClient.prefetchQuery({ 82 + queryKey: [...issuesQueryKey(repo.repoDid), 'count'], 83 + queryFn: () => getRepoIssueCount(repo), 84 + }), 85 + queryClient.prefetchQuery({ 86 + queryKey: [...pullsQueryKey(repo.repoDid), 'count'], 87 + queryFn: () => getRepoPullCount(repo), 88 + }), 89 + queryClient.prefetchQuery({ 90 + queryKey: ['repo-tags', repo.repoDid], 91 + queryFn: () => getRepoTags(repo), 92 + }), 93 + ]); 94 + 95 + const defaultBranchName = 96 + defaultBranchRes?.name ?? 97 + defaultBranchRes?.branch ?? 98 + defaultBranchRes?.reference?.name ?? 99 + resolveDefaultBranchName(branches?.branches) ?? 100 + 'main'; 101 + 102 + // Now that we have the default branch name, prefetch tree, log and languages 103 + await Promise.all([ 104 + queryClient.prefetchQuery({ 105 + queryKey: ['repo-tree', repo.repoDid, defaultBranchName, ''], 106 + queryFn: async () => ({ 107 + tree: await getRepoTree(repo, defaultBranchName, ''), 108 + path: '', 109 + ref: defaultBranchName, 110 + }), 111 + }), 112 + queryClient.prefetchQuery({ 113 + queryKey: ['repo-log', repo.repoDid, defaultBranchName], 114 + queryFn: () => getRepoLog(repo, defaultBranchName), 115 + }), 116 + queryClient.prefetchQuery({ 117 + queryKey: ['repo-languages', repo.repoDid, defaultBranchName], 118 + queryFn: () => getRepoLanguages(repo, defaultBranchName), 119 + }), 120 + ]).catch(() => { 121 + // Ignore background prefetch errors 122 + }); 123 + 124 + return repo; 125 + } 126 + ); 127 + }; 128 + }; 129 + 130 + export const useStringPreloader = () => { 131 + const preload = usePreloader(); 132 + return (actor: string, rkey: string) => { 133 + return preload(['string-detail', actor, rkey], () => getString(actor, rkey)); 134 + }; 135 + }; 7 136 8 137 export const useIssuePreloader = () => { 9 138 const queryClient = useQueryClient(); ··· 23 152 [kind, uri, 'prefetch'], 24 153 async () => { 25 154 const repo = 26 - repoContext || 155 + repoContext || 27 156 (await queryClient.fetchQuery({ 28 157 queryKey: repoQueryKey(owner, slug), 29 158 queryFn: () => getRepo(owner, slug),
+5 -6
src/pages/home.tsx
··· 17 17 import { createQuery } from '@tanstack/solid-query'; 18 18 import { For, Show, createEffect, createMemo, createSignal, onCleanup, type Component, type JSX } from 'solid-js'; 19 19 20 - import { Avatar, ErrorState, LoadingState, StateBadge, usePreloader } from '../components/common'; 20 + import { Avatar, ErrorState, LoadingState, StateBadge } from '../components/common'; 21 21 import { RepoCard } from '../components/repo'; 22 22 import { listFollowRecords } from '../lib/api/graph'; 23 23 import { resolveActor } from '../lib/api/identity'; 24 - import { getRepo, listRepoRecords, type RepoRecord } from '../lib/api/repos'; 24 + import { listRepoRecords, type RepoRecord } from '../lib/api/repos'; 25 25 import { useAuth } from '../lib/auth'; 26 26 import { useLiveEvents, type LiveEvent } from '../lib/live-events'; 27 - import { useIssuePreloader } from '../lib/preloading'; 27 + import { useIssuePreloader, useRepoPreloader } from '../lib/preloading'; 28 28 import { formatRelativeTime, getErrorMessage, loadRecentRepos, loadRecentIssuesPulls } from '../lib/repo-utils'; 29 - import { repoQueryKey } from '../lib/api/keys'; 30 29 31 30 const SAMPLE_REPOS: Array<{ 32 31 owner: string; ··· 747 746 const ActivityRow: Component<{ item: HomeActivityItem; index: number }> = (props) => { 748 747 const repoUrl = () => `/${props.item.repoLabel}`; 749 748 const actorUrl = () => (props.item.actorHandle ? `/${props.item.actorHandle}` : '#'); 750 - const preload = usePreloader(); 749 + const preloadRepo = useRepoPreloader(); 751 750 const issuePreloader = useIssuePreloader(); 752 751 753 752 const actionText = () => { ··· 768 767 769 768 const preloadRepoProps = () => { 770 769 const [owner, slug] = props.item.repoLabel.split('/'); 771 - return preload(repoQueryKey(owner, slug), () => getRepo(owner, slug)); 770 + return preloadRepo(owner, slug); 772 771 }; 773 772 774 773 return (
+5 -4
src/pages/profile.tsx
··· 23 23 import { createQuery, useQueryClient } from '@tanstack/solid-query'; 24 24 import { For, Show, Switch, Match, createMemo, createSignal, createEffect, type Component } from 'solid-js'; 25 25 import type { Did } from '@atcute/lexicons/syntax'; 26 - import { ErrorState, LoadingState, PlaceholderAvatar, SkeletonBlock, textareaStyles, inputStyles, usePreloader } from '../components/common'; 26 + import { ErrorState, LoadingState, PlaceholderAvatar, SkeletonBlock, textareaStyles, inputStyles } from '../components/common'; 27 27 import { RepoCard, RepoCardSkeleton, StringCardSkeleton } from '../components/repo'; 28 + import { useStringPreloader } from '../lib/preloading'; 28 29 import { 29 30 listFollowRecords, 30 31 createFollow, ··· 40 41 } from '../lib/api/graph'; 41 42 import { resolveActor, getActorProfile, resolveAvatarUrl, putActorProfile } from '../lib/api/identity'; 42 43 import { listRepoRecords, getRepoByDid, type RepoContext } from '../lib/api/repos'; 43 - import { listStringRecords, getString } from '../lib/api/strings'; 44 + import { listStringRecords } from '../lib/api/strings'; 44 45 import { useAuth } from '../lib/auth'; 45 46 import { listAllAppviewRecords } from '../lib/api/appview'; 46 47 import { formatRelativeTime, getErrorMessage } from '../lib/repo-utils'; ··· 972 973 const [searchParams, setSearchParams] = useSearchParams(); 973 974 const queryClient = useQueryClient(); 974 975 const auth = useAuth(); 975 - const preload = usePreloader(); 976 + const preloadString = useStringPreloader(); 976 977 977 978 const navigate = useNavigate(); 978 979 ··· 1555 1556 <A 1556 1557 href={`/strings/${resolvedActor().handle || resolvedActor().did}/${item.rkey}`} 1557 1558 class="hover:underline" 1558 - {...preload(['string-detail', resolvedActor().handle || resolvedActor().did, item.rkey], () => getString(resolvedActor().handle || resolvedActor().did, item.rkey))} 1559 + {...preloadString(resolvedActor().handle || resolvedActor().did, item.rkey)} 1559 1560 > 1560 1561 {item.value.filename} 1561 1562 </A> </div>
+174 -157
src/pages/repo/code.tsx
··· 5 5 import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, type Accessor, type Component } from 'solid-js'; 6 6 import { buildBlobDataUrl, decodeBlobBytes, decodeBlobText, getRepoBlob, getRepoBranches, getRepoDefaultBranch, getRepoLanguages, getRepoLog, getRepoTags, getRepoTree, type CommitHeadline, type RepoContext, type BranchEntry } from '../../lib/api/repos'; 7 7 import { Avatar, ErrorState, PlaceholderAvatar, buttonStyles, cardStyles } from '../../components/common'; 8 - import { CloneDropdown, CodeView, FileRow, MarkdownBlock, OverviewFileRow, ReadmeCard, RepoBlobSkeleton, RepoTreeSkeleton, PathBreadcrumbs, TreeLatestCommitPanel, RepoLanguageBar, LoadingFilePath } from '../../components/repo'; 8 + import { CloneDropdown, CodeView, FileRow, MarkdownBlock, OverviewFileRow, ReadmeCard, RepoBlobSkeleton, RepoTreeSkeleton, PathBreadcrumbs, TreeLatestCommitPanel, RepoLanguageBar, LoadingFilePath, TreeRowsSkeleton, RepoOverviewSidebarSkeleton } from '../../components/repo'; 9 9 import { RepoFrame, useRepoQuery } from './shared'; 10 10 import { blobHref, commitHref, commitsHref, countLines, decodeRoutePath, formatBytes, formatRelativeTime, getErrorMessage, getParentPath, imageLike, isDirectory, joinPath, markdownLike, safeDecode, sortedTreeEntries, svgLike, treeHref, updateRecentRepoLanguage, videoLike } from '../../lib/repo-utils'; 11 11 import { LOADING_DELAY_MS, hasNamedRef, normalizeLanguages, resolveDefaultBranchName, resolveRouteRefAndPath, shouldAnimateNavigation, useDelayedLoading } from './code-helpers'; ··· 451 451 }; 452 452 }); 453 453 const overviewError = createMemo( 454 - () => treeQuery.error || branchesQuery.error || tagsQuery.error || logQuery.error || languagesQuery.error, 454 + () => branchesQuery.error || tagsQuery.error, 455 455 ); 456 456 const overviewReady = createMemo(() => 457 - Boolean(repoQuery.data && treeQuery.data && branchesQuery.data && tagsQuery.data && logQuery.data), 457 + Boolean(repoQuery.data && branchesQuery.data && tagsQuery.data), 458 458 ); 459 459 const languagesLoading = createMemo(() => !languagesQuery.data && (languagesQuery.isLoading || languagesQuery.isFetching)); 460 460 const showOverviewSkeleton = useDelayedLoading(() => !overviewReady() && !overviewError()); ··· 564 564 <div class={cardStyles('p-6')}> 565 565 <ErrorState message={getErrorMessage(overviewError())} /> 566 566 </div> 567 - </Match> 568 - <Match when={overviewReady()}> 567 + </Match> <Match when={overviewReady()}> 569 568 {(() => { 570 569 const repo = repoQuery.data!; 571 - const treeData = treeQuery.data!; 572 - const tree = treeData.tree; 573 - const languages = normalizeLanguages(languagesQuery.data?.languages ?? [], languagesQuery.data?.totalSize); 570 + const treeData = () => treeQuery.data; 571 + const tree = () => treeData()?.tree; 572 + const languages = normalizeLanguages(languagesQuery.data?.languages ?? [], languagesQuery.data?.totalSize); 574 573 const branches = branchesQuery.data?.branches ?? []; 575 - const tags = tagsQuery.data?.tags ?? []; 576 - const commits = logQuery.data?.commits ?? []; 577 - const activeRef = selectedTreeRef(); 578 - const currentDefaultBranch = defaultBranch() ?? activeRef; 579 - const activeRefInOptions = hasNamedRef(activeRef, branches, tags); 580 - const treeRef = activeRef; 581 - const treePath = treeData.path; 582 - const parentPath = tree.dotdot ?? getParentPath(treePath); 583 - const sortedFiles = sortedTreeEntries(tree.files ?? []); 584 - const isNestedTreePath = treePath.length > 0; 574 + const tags = tagsQuery.data?.tags ?? []; 575 + const commits = () => logQuery.data?.commits ?? []; 576 + const activeRef = selectedTreeRef(); 577 + const currentDefaultBranch = defaultBranch() ?? activeRef; 578 + const activeRefInOptions = hasNamedRef(activeRef, branches, tags); 579 + const treeRef = activeRef; 580 + const treePath = () => treeData()?.path ?? ''; 581 + const parentPath = () => tree() ? (tree()!.dotdot ?? getParentPath(treePath())) : null; 582 + const sortedFiles = () => sortedTreeEntries(tree()?.files ?? []); 583 + const isNestedTreePath = () => treePath().length > 0; 585 584 586 585 return ( 587 586 <> ··· 639 638 <CloneDropdown repo={repo} refName={activeRef} /> 640 639 </div> 641 640 642 - <Show when={isNestedTreePath}> 641 + <Show when={isNestedTreePath()}> 643 642 <div> 644 - <PathBreadcrumbs repo={repo} refName={treeRef} path={treePath} withDivider /> 645 - <Show when={tree.lastCommit}> 643 + <PathBreadcrumbs repo={repo} refName={treeRef} path={treePath()} withDivider /> 644 + <Show when={tree()?.lastCommit}> 646 645 {(commit) => <TreeLatestCommitPanel repo={repo} commit={commit()} />} 647 646 </Show> 648 647 </div> 649 648 </Show> 650 649 651 - <div class={clsx('untangled-repo-overview', isNestedTreePath && 'untangled-repo-overview--path')}> 652 - <div class="untangled-file-tree-pane min-w-0 pr-0 md:pr-6"> 653 - <div class="space-y-3"> 654 - <Show when={parentPath !== null}> 655 - <OverviewFileRow 656 - name=".." 657 - href={treeHref(repo, treeRef, parentPath ?? '')} 658 - icon={<Folder class="size-4 flex-shrink-0 fill-current" />} 659 - class={pendingTreeHref() === treeHref(repo, treeRef, parentPath ?? '') ? 'untangled-file-row-loading' : undefined} 660 - onClick={(event) => { 661 - if (shouldAnimateNavigation(event)) { 662 - navigationAttempt += 1; 663 - setPendingTreeHref(treeHref(repo, treeRef, parentPath ?? '')); 664 - } 665 - }} 666 - /> 667 - </Show> 668 - <For each={sortedFiles}> 669 - {(entry) => { 670 - const entryPath = joinPath(treePath, entry.name); 671 - const directory = isDirectory(entry); 672 - const href = directory ? treeHref(repo, treeRef, entryPath) : blobHref(repo, treeRef, entryPath); 673 - const currentRef = effectiveRef(); 650 + <div class={clsx('untangled-repo-overview', isNestedTreePath() && 'untangled-repo-overview--path')}> 651 + <div class="untangled-file-tree-pane min-w-0 pr-0 md:pr-6"> 652 + <Show when={!treeQuery.isLoading} fallback={<TreeRowsSkeleton />}> 653 + <Switch fallback={ 654 + <div class="space-y-3"> 655 + <Show when={parentPath() !== null}> 656 + <OverviewFileRow 657 + name=".." 658 + href={treeHref(repo, treeRef, parentPath() ?? '')} 659 + icon={<Folder class="size-4 flex-shrink-0 fill-current" />} 660 + class={pendingTreeHref() === treeHref(repo, treeRef, parentPath() ?? '') ? 'untangled-file-row-loading' : undefined} 661 + onClick={(event) => { 662 + if (shouldAnimateNavigation(event)) { 663 + navigationAttempt += 1; 664 + setPendingTreeHref(treeHref(repo, treeRef, parentPath() ?? '')); 665 + } 666 + }} 667 + /> 668 + </Show> 669 + <For each={sortedFiles()}> 670 + {(entry) => { 671 + const entryPath = joinPath(treePath(), entry.name); 672 + const directory = isDirectory(entry); 673 + const href = directory ? treeHref(repo, treeRef, entryPath) : blobHref(repo, treeRef, entryPath); 674 + const currentRef = effectiveRef(); 675 + 676 + let hoverTimer: ReturnType<typeof setTimeout> | undefined; 674 677 675 - let hoverTimer: ReturnType<typeof setTimeout> | undefined; 678 + const handleMouseEnter = () => { 679 + hoverTimer = setTimeout(() => { 680 + if (!directory) { 681 + queryClient.prefetchQuery({ 682 + queryKey: ['blob', repo.repoDid, treeRef, entryPath], 683 + queryFn: () => getRepoBlob(repo, treeRef, entryPath), 684 + }); 685 + return; 686 + } 676 687 677 - const handleMouseEnter = () => { 678 - hoverTimer = setTimeout(() => { 679 - if (!directory) { 680 688 queryClient.prefetchQuery({ 681 - queryKey: ['blob', repo.repoDid, treeRef, entryPath], 682 - queryFn: () => getRepoBlob(repo, treeRef, entryPath), 689 + queryKey: ['repo-tree', repo.repoDid, currentRef, entryPath], 690 + queryFn: async () => ({ 691 + tree: await getRepoTree(repo, currentRef!, entryPath), 692 + path: entryPath, 693 + ref: currentRef!, 694 + }), 683 695 }); 684 - return; 685 - } 686 - 687 - queryClient.prefetchQuery({ 688 - queryKey: ['repo-tree', repo.repoDid, currentRef, entryPath], 689 - queryFn: async () => ({ 690 - tree: await getRepoTree(repo, currentRef!, entryPath), 691 - path: entryPath, 692 - ref: currentRef!, 693 - }), 694 - }); 695 - }, 30); 696 - }; 697 - const handleMouseLeave = () => { 698 - clearTimeout(hoverTimer); 699 - }; 696 + }, 30); 697 + }; 698 + const handleMouseLeave = () => { 699 + clearTimeout(hoverTimer); 700 + }; 700 701 701 - return ( 702 - <OverviewFileRow 703 - name={entry.name} 704 - href={href} 705 - icon={ 706 - directory ? ( 707 - <Folder class="size-4 flex-shrink-0 fill-current" /> 708 - ) : ( 709 - <File class="size-4 flex-shrink-0" /> 710 - ) 711 - } 712 - lastCommit={entry.last_commit} 713 - lastCommitHref={entry.last_commit ? commitHref(repo, entry.last_commit.hash) : undefined} 714 - class={pendingTreeHref() === href ? 'untangled-file-row-loading' : undefined} 715 - onMouseEnter={handleMouseEnter} 716 - onMouseLeave={handleMouseLeave} 717 - onClick={(event) => { 718 - if (directory) { 719 - if (shouldAnimateNavigation(event)) { 720 - navigationAttempt += 1; 721 - setPendingTreeHref(href); 702 + return ( 703 + <OverviewFileRow 704 + name={entry.name} 705 + href={href} 706 + icon={ 707 + directory ? ( 708 + <Folder class="size-4 flex-shrink-0 fill-current" /> 709 + ) : ( 710 + <File class="size-4 flex-shrink-0" /> 711 + ) 712 + } 713 + lastCommit={entry.last_commit} 714 + lastCommitHref={entry.last_commit ? commitHref(repo, entry.last_commit.hash) : undefined} 715 + class={pendingTreeHref() === href ? 'untangled-file-row-loading' : undefined} 716 + onMouseEnter={handleMouseEnter} 717 + onMouseLeave={handleMouseLeave} 718 + onClick={(event) => { 719 + if (directory) { 720 + if (shouldAnimateNavigation(event)) { 721 + navigationAttempt += 1; 722 + setPendingTreeHref(href); 723 + } 724 + return; 722 725 } 723 - return; 724 - } 725 726 726 - handleFileClick(event, href, repo, treeRef, entryPath); 727 - }} 728 - /> 729 - ); 730 - }} 731 - </For> 732 - </div> 733 - </div> 727 + handleFileClick(event, href, repo, treeRef, entryPath); 728 + }} 729 + /> 730 + ); 731 + }} 732 + </For> 733 + </div> 734 + }> 735 + <Match when={treeQuery.error}> 736 + <div class={cardStyles('p-4')}> 737 + <ErrorState message={getErrorMessage(treeQuery.error)} /> 738 + </div> 739 + </Match> 740 + </Switch> 741 + </Show> 742 + </div> 734 743 735 - <Show when={!isNestedTreePath}> 744 + <Show when={!isNestedTreePath()}> 745 + <Show when={!logQuery.isLoading} fallback={<RepoOverviewSidebarSkeleton />}> 736 746 <div class="min-w-0 md:pt-0 md:pl-6 border-gray-100 dark:border-gray-700 md:border-t-0 md:border-l"> 737 747 <A href={commitsHref(repo, activeRef)} class="flex items-center gap-2 pb-3 font-bold no-underline hover:underline dark:text-white"> 738 748 <List class="size-4" /> 739 749 <span>commits</span> 740 750 <span class="rounded bg-gray-200 px-1 py-0.5 text-sm font-normal dark:bg-gray-700"> 741 - {logQuery.data?.total ?? commits.length} 751 + {logQuery.data?.total ?? commits().length} 742 752 </span> 743 753 </A> 744 - <div class="space-y-6"> 745 - <For each={commits.slice(0, 6)}> 746 - {(commit) => ( 747 - <div> 748 - <div id="commit-message"> 749 - <div class="text-base cursor-pointer"> 750 - <div><div> 754 + <Switch fallback={ 755 + <div class="space-y-6"> 756 + <For each={commits().slice(0, 6)}> 757 + {(commit) => ( 758 + <div> 759 + <div id="commit-message"> 760 + <div class="text-base cursor-pointer"> 761 + <div><div> 762 + <a 763 + href={`/${repo.owner.handle}/${repo.slug}/commit/${commit.this}`} 764 + class="inline no-underline hover:underline dark:text-white" 765 + > 766 + {commit.message.trim().split('\n')[0]} 767 + </a> 768 + </div></div> 769 + </div> 770 + </div> 771 + <div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center flex-wrap"> 772 + <span class="font-mono"> 751 773 <a 752 774 href={`/${repo.owner.handle}/${repo.slug}/commit/${commit.this}`} 753 - class="inline no-underline hover:underline dark:text-white" 775 + class="no-underline hover:underline bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 px-2 py-1 rounded flex items-center gap-2" 754 776 > 755 - {commit.message.trim().split('\n')[0]} 777 + {commit.this.slice(0, 8)} 778 + <GitCommitHorizontal class="w-3 h-3" /> 756 779 </a> 757 - </div></div> 758 - </div> 759 - </div> 760 - <div class="text-xs mt-2 text-gray-500 dark:text-gray-400 flex items-center flex-wrap"> 761 - <span class="font-mono"> 762 - <a 763 - href={`/${repo.owner.handle}/${repo.slug}/commit/${commit.this}`} 764 - class="no-underline hover:underline bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 px-2 py-1 rounded flex items-center gap-2" 765 - > 766 - {commit.this.slice(0, 8)} 767 - <GitCommitHorizontal class="w-3 h-3" /> 768 - </a> 769 - </span> 770 - <span class="mx-1 before:content-['·'] before:select-none" /> 771 - <span class="flex items-center gap-1"> 772 - <Show 773 - when={commit.author?.Email?.startsWith('did:') ? commit.author.Email : undefined} 774 - fallback={<PlaceholderAvatar size="size-6" iconSize="size-4" />} 775 - > 776 - {(did) => <Avatar did={did()} size="size-6" />} 777 - </Show> 778 - <span>{commit.author?.Name || commit.committer?.Name || 'unknown author'}</span> 779 - </span> 780 - <div class="inline-block px-1 select-none after:content-['·']" /> 781 - <time datetime={commit.committer?.When || commit.author?.When || ''}> 782 - {formatRelativeTime(commit.committer?.When || commit.author?.When || '')} 783 - </time> 784 - <Show when={activeRef === currentDefaultBranch}> 780 + </span> 781 + <span class="mx-1 before:content-['·'] before:select-none" /> 782 + <span class="flex items-center gap-1"> 783 + <Show 784 + when={commit.author?.Email?.startsWith('did:') ? commit.author.Email : undefined} 785 + fallback={<PlaceholderAvatar size="size-6" iconSize="size-4" />} 786 + > 787 + {(did) => <Avatar did={did()} size="size-6" />} 788 + </Show> 789 + <span>{commit.author?.Name || commit.committer?.Name || 'unknown author'}</span> 790 + </span> 785 791 <div class="inline-block px-1 select-none after:content-['·']" /> 786 - <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-[2px] inline-flex items-center"> 787 - {currentDefaultBranch} 788 - </span> 789 - </Show> 792 + <time datetime={commit.committer?.When || commit.author?.When || ''}> 793 + {formatRelativeTime(commit.committer?.When || commit.author?.When || '')} 794 + </time> 795 + <Show when={activeRef === currentDefaultBranch}> 796 + <div class="inline-block px-1 select-none after:content-['·']" /> 797 + <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-[2px] inline-flex items-center"> 798 + {currentDefaultBranch} 799 + </span> 800 + </Show> 801 + </div> 790 802 </div> 791 - </div> 792 - )} 793 - </For> 794 - </div> 803 + )} 804 + </For> 805 + </div> 806 + }> 807 + <Match when={logQuery.error}> 808 + <div class={cardStyles('p-4 mb-4')}> 809 + <ErrorState message={getErrorMessage(logQuery.error)} /> 810 + </div> 811 + </Match> 812 + </Switch> 795 813 796 814 <div class="mt-6 border-t border-gray-200 pt-4 dark:border-gray-700"> 797 815 <div class="flex items-center gap-2 pb-2 font-bold"> ··· 823 841 </div> 824 842 </div> 825 843 </Show> 826 - </div> 827 - </section> 844 + </Show> 845 + </div> 846 + </section> 828 847 829 - <Show when={readmeIsAvailable(tree.readme, sortedFiles) && tree.readme}> 830 - {(readme) => ( 831 - <ReadmeCard 832 - filename={readme().filename} 833 - markdown={readme().contents} 834 - repo={repo} 835 - refName={activeRef} 836 - path={treePath} 837 - /> 838 - )} 839 - </Show> 848 + <Show when={tree() && tree()!.readme && readmeIsAvailable(tree()!.readme, sortedFiles())}> 849 + <ReadmeCard 850 + filename={tree()!.readme!.filename} 851 + markdown={tree()!.readme!.contents} 852 + repo={repo} 853 + refName={activeRef} 854 + path={treePath()} 855 + /> 856 + </Show> 840 857 </> 841 858 ); 842 859 })()}
+8 -7
src/pages/search.tsx
··· 17 17 import type { Did, ResourceUri } from '@atcute/lexicons/syntax'; 18 18 import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component, type JSX } from 'solid-js'; 19 19 20 - import { ErrorState, LoadingState, PaginationControls, usePreloader } from '../components/common'; 20 + import { ErrorState, LoadingState, PaginationControls } from '../components/common'; 21 + import { usePreloader, useRepoPreloader, useStringPreloader } from '../lib/preloading'; 21 22 import { RepoStatsList } from '../components/repo'; 22 23 import { getIssueRecord, getIssue } from '../lib/api/issues'; 23 24 import { getPullRecord, getPull } from '../lib/api/pulls'; 24 - import { getRepoByDid, getRepo } from '../lib/api/repos'; 25 - import { getString } from '../lib/api/strings'; 26 - import { issueQueryKey, pullQueryKey, repoQueryKey } from './repo/shared'; 25 + import { getRepoByDid } from '../lib/api/repos'; 26 + import { issueQueryKey, pullQueryKey } from './repo/shared'; 27 27 import { 28 28 ISSUE_COLLECTION, 29 29 } from '../lib/api/constants'; ··· 319 319 }; 320 320 321 321 const ResultTitle: Component<{ hit: SearchHit }> = (props) => { 322 - const preload = usePreloader(); 322 + const preloadRepo = useRepoPreloader(); 323 + const preloadString = useStringPreloader(); 323 324 324 325 const title = createMemo(() => { 325 326 const value = props.hit.value as { ··· 343 344 <A 344 345 href={`/${props.hit.author.handle}/${repoName()}`} 345 346 class="truncate min-w-0" 346 - {...preload(repoQueryKey(props.hit.author.handle, repoName()), () => getRepo(props.hit.author.handle, repoName()))} 347 + {...preloadRepo(props.hit.author.handle, repoName())} 347 348 > 348 349 {props.hit.author.handle}/{repoName()} 349 350 </A> ··· 367 368 <A 368 369 href={`/strings/${props.hit.author.handle || props.hit.author.did}/${props.hit.rkey}`} 369 370 class="truncate min-w-0" 370 - {...preload(['string-detail', props.hit.author.handle || props.hit.author.did, props.hit.rkey], () => getString(props.hit.author.handle || props.hit.author.did, props.hit.rkey))} 371 + {...preloadString(props.hit.author.handle || props.hit.author.did, props.hit.rkey)} 371 372 > 372 373 {props.hit.author.handle || props.hit.author.did}/{title()} 373 374 </A>