csr tangled client in solid-js
15

Configure Feed

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

preloading for issues, prs, and strings

dawn (May 30, 2026, 10:21 PM +0300) 0b35ac8e 73b5dda8

+68 -17
+21 -1
src/components/common.tsx
··· 12 12 } from 'lucide-solid'; 13 13 import { A } from '@solidjs/router'; 14 14 import { Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js'; 15 - import { createQuery } from '@tanstack/solid-query'; 15 + import { createQuery, useQueryClient } from '@tanstack/solid-query'; 16 16 import { resolveAvatarUrl } from '../lib/api/identity'; 17 17 18 18 export const ToggleButton: Component<{ ··· 253 253 254 254 export const textareaStyles = (): string => 255 255 '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'; 256 + 257 + export const usePreloader = () => { 258 + const queryClient = useQueryClient(); 259 + 260 + return (queryKey: readonly unknown[], queryFn: () => Promise<any>, delayMs = 30) => { 261 + let hoverTimer: ReturnType<typeof setTimeout> | undefined; 262 + 263 + const onMouseEnter = () => { 264 + hoverTimer = setTimeout(() => { 265 + queryClient.prefetchQuery({ queryKey, queryFn }); 266 + }, delayMs); 267 + }; 268 + 269 + const onMouseLeave = () => { 270 + clearTimeout(hoverTimer); 271 + }; 272 + 273 + return { onMouseEnter, onMouseLeave }; 274 + }; 275 + };
+4
src/components/repo.tsx
··· 1379 1379 href: string; 1380 1380 meta?: JSX.Element; 1381 1381 children?: JSX.Element; 1382 + onMouseEnter?: JSX.EventHandlerUnion<HTMLAnchorElement, MouseEvent>; 1383 + onMouseLeave?: JSX.EventHandlerUnion<HTMLAnchorElement, MouseEvent>; 1382 1384 }> = (props) => { 1383 1385 return ( 1384 1386 <A 1385 1387 href={props.href} 1386 1388 class="untangled-list-row" 1389 + onMouseEnter={props.onMouseEnter} 1390 + onMouseLeave={props.onMouseLeave} 1387 1391 > 1388 1392 <div class="min-w-0"> 1389 1393 <div class="text-base text-gray-900 dark:text-gray-100">
+9 -5
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 } from '../components/common'; 26 + import { ErrorState, LoadingState, PlaceholderAvatar, SkeletonBlock, textareaStyles, inputStyles, usePreloader } from '../components/common'; 27 27 import { RepoCard, RepoCardSkeleton, StringCardSkeleton } from '../components/repo'; 28 28 import { 29 29 listFollowRecords, ··· 40 40 } from '../lib/api/graph'; 41 41 import { resolveActor, getActorProfile, resolveAvatarUrl, putActorProfile } from '../lib/api/identity'; 42 42 import { listRepoRecords, getRepoByDid, type RepoContext } from '../lib/api/repos'; 43 - import { listStringRecords } from '../lib/api/strings'; 43 + import { listStringRecords, getString } from '../lib/api/strings'; 44 44 import { useAuth } from '../lib/auth'; 45 45 import { listAllAppviewRecords } from '../lib/api/appview'; 46 46 import { formatRelativeTime, getErrorMessage } from '../lib/repo-utils'; ··· 972 972 const [searchParams, setSearchParams] = useSearchParams(); 973 973 const queryClient = useQueryClient(); 974 974 const auth = useAuth(); 975 + const preload = usePreloader(); 975 976 976 977 const navigate = useNavigate(); 977 978 ··· 1550 1551 <div class="border border-gray-200 dark:border-gray-700 rounded-sm"> 1551 1552 <div class="py-4 px-6 rounded bg-white dark:bg-gray-800"> 1552 1553 <div class="font-medium dark:text-white flex gap-2 items-center"> 1553 - <A href={`/strings/${resolvedActor().handle || resolvedActor().did}/${item.rkey}`} class="hover:underline"> 1554 + <A 1555 + href={`/strings/${resolvedActor().handle || resolvedActor().did}/${item.rkey}`} 1556 + class="hover:underline" 1557 + {...preload(['string-detail', resolvedActor().handle || resolvedActor().did, item.rkey], () => getString(resolvedActor().handle || resolvedActor().did, item.rkey))} 1558 + > 1554 1559 {item.value.filename} 1555 - </A> 1556 - </div> 1560 + </A> </div> 1557 1561 {item.value.description && ( 1558 1562 <div class="text-gray-600 dark:text-gray-300 text-sm"> 1559 1563 {item.value.description}
+6 -3
src/pages/repo/issues.tsx
··· 9 9 import { parseAtUri } from '../../lib/api/records'; 10 10 import type { RepoContext } from '../../lib/api/repos'; 11 11 import { useAuth } from '../../lib/auth'; 12 - import { Avatar, ErrorState, PaginationControls, StateBadge, ToggleButton, buttonStyles, cardStyles, inputStyles, textareaStyles } from '../../components/common'; 12 + import { Avatar, ErrorState, PaginationControls, StateBadge, ToggleButton, buttonStyles, cardStyles, inputStyles, textareaStyles, usePreloader } from '../../components/common'; 13 13 import { AtUriPanel, MarkdownBlock, RepoFormSkeleton, RepoListPageSkeleton, RepoListSkeleton, RepoThreadSkeleton, IssueOrPullRow, ThreadCommentView } from '../../components/repo'; 14 14 import { RepoFrame, issueQueryKey, issuesQueryKey, useRepoQuery } from './shared'; 15 15 import { REPO_LIST_PAGE_LIMIT, formatRelativeTime, getErrorMessage, issueHref, parseIntegerSearchParam, uniqueCommenters, saveRecentIssueOrPull } from '../../lib/repo-utils'; ··· 56 56 limit: String(pageLimit()), 57 57 }); 58 58 }; 59 + 60 + const preload = usePreloader(); 59 61 60 62 const issuesQuery = createQuery(() => { 61 63 const repo = repoQuery.data; ··· 166 168 kind="issue" 167 169 author={{ did: issue.author.did, handle: issue.author.handle }} 168 170 createdAt={issue.value.createdAt} 169 - href={issueHref(repoQuery.data!, issue.uri)} 171 + href={issueHref(repoQuery.data!, issue.number || issue.rkey)} 172 + {...preload(issueQueryKey(repoQuery.data!.repoDid, String(issue.number || issue.rkey)), () => getIssue(repoQuery.data!, String(issue.number || issue.rkey)))} 170 173 /> 171 174 )} 172 175 </For> ··· 221 224 try { 222 225 const created = await createIssue(agent, repo, { title: title(), body: body() }); 223 226 await client.invalidateQueries({ queryKey: issuesQueryKey(repo.repoDid) }); 224 - navigate(issueHref(repo, created.uri)); 227 + navigate(issueHref(repo, created.rkey)); 225 228 } catch (cause) { 226 229 setError(getErrorMessage(cause)); 227 230 } finally {
+6 -3
src/pages/repo/pulls.tsx
··· 27 27 import { parseAtUri } from '../../lib/api/records'; 28 28 import { compareBranches, getRepoBranches, type RepoContext, compareFork, listRepoRecords } from '../../lib/api/repos'; 29 29 import { useAuth } from '../../lib/auth'; 30 - import { Avatar, ErrorState, LoadingState, PaginationControls, StateBadge, ToggleButton, buttonStyles, cardStyles, textareaStyles } from '../../components/common'; 30 + import { Avatar, ErrorState, LoadingState, PaginationControls, StateBadge, ToggleButton, buttonStyles, cardStyles, textareaStyles, usePreloader } from '../../components/common'; 31 31 import { AtUriPanel, BranchPill, DiffView, MarkdownBlock, PullComposeSkeleton, PullDiffSkeleton, PullDiffView, RepoListPageSkeleton, RepoListSkeleton, RepoThreadSkeleton, IssueOrPullRow, ThreadCommentView } from '../../components/repo'; 32 32 import { RepoFrame, pullQueryKey, pullsQueryKey, useRepoQuery } from './shared'; 33 33 import { REPO_LIST_PAGE_LIMIT, formatRelativeTime, getErrorMessage, parseIntegerSearchParam, pullHref, uniqueCommenters, saveRecentIssueOrPull } from '../../lib/repo-utils'; ··· 74 74 limit: String(pageLimit()), 75 75 }); 76 76 }; 77 + 78 + const preload = usePreloader(); 77 79 78 80 const pullsQuery = createQuery(() => { 79 81 const repo = repoQuery.data; ··· 190 192 kind="pull" 191 193 author={{ did: pull.author.did, handle: pull.author.handle }} 192 194 createdAt={pull.value.createdAt} 193 - href={pullHref(repoQuery.data!, pull.uri)} 195 + href={pullHref(repoQuery.data!, pull.number || pull.rkey)} 196 + {...preload(pullQueryKey(repoQuery.data!.repoDid, String(pull.number || pull.rkey)), () => getPull(repoQuery.data!, String(pull.number || pull.rkey)))} 194 197 meta={ 195 198 <> 196 199 <span>·</span> ··· 473 476 sourceRepoDid: mode === 'fork' ? (selectedForkDid() as Did) : undefined, 474 477 }); 475 478 await client.invalidateQueries({ queryKey: pullsQueryKey(repo.repoDid) }); 476 - navigate(pullHref(repo, created.uri)); 479 + navigate(pullHref(repo, created.rkey)); 477 480 } catch (cause) { 478 481 setError(getErrorMessage(cause)); 479 482 } finally {
+22 -5
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 } from '../components/common'; 20 + import { ErrorState, LoadingState, PaginationControls, usePreloader } from '../components/common'; 21 21 import { RepoStatsList } from '../components/repo'; 22 - import { getIssueRecord } from '../lib/api/issues'; 23 - import { getPullRecord } from '../lib/api/pulls'; 22 + import { getIssueRecord, getIssue } from '../lib/api/issues'; 23 + import { getPullRecord, getPull } from '../lib/api/pulls'; 24 24 import { getRepoByDid } from '../lib/api/repos'; 25 + import { getString } from '../lib/api/strings'; 26 + import { issueQueryKey, pullQueryKey } from './repo/shared'; 25 27 import { 26 28 ISSUE_COLLECTION, 27 29 } from '../lib/api/constants'; ··· 186 188 queryFn: async () => getRepoByDid(props.repoDid), 187 189 staleTime: 300_000, 188 190 })); 191 + const preload = usePreloader(); 189 192 const href = createMemo(() => { 190 193 const repo = repoQuery.data; 191 194 if (!repo) return undefined; ··· 193 196 const baseHref = fn(repo, props.threadRef); 194 197 return props.commentRef ? `${baseHref}#comment-${props.commentRef}` : baseHref; 195 198 }); 199 + const preloadProps = createMemo(() => { 200 + const repo = repoQuery.data; 201 + if (!repo) return {}; 202 + if (props.kind === 'issue') { 203 + return preload(issueQueryKey(props.repoDid, props.threadRef), () => getIssue(repo, props.threadRef)); 204 + } 205 + return preload(pullQueryKey(props.repoDid, props.threadRef), () => getPull(repo, props.threadRef)); 206 + }); 196 207 197 208 return ( 198 209 <Show when={href()} fallback={<span>{props.children}</span>}> 199 - {(target) => <A href={target()} class="truncate min-w-0">{props.children}</A>} 210 + {(target) => <A href={target()} class="truncate min-w-0" {...preloadProps()}>{props.children}</A>} 200 211 </Show> 201 212 ); 202 213 }; ··· 308 319 }; 309 320 310 321 const ResultTitle: Component<{ hit: SearchHit }> = (props) => { 322 + const preload = usePreloader(); 323 + 311 324 const title = createMemo(() => { 312 325 const value = props.hit.value as { 313 326 title?: string; ··· 347 360 </A> 348 361 </Match> 349 362 <Match when={props.hit.nsid === 'sh.tangled.string'}> 350 - <A href={`/strings/${props.hit.author.handle || props.hit.author.did}/${props.hit.rkey}`} class="truncate min-w-0"> 363 + <A 364 + href={`/strings/${props.hit.author.handle || props.hit.author.did}/${props.hit.rkey}`} 365 + class="truncate min-w-0" 366 + {...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))} 367 + > 351 368 {props.hit.author.handle || props.hit.author.did}/{title()} 352 369 </A> 353 370 </Match>