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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
102 lines · 3.3 KB Raw History
import { useQuery, useQueryClient } from '@tanstack/react-query';
import {
  useNavigate as useRouterNavigate,
  useRouterState,
} from '@tanstack/react-router';
import { useCallback } from 'react';
import { loadAccount } from '#/lib/account.js';
import { loadRepos, repoRecord, reposKey, spaceOf } from '#/lib/git.js';

/**
 * The router's location and navigation, in the shape the pages take: a path
 * string, and a function that goes to one.
 *
 * The pages build their own addresses, since a pull request's address holds
 * a DID and a branch name with slashes in it, and a route pattern cannot say
 * where one ends. So a link and a navigation take a path, and the router
 * matches it.
 */
export function useLocation() {
  const path = useRouterState({ select: (state) => state.location.pathname });
  const navigate = useNavigate();
  return { path, navigate };
}

/** @returns {(to: string, search?: Record<string, unknown>) => void} */
export function useNavigate() {
  const navigate = useRouterNavigate();
  return useCallback(
    (to, search) => {
      navigate(search ? { to, search } : { to });
      window.scrollTo(0, 0);
    },
    [navigate],
  );
}

/**
 * The space a repository page is reading from, from its `?space=`. A private
 * repository is addressed by name and space together; the space is kept on
 * the address as a query parameter, so a page inside the repository carries
 * it by the router's retainSearchParams middleware.
 * @returns {string} - the space URI, '' for a public repository
 */
export function useSpace() {
  const repo = useRouterState({
    select: (state) => state.location.pathname.split('/').filter(Boolean)[0],
  });
  return repo ? spaceOf(decodeURIComponent(repo)) : '';
}

/**
 * The fragment on the address now, and a way to set one.
 *
 * A fragment names a place on the page the reader is already on, so setting
 * one replaces the history entry rather than adding to it, and neither the
 * router nor the browser moves the page to it.
 * @returns {[string, (hash: string) => void]}
 */
export function useHash() {
  const hash = useRouterState({ select: (state) => state.location.hash });
  const navigate = useRouterNavigate();
  const setHash = useCallback(
    (next) => {
      navigate({
        hash: next,
        replace: true,
        resetScroll: false,
        hashScrollIntoView: false,
      });
    },
    [navigate],
  );
  return [hash, setHash];
}

/**
 * The repository record a route names, subscribed to the listing so the
 * route re-renders when a fresh listing lands. A direct load paints from the
 * last visit's rows, which may not hold a repository made since; `settled`
 * says whether the fresh listing has been read, so a route can hold its
 * placeholder rather than saying "no such repository" too soon.
 * @param {string} repo
 * @param {string} [space]
 * @returns {{record: Record<string, unknown>|null, settled: boolean}}
 */
export function useRepoRecord(repo, space) {
  const queryClient = useQueryClient();
  const account = useQuery({
    queryKey: ['account'],
    queryFn: () => loadAccount(queryClient),
    staleTime: Number.POSITIVE_INFINITY,
  });
  useQuery({
    queryKey: [reposKey()],
    queryFn: loadRepos,
    staleTime: Number.POSITIVE_INFINITY,
  });
  return {
    record: repoRecord(repo, space || spaceOf(repo)),
    settled: account.isSuccess || account.isError,
  };
}