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

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2
517 lines · 17.9 KB Raw History
import { infiniteQueryOptions, queryOptions } from "@tanstack/svelte-query";
import { callXrpc } from "$hatk/client";

type Fetch = typeof globalThis.fetch;

// ─── Feeds ──────────────────────────────────────────────────────────

/**
 * Rows per feed page. Keep this as the default for every feed query — the
 * feed query keys deliberately omit `limit`, so an SSR prefetch and the
 * component's own `createQuery` share one cache entry. If they disagree on
 * limit, the larger value silently wins on the first refetch after staleTime.
 *
 * Galleries hydrate with all of their photos, so each row is expensive: a
 * photo-dense feed (e.g. /camera/Ricoh GR II) ran ~900KB at 50.
 */
export const FEED_PAGE_SIZE = 30;

export const recentFeedQuery = (limit = FEED_PAGE_SIZE, f?: Fetch) =>
  queryOptions({
    queryKey: ["getFeed", "recent"],
    queryFn: () => callXrpc("dev.hatk.getFeed", { feed: "recent", limit }, f),
    staleTime: 60_000,
  });

export const followingFeedQuery = (did: string, limit = FEED_PAGE_SIZE, f?: Fetch) =>
  queryOptions({
    queryKey: ["getFeed", "following", did],
    queryFn: () => callXrpc("dev.hatk.getFeed", { feed: "following", actor: did, limit }, f),
    staleTime: 60_000,
  });

export const forYouFeedQuery = (did: string, limit = FEED_PAGE_SIZE, f?: Fetch) =>
  queryOptions({
    queryKey: ["getFeed", "foryou", did],
    queryFn: () => callXrpc("dev.hatk.getFeed", { feed: "foryou", actor: did, limit }, f),
    staleTime: 60_000,
  });

export const actorFeedQuery = (did: string, f?: Fetch) =>
  infiniteQueryOptions({
    queryKey: ["getFeed", "actor", did],
    initialPageParam: undefined as string | undefined,
    queryFn: ({ pageParam }) =>
      callXrpc(
        "dev.hatk.getFeed",
        { feed: "actor", actor: did, limit: 30, ...(pageParam ? { cursor: pageParam } : {}) },
        f,
      ),
    getNextPageParam: (lastPage) => lastPage?.cursor,
    staleTime: 60_000,
  });

export const cameraFeedQuery = (camera: string, limit = FEED_PAGE_SIZE, f?: Fetch) =>
  queryOptions({
    queryKey: ["getFeed", "camera", camera],
    queryFn: () => callXrpc("dev.hatk.getFeed", { feed: "camera", camera, limit }, f),
    staleTime: 60_000,
  });

/** Paged variant for the camera page's grid, mirroring the location feed. */
export const cameraFeedInfiniteQuery = (camera: string, f?: Fetch) =>
  infiniteQueryOptions({
    queryKey: ["getFeed", "camera", "infinite", camera],
    initialPageParam: undefined as string | undefined,
    queryFn: ({ pageParam }) =>
      callXrpc(
        "dev.hatk.getFeed",
        {
          feed: "camera",
          camera,
          limit: FEED_PAGE_SIZE,
          ...(pageParam ? { cursor: pageParam } : {}),
        },
        f,
      ),
    getNextPageParam: (lastPage) => lastPage?.cursor,
    staleTime: 60_000,
  });

/** Paged variant for the hashtag page's grid, mirroring the camera feed. */
export const hashtagFeedInfiniteQuery = (tag: string, f?: Fetch) =>
  infiniteQueryOptions({
    queryKey: ["getFeed", "hashtag", "infinite", tag],
    initialPageParam: undefined as string | undefined,
    queryFn: ({ pageParam }) =>
      callXrpc(
        "dev.hatk.getFeed",
        {
          feed: "hashtag",
          tag,
          limit: FEED_PAGE_SIZE,
          ...(pageParam ? { cursor: pageParam } : {}),
        },
        f,
      ),
    getNextPageParam: (lastPage) => lastPage?.cursor,
    staleTime: 60_000,
  });

export const camerasQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["cameras"],
    queryFn: () =>
      callXrpc("social.grain.unspecced.getCameras", undefined, f).then((r) => r?.cameras ?? []),
    staleTime: 5 * 60_000,
  });

