diff --git a/src/api/home.ts b/src/api/home.ts index f3174c88..625a56f4 100644 --- a/src/api/home.ts +++ b/src/api/home.ts @@ -1,10 +1,16 @@ +import { z } from '@/deps.ts'; + import { fetchFeed, fetchFollows } from '../client.ts'; +import { toStatus } from '../transmute.ts'; import { getKeys } from '../utils.ts'; import type { Context } from '@/deps.ts'; -import { toStatus } from '../transmute.ts'; +import { LOCAL_DOMAIN } from '../config.ts'; async function homeController(c: Context) { + const since = paramSchema.parse(c.req.query('since')); + const until = paramSchema.parse(c.req.query('until')); + const keys = getKeys(c); if (!keys) { return c.json({ error: 'Unauthorized' }, 401); @@ -15,10 +21,17 @@ async function homeController(c: Context) { return c.json([]); } - const events = await fetchFeed(follows); + const events = await fetchFeed(follows, { since, until }); const statuses = (await Promise.all(events.map(toStatus))).filter(Boolean); - return c.json(statuses); + 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 paramSchema = z.coerce.number().optional().catch(undefined); + export default homeController; diff --git a/src/client.ts b/src/client.ts index 8d5e5d7d..a2c7bc7f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3,6 +3,7 @@ import { Author, RelayPool } from '@/deps.ts'; import { poolRelays } from './config.ts'; import type { Event, SignedEvent } from './event.ts'; +import { eventDateComparator } from './utils.ts'; const pool = new RelayPool(poolRelays); @@ -34,23 +35,40 @@ const fetchFollows = (pubkey: string): Promise | null> => { }); }; -/** Fetch 20 events from people the user follows. */ -function fetchFeed(event3: Event<3>): Promise[]> { +interface PaginationParams { + since?: number; + until?: number; + limit?: number; +} + +/** Fetch events from people the user follows. */ +function fetchFeed(event3: Event<3>, params: PaginationParams = {}): Promise[]> { + const limit = params.limit ?? 20; const authors = event3.tags.filter((tag) => tag[0] === 'p').map((tag) => tag[1]); const results: SignedEvent<1>[] = []; authors.push(event3.pubkey); // see own events in feed return new Promise((resolve) => { pool.subscribe( - [{ authors, kinds: [1], limit: 20 }], + [{ + authors, + kinds: [1], + since: params.since, + until: params.until, + limit, + }], poolRelays, (event: SignedEvent<1> | null) => { if (event) { results.push(event); + + if (results.length >= limit) { + resolve(results.slice(0, limit).sort(eventDateComparator)); + } } }, void 0, - () => resolve(results), + () => resolve(results.sort(eventDateComparator)), { unsubscribeOnEose: true }, ); }); diff --git a/src/utils.ts b/src/utils.ts index 498ee71e..bb7d5436 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,13 @@ import { Context, getPublicKey } from '@/deps.ts'; +import type { Event } from './event.ts'; + +/** Get the current time in Nostr format. */ +const nostrNow = () => Math.floor(new Date().getTime() / 1000); + +/** Pass to sort() to sort events by date. */ +const eventDateComparator = (a: Event, b: Event) => b.created_at - a.created_at; + function getKeys(c: Context) { const auth = c.req.headers.get('Authorization') || ''; @@ -14,4 +22,4 @@ function getKeys(c: Context) { } } -export { getKeys }; +export { eventDateComparator, getKeys, nostrNow };