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, ), 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, });