export const locationFeedQuery = (
  location: string,
  name?: string,
  limit = FEED_PAGE_SIZE,
  f?: Fetch,
) =>
  queryOptions({
    queryKey: ["getFeed", "location", name || location],
    queryFn: () =>
      callXrpc(
        "dev.hatk.getFeed",
        { feed: "location", location, ...(name ? { name } : {}), limit },
        f,
      ),
    staleTime: 60_000,
  });

/**
 * Paged variant for the location page's grid. The plain query above still backs
 * the card feed and the SSR prefetch.
 */
export const locationFeedInfiniteQuery = (location: string, name?: string, f?: Fetch) =>
  infiniteQueryOptions({
    queryKey: ["getFeed", "location", "infinite", name || location],
    initialPageParam: undefined as string | undefined,
    queryFn: ({ pageParam }) =>
      callXrpc(
        "dev.hatk.getFeed",
        {
          feed: "location",
          location,
          ...(name ? { name } : {}),
          limit: FEED_PAGE_SIZE,
          ...(pageParam ? { cursor: pageParam } : {}),
        },
        f,
      ),
    getNextPageParam: (lastPage) => lastPage?.cursor,
    staleTime: 60_000,
  });

export const locationsQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["locations"],
    queryFn: () =>
      callXrpc("social.grain.unspecced.getLocations", undefined, f).then((r) => r?.locations ?? []),
    staleTime: 5 * 60_000,
  });

// Every place, carrying only what the index map plots. Separate from
// locationsQuery because the shapes are wanted in different sizes: tiles need
// thumbnails for thirty places, the map needs coordinates for all seven
// hundred, and serving the rich shape for all of them is ~780KB.
export const locationPinsQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["locationPins"],
    queryFn: () =>
      callXrpc("social.grain.unspecced.getLocations", { pins: true }, f).then(
        (r) => r?.locations ?? [],
      ),
    staleTime: 5 * 60_000,
  });

// ─── Favorites ──────────────────────────────────────────────────────

export const actorFavoritesInfiniteQuery = (did: string, f?: Fetch) =>
  infiniteQueryOptions({
    queryKey: ["actorFavorites", did],
    initialPageParam: undefined as string | undefined,
    queryFn: ({ pageParam }) =>
      callXrpc(
        "social.grain.unspecced.getActorFavorites",
        { actor: did, limit: 30, ...(pageParam ? { cursor: pageParam } : {}) },
        f,
      ),
    getNextPageParam: (lastPage) => lastPage?.cursor,
    staleTime: 60_000,
  });

// ─── Stories ────────────────────────────────────────────────────────

export const storyAuthorsQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["storyAuthors"],
    queryFn: () =>
      callXrpc("social.grain.unspecced.getStoryAuthors", undefined, f).then(
        (r) => r?.authors ?? [],
      ),
    staleTime: 60_000,
  });

export const storyQuery = (storyUri: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["getStory", storyUri],
    queryFn: () =>
      callXrpc("social.grain.unspecced.getStory", { story: storyUri }, f).then(
        (r) => r?.story ?? null,
      ),
    staleTime: 60_000,
  });

export const storiesQuery = (did: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["stories", did],
    queryFn: () =>
      callXrpc("social.grain.unspecced.getStories", { actor: did }, f).then(
        (r) => r?.stories ?? [],
      ),
    staleTime: 30_000,
  });

export const storyArchiveQuery = (did: string, f?: Fetch) =>
  infiniteQueryOptions({
    queryKey: ["stories", "archive", did],
    initialPageParam: undefined as string | undefined,
    queryFn: ({ pageParam }) =>
      callXrpc(
        "social.grain.unspecced.getStoryArchive",
        { actor: did, limit: 30, ...(pageParam ? { cursor: pageParam } : {}) },
        f,
      ).then((r) => r ?? { stories: [], cursor: undefined }),
    getNextPageParam: (lastPage) => lastPage?.cursor,
    staleTime: 60_000,
  });

// ─── Preferences ────────────────────────────────────────────────────

export const preferencesQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["preferences"],
    queryFn: () =>
      callXrpc("dev.hatk.getPreferences", undefined, f).then(
        (r: any) => (r?.preferences ?? {}) as Record<string, unknown>,
      ),
    staleTime: 60_000,
  });

