csr tangled client in solid-js
15

Configure Feed

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

add preloading to more places, add one part of the repo preloading

dawn (May 30, 2026, 11:07 PM +0300) 6293bcce 0b35ac8e

+127 -33
+1
src/components/common.tsx
··· 13 13 import { A } from '@solidjs/router'; 14 14 import { Match, Show, Switch, createEffect, createMemo, createSignal, type Component } from 'solid-js'; 15 15 import { createQuery, useQueryClient } from '@tanstack/solid-query'; 16 + export { useQueryClient }; 16 17 import { resolveAvatarUrl } from '../lib/api/identity'; 17 18 18 19 export const ToggleButton: Component<{
+5 -1
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 } from './common'; 35 + import { Avatar, SkeletonBlock, StateBadge, PlaceholderAvatar, buttonStyles, cardStyles, usePreloader } from './common'; 36 + import { repoQueryKey } from '../pages/repo/shared'; 36 37 37 38 marked.setOptions({ 38 39 gfm: true, ··· 1087 1088 href?: string; 1088 1089 noBorder?: boolean; 1089 1090 showOwner?: boolean; 1091 + repoDid?: string; 1090 1092 } 1091 1093 1092 1094 export interface RepoStatsListProps { ··· 1233 1235 1234 1236 export const RepoCard: Component<RepoCardProps> = (props) => { 1235 1237 const linkHref = () => props.href || `/${props.owner}/${props.name}`; 1238 + const preload = usePreloader(); 1236 1239 1237 1240 return ( 1238 1241 <div ··· 1256 1259 'truncate min-w-0 no-underline hover:underline text-black dark:text-white font-bold', 1257 1260 props.compact && 'focus:outline-none' 1258 1261 )} 1262 + {...preload(repoQueryKey(props.owner, props.name), () => getRepo(props.owner, props.name))} 1259 1263 > 1260 1264 <Show when={props.showOwner !== false} fallback={props.name}> 1261 1265 {props.owner}/{props.name}
+5
src/lib/api/keys.ts
··· 1 + export const repoQueryKey = (owner: string, repo: string) => ['repo', owner, repo] as const; 2 + export const issuesQueryKey = (repoDid: string) => ['issues', repoDid] as const; 3 + export const issueQueryKey = (repoDid: string, issueRef: string) => ['issue', repoDid, issueRef] as const; 4 + export const pullsQueryKey = (repoDid: string) => ['pulls', repoDid] as const; 5 + export const pullQueryKey = (repoDid: string, pullRef: string) => ['pull', repoDid, pullRef] as const;
+5 -2
src/lib/api/repos.ts
··· 316 316 const owner = await resolveActor(ownerIdentifier); 317 317 const all = await listRepoRecords(owner); 318 318 const wanted = repoSlug.trim().toLowerCase(); 319 - const match = all.find((record) => { 319 + const matches = all.filter((record) => { 320 320 const name = record.value.name?.trim().toLowerCase(); 321 321 return name === wanted || record.rkey.toLowerCase() === wanted; 322 322 }); 323 323 324 - if (!match) { 324 + if (matches.length === 0) { 325 325 throw new Error(`Repository not found: ${ownerIdentifier}/${repoSlug}`); 326 326 } 327 + 328 + // Prefer a record that doesn't use a localhost knot if multiple match 329 + const match = matches.find((r) => !r.value.knot?.includes('localhost')) || matches[0]; 327 330 328 331 if (!match.value.repoDid) { 329 332 throw new Error(`Repository is missing repoDid metadata`);
+38
src/lib/preloading.tsx
··· 1 + import { useQueryClient } from '@tanstack/solid-query'; 2 + import { usePreloader } from '../components/common'; 3 + import { getRepo } from './api/repos'; 4 + import { getIssue } from './api/issues'; 5 + import { getPull } from './api/pulls'; 6 + import { repoQueryKey, issueQueryKey, pullQueryKey } from './api/keys'; 7 + 8 + export const useIssuePreloader = () => { 9 + const queryClient = useQueryClient(); 10 + const preload = usePreloader(); 11 + 12 + return (repoLabel: string, kind: 'issue' | 'pull', uri: string, number?: number) => { 13 + const [owner, slug] = repoLabel.split('/'); 14 + const issueRef = String(number || uri.split('/').pop()); 15 + 16 + return preload( 17 + [kind, uri, 'prefetch'], 18 + async () => { 19 + const repo = await queryClient.fetchQuery({ 20 + queryKey: repoQueryKey(owner, slug), 21 + queryFn: () => getRepo(owner, slug), 22 + }); 23 + 24 + if (kind === 'issue') { 25 + await queryClient.prefetchQuery({ 26 + queryKey: issueQueryKey(repo.repoDid, issueRef), 27 + queryFn: () => getIssue(repo, issueRef), 28 + }); 29 + } else { 30 + await queryClient.prefetchQuery({ 31 + queryKey: pullQueryKey(repo.repoDid, issueRef), 32 + queryFn: () => getPull(repo, issueRef), 33 + }); 34 + } 35 + }, 36 + ); 37 + }; 38 + };
+63 -22
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 } from '../components/common'; 20 + import { Avatar, ErrorState, LoadingState, StateBadge, usePreloader } 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 { listRepoRecords, type RepoRecord } from '../lib/api/repos'; 24 + import { getRepo, 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 28 import { formatRelativeTime, getErrorMessage, loadRecentRepos, loadRecentIssuesPulls } from '../lib/repo-utils'; 29 + import { repoQueryKey } from '../lib/api/keys'; 28 30 29 31 const SAMPLE_REPOS: Array<{ 30 32 owner: string; ··· 213 215 title: string; 214 216 repoLabel: string; 215 217 href: string; 218 + uri?: string; 216 219 at?: string; 217 220 body?: string; 218 221 actorDid?: string; ··· 320 323 export const HomePage: Component = () => { 321 324 const auth = useAuth(); 322 325 const live = useLiveEvents(); 326 + const issuePreloader = useIssuePreloader(); 323 327 const recentRepos = createMemo(loadRecentRepos); 324 328 const recentIssuesPulls = createMemo(loadRecentIssuesPulls); 325 329 ··· 435 439 ...label, 436 440 repoLabel: repo.title, 437 441 href: eventActivityHref(repo, event), 442 + uri: event.sourceRecord, 438 443 at: event.receivedAt, 439 444 actorDid: currentUserDid(), 440 445 actorHandle: currentUserHandle(), ··· 535 540 > 536 541 <div class="untangled-home-compact-list"> 537 542 <For each={recentIssuesPulls().slice(0, 5)}> 538 - {(item) => ( 539 - <A href={item.href} class="untangled-home-repo-row"> 540 - <div class="text-sm font-semibold text-black dark:text-white truncate"> 541 - {item.title}{' '} 542 - <span class="text-gray-500 dark:text-gray-400 font-normal">#{item.uri.split('/').pop()}</span> 543 - </div> 544 - <div class="mt-2 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 min-w-0"> 545 - <StateBadge state={item.state} kind={item.kind} /> 546 - <span class="truncate min-w-0">{item.author} · {item.repoLabel}</span> 547 - </div> 548 - </A> 549 - )} 543 + {(item) => { 544 + const preloadProps = issuePreloader(item.repoLabel, item.kind, item.uri, item.number); 545 + 546 + return ( 547 + <A href={item.href} class="untangled-home-repo-row" {...preloadProps}> 548 + <div class="text-sm font-semibold text-black dark:text-white truncate"> 549 + {item.title}{' '} 550 + <span class="text-gray-500 dark:text-gray-400 font-normal"> 551 + #{item.uri.split('/').pop()} 552 + </span> 553 + </div> 554 + <div class="mt-2 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 min-w-0"> 555 + <StateBadge state={item.state} kind={item.kind} /> 556 + <span class="truncate min-w-0"> 557 + {item.author} · {item.repoLabel} 558 + </span> 559 + </div> 560 + </A> 561 + ); 562 + }} 550 563 </For> 551 564 </div> 552 565 </Show> ··· 733 746 734 747 const ActivityRow: Component<{ item: HomeActivityItem; index: number }> = (props) => { 735 748 const repoUrl = () => `/${props.item.repoLabel}`; 736 - const actorUrl = () => props.item.actorHandle ? `/${props.item.actorHandle}` : '#'; 749 + const actorUrl = () => (props.item.actorHandle ? `/${props.item.actorHandle}` : '#'); 750 + const preload = usePreloader(); 751 + const issuePreloader = useIssuePreloader(); 737 752 738 753 const actionText = () => { 739 754 if (props.item.kind === 'repo') return 'published repo'; ··· 742 757 return 'updated'; 743 758 }; 744 759 760 + const preloadProps = () => { 761 + if (props.item.kind === 'issue' || props.item.kind === 'pull') { 762 + if (props.item.uri) { 763 + return issuePreloader(props.item.repoLabel, props.item.kind, props.item.uri); 764 + } 765 + } 766 + return {}; 767 + }; 768 + 769 + const preloadRepoProps = () => { 770 + const [owner, slug] = props.item.repoLabel.split('/'); 771 + return preload(repoQueryKey(owner, slug), () => getRepo(owner, slug)); 772 + }; 773 + 745 774 return ( 746 775 <div class="relative"> 747 776 <Show when={props.index > 0}> ··· 753 782 <Show when={props.item.actorDid}> 754 783 <Avatar did={props.item.actorDid!} size="size-5" /> 755 784 </Show> 756 - <A href={actorUrl()} class="font-bold no-underline hover:underline text-black dark:text-white truncate shrink-0 max-w-[8rem] sm:max-w-none"> 785 + <A 786 + href={actorUrl()} 787 + class="font-bold no-underline hover:underline text-black dark:text-white truncate shrink-0 max-w-[8rem] sm:max-w-none" 788 + > 757 789 {props.item.actorHandle || 'user'} 758 790 </A> 759 791 <span class="whitespace-nowrap shrink-0">{actionText()}</span> 760 - <A href={repoUrl()} class="no-underline hover:underline font-bold text-black dark:text-white truncate min-w-0"> 792 + <A 793 + href={repoUrl()} 794 + class="no-underline hover:underline font-bold text-black dark:text-white truncate min-w-0" 795 + {...preloadRepoProps()} 796 + > 761 797 {props.item.repoLabel} 762 798 </A> 763 799 <span class="text-gray-400 dark:text-gray-500 text-xs ml-auto pr-6 shrink-0 whitespace-nowrap"> ··· 771 807 fallback={ 772 808 <div class="py-4 px-6 flex flex-col gap-1 bg-white dark:bg-gray-800"> 773 809 <div class="font-medium text-black dark:text-white flex items-center"> 774 - <Show when={props.item.kind === 'pull'} fallback={<CircleDot class="w-4 h-4 mr-1.5 shrink-0 text-green-600" />}> 810 + <Show 811 + when={props.item.kind === 'pull'} 812 + fallback={<CircleDot class="w-4 h-4 mr-1.5 shrink-0 text-green-600" />} 813 + > 775 814 <GitPullRequest class="w-4 h-4 mr-1.5 shrink-0 text-purple-600" /> 776 815 </Show> 777 - <A href={props.item.href} class="no-underline hover:underline font-medium text-black dark:text-white text-base"> 816 + <A 817 + href={props.item.href} 818 + class="no-underline hover:underline font-medium text-black dark:text-white text-base" 819 + {...preloadProps()} 820 + > 778 821 {props.item.title} 779 822 </A> 780 823 </div> 781 824 <Show when={props.item.body}> 782 - <div class="text-gray-600 dark:text-gray-300 text-sm line-clamp-2 mt-1"> 783 - {props.item.body} 784 - </div> 825 + <div class="text-gray-600 dark:text-gray-300 text-sm line-clamp-2 mt-1">{props.item.body}</div> 785 826 </Show> 786 827 </div> 787 828 }
+1
src/pages/profile.tsx
··· 1511 1511 <RepoCard 1512 1512 owner={repo.owner.handle} 1513 1513 name={repo.slug} 1514 + repoDid={repo.repoDid} 1514 1515 description={repo.record.value.description} 1515 1516 isFork={Boolean(repo.record.value.source)} 1516 1517 showOwner={true}
+2 -5
src/pages/repo/shared.tsx
··· 14 14 import { Avatar, ErrorState, cardStyles } from '../../components/common'; 15 15 import { RepoFrameSkeleton, RepoTabLink } from '../../components/repo'; 16 16 import { getErrorMessage, saveRecentRepo, trimUri } from '../../lib/repo-utils'; 17 + import { repoQueryKey, issuesQueryKey, issueQueryKey, pullsQueryKey, pullQueryKey } from '../../lib/api/keys'; 17 18 18 - export const repoQueryKey = (owner: string, repo: string) => ['repo', owner, repo] as const; 19 - export const issuesQueryKey = (repoDid: string) => ['issues', repoDid] as const; 20 - export const issueQueryKey = (repoDid: string, issueRef: string) => ['issue', repoDid, issueRef] as const; 21 - export const pullsQueryKey = (repoDid: string) => ['pulls', repoDid] as const; 22 - export const pullQueryKey = (repoDid: string, pullRef: string) => ['pull', repoDid, pullRef] as const; 19 + export { repoQueryKey, issuesQueryKey, issueQueryKey, pullsQueryKey, pullQueryKey }; 23 20 24 21 type OptimisticStarState = { state: 'starred'; rkey?: string } | { state: 'unstarred'; rkey: string }; 25 22
+7 -3
src/pages/search.tsx
··· 21 21 import { RepoStatsList } from '../components/repo'; 22 22 import { getIssueRecord, getIssue } from '../lib/api/issues'; 23 23 import { getPullRecord, getPull } from '../lib/api/pulls'; 24 - import { getRepoByDid } from '../lib/api/repos'; 24 + import { getRepoByDid, getRepo } from '../lib/api/repos'; 25 25 import { getString } from '../lib/api/strings'; 26 - import { issueQueryKey, pullQueryKey } from './repo/shared'; 26 + import { issueQueryKey, pullQueryKey, repoQueryKey } from './repo/shared'; 27 27 import { 28 28 ISSUE_COLLECTION, 29 29 } from '../lib/api/constants'; ··· 340 340 <SearchResultIcon hit={props.hit} /> 341 341 <Switch fallback={<span class="truncate min-w-0">{title()}</span>}> 342 342 <Match when={props.hit.nsid === 'sh.tangled.repo'}> 343 - <A href={`/${props.hit.author.handle}/${repoName()}`} class="truncate min-w-0"> 343 + <A 344 + href={`/${props.hit.author.handle}/${repoName()}`} 345 + class="truncate min-w-0" 346 + {...preload(repoQueryKey(props.hit.author.handle, repoName()), () => getRepo(props.hit.author.handle, repoName()))} 347 + > 344 348 {props.hit.author.handle}/{repoName()} 345 349 </A> 346 350 </Match>