From 8a62cb604d562cc6f36aac5874960c1d2292fef2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 9 Jun 2023 19:22:38 -0500 Subject: [PATCH] Support pagination on user profile --- src/controllers/api/accounts.ts | 8 +++++--- src/controllers/api/timelines.ts | 20 ++++++------------ src/utils.ts | 35 ++++++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 8a0e1bc..d4831d9 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -2,7 +2,7 @@ import { type AppController } from '@/app.ts'; import { z } from '@/deps.ts'; import { getAuthor, getFilter, getFollows } from '@/client.ts'; import { toAccount, toStatus } from '@/transmute.ts'; -import { eventDateComparator, lookupAccount } from '@/utils.ts'; +import { buildLinkHeader, eventDateComparator, lookupAccount, paginationSchema } from '@/utils.ts'; const verifyCredentialsController: AppController = async (c) => { const pubkey = c.get('pubkey')!; @@ -91,6 +91,7 @@ const accountStatusesQuerySchema = z.object({ const accountStatusesController: AppController = async (c) => { const pubkey = c.req.param('pubkey'); + const { since, until } = paginationSchema.parse(c.req.query()); const { pinned, limit } = accountStatusesQuerySchema.parse(c.req.query()); // Nostr doesn't support pinned statuses. @@ -98,10 +99,11 @@ const accountStatusesController: AppController = async (c) => { return c.json([]); } - const events = (await getFilter({ authors: [pubkey], kinds: [1], limit })).sort(eventDateComparator); + const events = (await getFilter({ authors: [pubkey], kinds: [1], since, until, limit })).sort(eventDateComparator); const statuses = await Promise.all(events.map((event) => toStatus(event))); - return c.json(statuses); + const link = buildLinkHeader(c.req.url, events); + return c.json(statuses, 200, link ? { link } : undefined); }; export { diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 8e10907..dfd3197 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -1,13 +1,11 @@ -import { type AppController } from '@/app.ts'; import { getFeed, getFollows } from '@/client.ts'; -import { LOCAL_DOMAIN } from '@/config.ts'; -import { z } from '@/deps.ts'; import { toStatus } from '@/transmute.ts'; +import { buildLinkHeader, paginationSchema } from '@/utils.ts'; + +import type { AppController } from '@/app.ts'; const homeController: AppController = async (c) => { - const since = paramSchema.parse(c.req.query('since')); - const until = paramSchema.parse(c.req.query('until')); - + const { since, until } = paginationSchema.parse(c.req.query()); const pubkey = c.get('pubkey')!; const follows = await getFollows(pubkey); @@ -22,14 +20,8 @@ const homeController: AppController = async (c) => { const statuses = (await Promise.all(events.map(toStatus))).filter(Boolean); - const next = `${LOCAL_DOMAIN}/api/v1/timelines/home?until=${events[events.length - 1].created_at}`; - const prev = `${LOCAL_DOMAIN}/api/v1/timelines/home?since=${events[0].created_at}`; - - return c.json(statuses, 200, { - link: `<${next}>; rel="next", <${prev}>; rel="prev"`, - }); + const link = buildLinkHeader(c.req.url, events); + return c.json(statuses, 200, link ? { link } : undefined); }; -const paramSchema = z.coerce.number().optional().catch(undefined); - export { homeController }; diff --git a/src/utils.ts b/src/utils.ts index 62c2ff7..b47cd73 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import { getAuthor } from '@/client.ts'; -import { nip19, parseFormData } from '@/deps.ts'; +import { LOCAL_DOMAIN } from '@/config.ts'; +import { nip19, parseFormData, z } from '@/deps.ts'; import { type Event } from '@/event.ts'; import { lookupNip05Cached } from '@/nip05.ts'; @@ -75,4 +76,34 @@ async function parseBody(req: Request): Promise { } } -export { bech32ToPubkey, eventDateComparator, lookupAccount, type Nip05, nostrNow, parseBody, parseNip05 }; +const paginationSchema = z.object({ + since: z.coerce.number().optional().catch(undefined), + until: z.coerce.number().optional().catch(undefined), +}); + +function buildLinkHeader(url: string, events: Event[]): string | undefined { + if (!events.length) return; + const firstEvent = events[0]; + const lastEvent = events[events.length - 1]; + + const { pathname, search } = new URL(url); + const next = new URL(pathname + search, LOCAL_DOMAIN); + const prev = new URL(pathname + search, LOCAL_DOMAIN); + + next.searchParams.set('until', String(lastEvent.created_at)); + prev.searchParams.set('since', String(firstEvent.created_at)); + + return `<${next}>; rel="next", <${prev}>; rel="prev"`; +} + +export { + bech32ToPubkey, + buildLinkHeader, + eventDateComparator, + lookupAccount, + type Nip05, + nostrNow, + paginationSchema, + parseBody, + parseNip05, +};