// ─── Profile ────────────────────────────────────────────────────────

export const actorProfileQuery = (did: string, viewer?: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["actorProfile", did],
    queryFn: () =>
      callXrpc(
        "social.grain.unspecced.getActorProfile",
        { actor: did, ...(viewer ? { viewer } : {}) },
        f,
      ),
    staleTime: 60_000,
  });

// ─── Followers / Following ───────────────────────────────────────────

export const followersQuery = (did: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["followers", did],
    queryFn: () => callXrpc("social.grain.unspecced.getFollowers", { actor: did }, f),
    staleTime: 60_000,
  });

export const followingQuery = (did: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["following", did],
    queryFn: () => callXrpc("social.grain.unspecced.getFollowing", { actor: did }, f),
    staleTime: 60_000,
  });

// ─── Blocks / Mutes ─────────────────────────────────────────────────

export const blocksQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["blocks"],
    queryFn: () => callXrpc("social.grain.unspecced.getBlocks", {}, f),
    staleTime: 60_000,
  });

export const mutesQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["mutes"],
    queryFn: () => callXrpc("social.grain.unspecced.getMutes", {}, f),
    staleTime: 60_000,
  });

/**
 * Whether the viewer's PDS serves permissioned spaces. A property of their
 * server, so it only changes when that server is redeployed — the server-side
 * probe caches for an hour and this sits in front of it.
 */
export const spaceSupportQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["spaceSupport"],
    queryFn: () => callXrpc("social.grain.unspecced.getSpaceSupport", {}, f),
    staleTime: 5 * 60_000,
  });

/**
 * The viewer's own private galleries. Read from their PDS on every call —
 * there is no index behind this, so it is not held stale for long.
 */
export const privateGalleriesQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["privateGalleries"],
    queryFn: () => callXrpc("social.grain.unspecced.listPrivateGalleries", {}, f),
    staleTime: 30_000,
    retry: false,
  });

/** Who a private gallery is shared with. Answerable only for its author. */
export const spaceMembersQuery = (space: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["spaceMembers", space],
    queryFn: () => callXrpc("social.grain.unspecced.getSpaceMembers", { space }, f),
    staleTime: 30_000,
    retry: false,
  });

/** Private galleries shared with the viewer by someone else. */
export const sharedGalleriesQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["sharedGalleries"],
    queryFn: () => callXrpc("social.grain.unspecced.listSharedGalleries", {}, f),
    staleTime: 30_000,
    retry: false,
  });

/**
 * A gallery living in a permissioned space. Assembled from member repos on
 * every request rather than read from the index — there is no index for it —
 * so this is deliberately not prefetched or held stale.
 */
export const privateGalleryQuery = (space: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["privateGallery", space],
    queryFn: () => callXrpc("social.grain.unspecced.getPrivateGallery", { space }, f),
    staleTime: 30_000,
    retry: false,
  });

/**
 * A group's pool: every member's galleries, read from their repos through
 * the group's space on every call. No index stands behind it — nothing in a
 * space reaches a firehose — and a viewer who is not a member gets an error
 * rather than an empty pool, so this never retries into a refusal.
 */
export const poolGalleriesQuery = (group: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["poolGalleries", group],
    queryFn: () => callXrpc("social.grain.unspecced.listPoolGalleries", { group }, f),
    staleTime: 30_000,
    retry: false,
  });

/**
 * Every pool the viewer is in, as one feed. Per viewer, per request — there is
 * no index behind it and no cursor to page with, so it asks for a page's worth
 * and stops.
 */
export const poolFeedQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["poolFeed"],
    queryFn: () => callXrpc("social.grain.unspecced.listPoolFeed", {}, f),
    staleTime: 30_000,
    retry: false,
  });

/** One gallery from a group's pool. */
export const poolGalleryQuery = (group: string, did: string, rkey: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["poolGallery", group, did, rkey],
    queryFn: () => callXrpc("social.grain.unspecced.getPoolGallery", { group, did, rkey }, f),
    staleTime: 30_000,
    retry: false,
  });

export const knownFollowersQuery = (did: string, viewer: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["knownFollowers", did, viewer],
    queryFn: () => callXrpc("social.grain.unspecced.getKnownFollowers", { actor: did, viewer }, f),
    staleTime: 60_000,
  });

// ─── Search ─────────────────────────────────────────────────────────

