diff --git a/src/components/common.tsx b/src/components/common.tsx --- a/src/components/common.tsx +++ b/src/components/common.tsx @@ -12,7 +12,7 @@ } from 'lucide-solid'; import { A } from '@solidjs/router'; import { Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js'; -import { createQuery } from '@tanstack/solid-query'; +import { createQuery, useQueryClient } from '@tanstack/solid-query'; import { resolveAvatarUrl } from '../lib/api/identity'; export const ToggleButton: Component<{ @@ -253,3 +253,23 @@ export const textareaStyles = (): string => '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'; + +export const usePreloader = () => { + const queryClient = useQueryClient(); + + return (queryKey: readonly unknown[], queryFn: () => Promise, delayMs = 30) => { + let hoverTimer: ReturnType | undefined; + + const onMouseEnter = () => { + hoverTimer = setTimeout(() => { + queryClient.prefetchQuery({ queryKey, queryFn }); + }, delayMs); + }; + + const onMouseLeave = () => { + clearTimeout(hoverTimer); + }; + + return { onMouseEnter, onMouseLeave }; + }; +}; diff --git a/src/components/repo.tsx b/src/components/repo.tsx --- a/src/components/repo.tsx +++ b/src/components/repo.tsx @@ -1379,11 +1379,15 @@ href: string; meta?: JSX.Element; children?: JSX.Element; + onMouseEnter?: JSX.EventHandlerUnion; + onMouseLeave?: JSX.EventHandlerUnion; }> = (props) => { return (
diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -23,7 +23,7 @@ import { createQuery, useQueryClient } from '@tanstack/solid-query'; import { For, Show, Switch, Match, createMemo, createSignal, createEffect, type Component } from 'solid-js'; import type { Did } from '@atcute/lexicons/syntax'; -import { ErrorState, LoadingState, PlaceholderAvatar, SkeletonBlock, textareaStyles, inputStyles } from '../components/common'; +import { ErrorState, LoadingState, PlaceholderAvatar, SkeletonBlock, textareaStyles, inputStyles, usePreloader } from '../components/common'; import { RepoCard, RepoCardSkeleton, StringCardSkeleton } from '../components/repo'; import { listFollowRecords, @@ -40,7 +40,7 @@ } from '../lib/api/graph'; import { resolveActor, getActorProfile, resolveAvatarUrl, putActorProfile } from '../lib/api/identity'; import { listRepoRecords, getRepoByDid, type RepoContext } from '../lib/api/repos'; -import { listStringRecords } from '../lib/api/strings'; +import { listStringRecords, getString } from '../lib/api/strings'; import { useAuth } from '../lib/auth'; import { listAllAppviewRecords } from '../lib/api/appview'; import { formatRelativeTime, getErrorMessage } from '../lib/repo-utils'; @@ -972,6 +972,7 @@ const [searchParams, setSearchParams] = useSearchParams(); const queryClient = useQueryClient(); const auth = useAuth(); + const preload = usePreloader(); const navigate = useNavigate(); @@ -1550,10 +1551,13 @@
{item.value.description && (
{item.value.description} diff --git a/src/pages/search.tsx b/src/pages/search.tsx --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -17,11 +17,13 @@ import type { Did, ResourceUri } from '@atcute/lexicons/syntax'; import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component, type JSX } from 'solid-js'; -import { ErrorState, LoadingState, PaginationControls } from '../components/common'; +import { ErrorState, LoadingState, PaginationControls, usePreloader } from '../components/common'; import { RepoStatsList } from '../components/repo'; -import { getIssueRecord } from '../lib/api/issues'; -import { getPullRecord } from '../lib/api/pulls'; +import { getIssueRecord, getIssue } from '../lib/api/issues'; +import { getPullRecord, getPull } from '../lib/api/pulls'; import { getRepoByDid } from '../lib/api/repos'; +import { getString } from '../lib/api/strings'; +import { issueQueryKey, pullQueryKey } from './repo/shared'; import { ISSUE_COLLECTION, } from '../lib/api/constants'; @@ -186,6 +188,7 @@ queryFn: async () => getRepoByDid(props.repoDid), staleTime: 300_000, })); + const preload = usePreloader(); const href = createMemo(() => { const repo = repoQuery.data; if (!repo) return undefined; @@ -193,10 +196,18 @@ const baseHref = fn(repo, props.threadRef); return props.commentRef ? `${baseHref}#comment-${props.commentRef}` : baseHref; }); + const preloadProps = createMemo(() => { + const repo = repoQuery.data; + if (!repo) return {}; + if (props.kind === 'issue') { + return preload(issueQueryKey(props.repoDid, props.threadRef), () => getIssue(repo, props.threadRef)); + } + return preload(pullQueryKey(props.repoDid, props.threadRef), () => getPull(repo, props.threadRef)); + }); return ( {props.children}}> - {(target) => {props.children}} + {(target) => {props.children}} ); }; @@ -308,6 +319,8 @@ }; const ResultTitle: Component<{ hit: SearchHit }> = (props) => { + const preload = usePreloader(); + const title = createMemo(() => { const value = props.hit.value as { title?: string; @@ -347,7 +360,11 @@ - + getString(props.hit.author.handle || props.hit.author.did, props.hit.rkey))} + > {props.hit.author.handle || props.hit.author.did}/{title()} diff --git a/src/pages/repo/issues.tsx b/src/pages/repo/issues.tsx --- a/src/pages/repo/issues.tsx +++ b/src/pages/repo/issues.tsx @@ -9,7 +9,7 @@ import { parseAtUri } from '../../lib/api/records'; import type { RepoContext } from '../../lib/api/repos'; import { useAuth } from '../../lib/auth'; -import { Avatar, ErrorState, PaginationControls, StateBadge, ToggleButton, buttonStyles, cardStyles, inputStyles, textareaStyles } from '../../components/common'; +import { Avatar, ErrorState, PaginationControls, StateBadge, ToggleButton, buttonStyles, cardStyles, inputStyles, textareaStyles, usePreloader } from '../../components/common'; import { AtUriPanel, MarkdownBlock, RepoFormSkeleton, RepoListPageSkeleton, RepoListSkeleton, RepoThreadSkeleton, IssueOrPullRow, ThreadCommentView } from '../../components/repo'; import { RepoFrame, issueQueryKey, issuesQueryKey, useRepoQuery } from './shared'; import { REPO_LIST_PAGE_LIMIT, formatRelativeTime, getErrorMessage, issueHref, parseIntegerSearchParam, uniqueCommenters, saveRecentIssueOrPull } from '../../lib/repo-utils'; @@ -56,6 +56,8 @@ limit: String(pageLimit()), }); }; + + const preload = usePreloader(); const issuesQuery = createQuery(() => { const repo = repoQuery.data; @@ -166,7 +168,8 @@ kind="issue" author={{ did: issue.author.did, handle: issue.author.handle }} createdAt={issue.value.createdAt} - href={issueHref(repoQuery.data!, issue.uri)} + href={issueHref(repoQuery.data!, issue.number || issue.rkey)} + {...preload(issueQueryKey(repoQuery.data!.repoDid, String(issue.number || issue.rkey)), () => getIssue(repoQuery.data!, String(issue.number || issue.rkey)))} /> )} @@ -221,7 +224,7 @@ try { const created = await createIssue(agent, repo, { title: title(), body: body() }); await client.invalidateQueries({ queryKey: issuesQueryKey(repo.repoDid) }); - navigate(issueHref(repo, created.uri)); + navigate(issueHref(repo, created.rkey)); } catch (cause) { setError(getErrorMessage(cause)); } finally { diff --git a/src/pages/repo/pulls.tsx b/src/pages/repo/pulls.tsx --- a/src/pages/repo/pulls.tsx +++ b/src/pages/repo/pulls.tsx @@ -27,7 +27,7 @@ import { parseAtUri } from '../../lib/api/records'; import { compareBranches, getRepoBranches, type RepoContext, compareFork, listRepoRecords } from '../../lib/api/repos'; import { useAuth } from '../../lib/auth'; -import { Avatar, ErrorState, LoadingState, PaginationControls, StateBadge, ToggleButton, buttonStyles, cardStyles, textareaStyles } from '../../components/common'; +import { Avatar, ErrorState, LoadingState, PaginationControls, StateBadge, ToggleButton, buttonStyles, cardStyles, textareaStyles, usePreloader } from '../../components/common'; import { AtUriPanel, BranchPill, DiffView, MarkdownBlock, PullComposeSkeleton, PullDiffSkeleton, PullDiffView, RepoListPageSkeleton, RepoListSkeleton, RepoThreadSkeleton, IssueOrPullRow, ThreadCommentView } from '../../components/repo'; import { RepoFrame, pullQueryKey, pullsQueryKey, useRepoQuery } from './shared'; import { REPO_LIST_PAGE_LIMIT, formatRelativeTime, getErrorMessage, parseIntegerSearchParam, pullHref, uniqueCommenters, saveRecentIssueOrPull } from '../../lib/repo-utils'; @@ -74,6 +74,8 @@ limit: String(pageLimit()), }); }; + + const preload = usePreloader(); const pullsQuery = createQuery(() => { const repo = repoQuery.data; @@ -190,7 +192,8 @@ kind="pull" author={{ did: pull.author.did, handle: pull.author.handle }} createdAt={pull.value.createdAt} - href={pullHref(repoQuery.data!, pull.uri)} + href={pullHref(repoQuery.data!, pull.number || pull.rkey)} + {...preload(pullQueryKey(repoQuery.data!.repoDid, String(pull.number || pull.rkey)), () => getPull(repoQuery.data!, String(pull.number || pull.rkey)))} meta={ <> ยท @@ -473,7 +476,7 @@ sourceRepoDid: mode === 'fork' ? (selectedForkDid() as Did) : undefined, }); await client.invalidateQueries({ queryKey: pullsQueryKey(repo.repoDid) }); - navigate(pullHref(repo, created.uri)); + navigate(pullHref(repo, created.rkey)); } catch (cause) { setError(getErrorMessage(cause)); } finally {