csr tangled client in solid-js
0

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

+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>