From da0139ff4e4870d035ca799e364e0c7e424e17d7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 2 Jun 2024 22:46:07 -0500 Subject: [PATCH] Suggestions: add offset based pagination --- src/controllers/api/suggestions.ts | 78 +++++++++++++++++++----------- src/utils/api.ts | 12 +++-- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/controllers/api/suggestions.ts b/src/controllers/api/suggestions.ts index 012244a..620de68 100644 --- a/src/controllers/api/suggestions.ts +++ b/src/controllers/api/suggestions.ts @@ -1,51 +1,75 @@ -import { NStore } from '@nostrify/nostrify'; +import { NostrFilter } from '@nostrify/nostrify'; +import { matchFilter } from 'nostr-tools'; -import { AppController } from '@/app.ts'; +import { AppContext, AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; +import { listPaginationSchema, paginatedList, PaginatedListParams } from '@/utils/api.ts'; import { getTagSet } from '@/utils/tags.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; export const suggestionsV1Controller: AppController = async (c) => { - const store = c.get('store'); const signal = c.req.raw.signal; - const accounts = await renderSuggestedAccounts(store, signal); - - return c.json(accounts); + const params = listPaginationSchema.parse(c.req.query()); + const suggestions = await renderV2Suggestions(c, params, signal); + const accounts = suggestions.map(({ account }) => account); + return paginatedList(c, params, accounts); }; export const suggestionsV2Controller: AppController = async (c) => { - const store = c.get('store'); const signal = c.req.raw.signal; - const accounts = await renderSuggestedAccounts(store, signal); - - const suggestions = accounts.map((account) => ({ - source: 'staff', - account, - })); - - return c.json(suggestions); + const params = listPaginationSchema.parse(c.req.query()); + const suggestions = await renderV2Suggestions(c, params, signal); + return paginatedList(c, params, suggestions); }; -async function renderSuggestedAccounts(store: NStore, signal?: AbortSignal) { - const [follows] = await store.query( - [{ kinds: [3], authors: [Conf.pubkey], limit: 1 }], - { signal }, - ); +async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, signal?: AbortSignal) { + const { offset, limit } = params; - // TODO: pagination - const pubkeys = [...getTagSet(follows?.tags ?? [], 'p')].slice(0, 20); + const store = c.get('store'); + const signer = c.get('signer'); + const pubkey = await signer?.getPublicKey(); + + const filters: NostrFilter[] = [ + { kinds: [3], authors: [Conf.pubkey], limit: 1 }, + ]; + + if (pubkey) { + filters.push({ kinds: [3], authors: [pubkey], limit: 1 }); + filters.push({ kinds: [10000], authors: [pubkey], limit: 1 }); + } + + const events = await store.query(filters, { signal }); + + const [suggestedEvent, followsEvent, mutesEvent] = [ + events.find((event) => matchFilter({ kinds: [3], authors: [Conf.pubkey] }, event)), + pubkey ? events.find((event) => matchFilter({ kinds: [3], authors: [pubkey] }, event)) : undefined, + pubkey ? events.find((event) => matchFilter({ kinds: [10000], authors: [pubkey] }, event)) : undefined, + ]; + + const [suggested, follows, mutes] = [ + getTagSet(suggestedEvent?.tags ?? [], 'p'), + getTagSet(followsEvent?.tags ?? [], 'p'), + getTagSet(mutesEvent?.tags ?? [], 'p'), + ]; + + const ignored = follows.union(mutes); + const pubkeys = suggested.difference(ignored); + + const authors = [...pubkeys].slice(offset, offset + limit); const profiles = await store.query( - [{ kinds: [0], authors: pubkeys, limit: pubkeys.length }], + [{ kinds: [0], authors, limit: authors.length }], { signal }, ) .then((events) => hydrateEvents({ events, store, signal })); - const accounts = await Promise.all(pubkeys.map((pubkey) => { + return Promise.all(authors.map((pubkey) => { const profile = profiles.find((event) => event.pubkey === pubkey); - return profile ? renderAccount(profile) : accountFromPubkey(pubkey); - })); - return accounts.filter(Boolean); + return { + source: suggested.has(pubkey) ? 'staff' : 'global', + account: profile ? renderAccount(profile) : accountFromPubkey(pubkey), + }; + })); } diff --git a/src/utils/api.ts b/src/utils/api.ts index 5ab4cc6..3cc8b7d 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -202,11 +202,16 @@ function buildListLinkHeader(url: string, params: { offset: number; limit: numbe return `<${next}>; rel="next", <${prev}>; rel="prev"`; } +interface PaginatedListParams { + offset: number; + limit: number; +} + /** paginate a list of tags. */ function paginatedList( c: AppContext, - params: { offset: number; limit: number }, - entities: (Entity | undefined)[], + params: PaginatedListParams, + entities: unknown[], headers: HeaderRecord = {}, ) { const link = buildListLinkHeader(c.req.url, params); @@ -217,7 +222,7 @@ function paginatedList( } // Filter out undefined entities. - const results = entities.filter((entity): entity is Entity => Boolean(entity)); + const results = entities.filter(Boolean); return c.json(results, 200, headers); } @@ -255,6 +260,7 @@ export { localRequest, paginated, paginatedList, + type PaginatedListParams, type PaginationParams, paginationSchema, parseBody,