~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
215 lines · 8.4 KB Raw History
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Outlet, useRouterState } from '@tanstack/react-router';
import { LockIcon } from 'lucide-react';
import { Avatar } from '#/components/atoms/avatar.jsx';
import { Link } from '#/components/atoms/link.jsx';
import { PageNotice } from '#/components/molecules/page-notice.jsx';
import { RepoListSkeleton } from '#/components/molecules/repo-list-skeleton.jsx';
import { RepoPageSkeleton } from '#/components/molecules/repo-page-skeleton.jsx';
import { RepoTabs } from '#/components/molecules/repo-tabs.jsx';
import { SessionControls } from '#/components/molecules/session-controls.jsx';
import { ThemeMenu } from '#/components/molecules/theme-menu.jsx';
import { loadAccount, primeOnce } from '#/lib/account.js';
import { initial, loadAuthors, owner } from '#/lib/authors.js';
import { identicon } from '#/lib/avatars.js';
import {
  account as accountInfo,
  loadCapabilities,
  repoRecord,
} from '#/lib/git.js';
import { useSpace } from '#/lib/navigation.jsx';
import { loadThemes } from '#/lib/theme.js';
import { cn } from '#/lib/utils.js';

/**
 * Which section of a repository a path names. The tab bar marks it, and each
 * page belongs to exactly one: a commit is part of the history, a run is part
 * of the checks, a file is part of the code.
 * @param {string|undefined} kind - the path segment after the repository
 * @returns {string}
 */
function sectionOf(kind) {
  if (kind === 'commits' || kind === 'commit') return 'commits';
  if (kind === 'checks') return 'checks';
  if (kind === 'packages') return 'packages';
  if (kind === 'pulls' || kind === 'pull') return 'pulls';
  if (kind === 'issues') return 'issues';
  if (kind === 'settings') return 'settings';
  return 'code';
}

/**
 * The shell: the bar, the repository's sections, and the account gate every
 * screen waits on. The screen itself is the route's, rendered where the
 * shell leaves room for it.
 */
export function App() {
  const path = useRouterState({ select: (state) => state.location.pathname });
  const queryClient = useQueryClient();
  const segments = path.split('/').filter(Boolean).map(decodeURIComponent);
  const space = useSpace();

  // The previous visit's account and repository rows, read synchronously.
  // With them in hand every screen renders real content on the first frame,
  // and the fresh reads below correct it in place.
  const primed = primeOnce();

  // Discovery names the account and its PDS, and the repository list has to
  // be in hand before any screen renders: every route below the list reads
  // its refs out of that record.
  const { data: account, error } = useQuery({
    queryKey: ['account'],
    queryFn: () => loadAccount(queryClient),
    staleTime: Number.POSITIVE_INFINITY,
  });
  const ready = Boolean(account) || Boolean(primed);

  // The repository in view, and which of its sections. Both come from the
  // path, so every page carries the same heading and the same tabs without
  // passing them down.
  const repo = ready && segments.length > 0 ? segments[0] : '';
  const known = Boolean(repo) && Boolean(repoRecord(repo, space));
  const section = sectionOf(segments[1]);

  // The capability probes decide which clone URLs and which file links a
  // screen offers. That decorates a page rather than filling it, so the
  // probes run behind the first paint; the answers appear through this
  // query's re-render. The probe asks about a repository, so the list
  // comes first.
  useQuery({
    queryKey: ['capabilities'],
    enabled: ready,
    queryFn: loadCapabilities,
    staleTime: Number.POSITIVE_INFINITY,
  });

  // The palettes the account publishes, added to the ones the page ships.
  // A theme is decoration, so this runs beside the screen; the remembered
  // choice is already painted below.
  useQuery({
    queryKey: ['themes'],
    enabled: ready,
    queryFn: loadThemes,
    staleTime: Number.POSITIVE_INFINITY,
  });

  // Which atproto identity each commit author is, where the account claims
  // one. This runs beside the screen rather than before it; the pages that
  // show authors wait for it themselves, and the last visit's answer is
  // already in hand, so a face is on the first frame either way.
  useQuery({
    queryKey: ['authors'],
    enabled: ready && known,
    queryFn: loadAuthors,
    staleTime: Number.POSITIVE_INFINITY,
  });

  return (
    <>
      {/* The window is the app: the bar holds its place and each pane below
          takes its own scroll, so nothing but the reading area ever moves. */}
      <div className="flex h-dvh flex-col overflow-hidden">
        {/* Where the reader is, and nothing else. Which section is open and
            what that section is looking at each get a band of their own
            below, so no one bar carries three different jobs. */}
        <header
          className={cn(
            'shrink-0 bg-muted',
            // The sections band below carries the rule when there is one.
            !known && 'border-border border-b',
          )}
        >
          <div className="flex h-14 items-center gap-3 px-(--gutter)">
            {ready && (
              <Link
                href="/"
                title={accountInfo.handle}
                className="flex shrink-0"
              >
                <Avatar
                  src={owner()?.avatar || identicon(accountInfo.did)}
                  fallback={
                    <span className="text-[10px] text-faint">
                      {initial(owner()?.displayName || accountInfo.handle)}
                    </span>
                  }
                  className="size-7"
                />
              </Link>
            )}
            <nav className="flex min-w-0 shrink items-center gap-2 text-[15px]">
              {/* On a narrow window the repository's own name is the one that
                  has to survive. The avatar carries the way back to the list
                  at every width. */}
              <Link
                href="/"
                className={cn(
                  'truncate no-underline hover:underline',
                  known
                    ? 'hidden text-muted-foreground sm:inline'
                    : 'font-semibold text-foreground',
                )}
              >
                {accountInfo.handle || 'Repositories'}
              </Link>
              {known && (
                <>
                  <span className="hidden text-faint sm:inline">/</span>
                  <Link
                    href={`/${encodeURIComponent(repo)}`}
                    className="flex min-w-0 items-center gap-1.5 font-semibold text-foreground no-underline hover:underline"
                  >
                    {space && (
                      <LockIcon
                        className="size-3.5 shrink-0 text-faint"
                        aria-label="Private"
                      />
                    )}
                    <span className="truncate">{repo}</span>
                  </Link>
                </>
              )}
            </nav>
            <div className="ml-auto flex shrink-0 items-center gap-2 pl-2">
              <SessionControls />
              <span className="h-4 w-px bg-border" aria-hidden="true" />
              <ThemeMenu />
            </div>
          </div>
        </header>

        {/* The repository's sections, with the one action that belongs to the
            repository rather than to any one of them. */}
        {known && (
          <div className="shrink-0 border-border border-b bg-muted">
            <div className="flex items-end gap-3 px-(--gutter)">
              <RepoTabs repo={repo} active={section} space={space} />
            </div>
          </div>
        )}

        {/* Every screen holds the window and scrolls inside itself, so the
            bar and whatever each page fixes above its list stay put. */}
        <main className="flex min-h-0 flex-1 flex-col overflow-hidden">
          {path === '/callback' ? (
            <Outlet />
          ) : error && !ready ? (
            <PageNotice tone="error">
              Could not reach the PDS. {error.message}
            </PageNotice>
          ) : !ready ? (
            // The URL says which screen is coming, so the placeholder can hold
            // its shape rather than a bare block.
            segments.length === 0 ? (
              <RepoListSkeleton />
            ) : (
              <RepoPageSkeleton />
            )
          ) : (
            <Outlet />
          )}
        </main>
      </div>
    </>
  );
}