export const searchGalleriesQuery = (q: string) =>
  queryOptions({
    queryKey: ["search", "galleries", q],
    queryFn: () => callXrpc("social.grain.unspecced.searchGalleries", { q, limit: 30 }),
    enabled: !!q,
    staleTime: 60_000,
  });

export const searchProfilesQuery = (q: string) =>
  queryOptions({
    queryKey: ["search", "people", q],
    queryFn: () => callXrpc("social.grain.unspecced.searchProfiles", { q, limit: 30 }),
    enabled: !!q,
    staleTime: 60_000,
  });

// ─── Gallery ────────────────────────────────────────────────────────

export const galleryQuery = (galleryUri: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["getGallery", galleryUri],
    queryFn: () =>
      callXrpc("social.grain.unspecced.getGallery", { gallery: galleryUri }, f).then(
        (r) => r.gallery,
      ),
    staleTime: 60_000,
  });

export const galleryFavoritesQuery = (galleryUri: string, viewer?: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["galleryFavorites", galleryUri],
    queryFn: () =>
      callXrpc(
        "social.grain.unspecced.getGalleryFavorites",
        { gallery: galleryUri, ...(viewer ? { viewer } : {}) },
        f,
      ),
    staleTime: 60_000,
  });

// ─── Gallery Thread (Comments) ──────────────────────────────────────

export const commentThreadQuery = (subjectUri: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["getCommentThread", subjectUri],
    queryFn: () => callXrpc("social.grain.unspecced.getCommentThread", { subject: subjectUri }, f),
    staleTime: 30_000,
  });

// ─── Notifications ─────────────────────────────────────────────────

export const notificationsQuery = (viewer: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["notifications", viewer],
    queryFn: () => callXrpc("social.grain.unspecced.getNotifications", { limit: 100 }, f),
    staleTime: 60_000,
  });

export const unseenNotificationCountQuery = (viewer: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["unseenNotificationCount", viewer],
    queryFn: () =>
      callXrpc("social.grain.unspecced.getNotifications", { countOnly: true }, f).then(
        (r: any) => (r?.unseenCount ?? 0) as number,
      ),
    staleTime: 60_000,
  });

// ─── Groups ─────────────────────────────────────────────────────────

export const groupQuery = (actor: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["group", actor],
    queryFn: () => callXrpc("social.grain.unspecced.getGroup", { actor }, f),
    staleTime: 30_000,
  });

/** The group host this Grain starts groups on; empty when it starts none. */
export const groupHostQuery = (f?: Fetch) =>
  queryOptions({
    queryKey: ["groupHost"],
    queryFn: () => callXrpc("social.grain.unspecced.getGroupHost", {}, f),
    staleTime: 60 * 60_000,
  });

/** Every group; with `gallery`, each carries the viewer's standing for it. */
export const groupsQuery = (gallery?: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["groups", gallery ?? ""],
    queryFn: () =>
      callXrpc("social.grain.unspecced.listGroups", gallery ? { gallery } : undefined, f).then(
        (r) => r?.groups ?? [],
      ),
    staleTime: 30_000,
  });

export const groupFeedQuery = (did: string, f?: Fetch) =>
  infiniteQueryOptions({
    queryKey: ["getFeed", "group", did],
    initialPageParam: undefined as string | undefined,
    queryFn: ({ pageParam }) =>
      callXrpc(
        "dev.hatk.getFeed",
        { feed: "group", group: did, limit: 30, ...(pageParam ? { cursor: pageParam } : {}) },
        f,
      ),
    getNextPageParam: (lastPage) => lastPage?.cursor,
    staleTime: 60_000,
  });

export const groupsFeedQuery = (limit = FEED_PAGE_SIZE, f?: Fetch) =>
  queryOptions({
    queryKey: ["getFeed", "groups"],
    queryFn: () => callXrpc("dev.hatk.getFeed", { feed: "groups", limit }, f),
    staleTime: 60_000,
  });

export const groupSubmissionsQuery = (group: string, f?: Fetch) =>
  queryOptions({
    queryKey: ["groupSubmissions", group],
    queryFn: () =>
      callXrpc("social.grain.unspecced.getGroupSubmissions", { group }, f).then(
        (r) => r?.submissions ?? [],
      ),
    staleTime: 15_000,
  });