From 62482722433af110e3f0a20df8ad5de2b45b15a7 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 15 Apr 2024 17:10:25 -0300 Subject: [PATCH] feat: add quote repost --- src/controllers/api/accounts.ts | 4 +-- src/controllers/api/search.ts | 2 +- src/controllers/api/statuses.ts | 24 +++++++-------- src/controllers/api/streaming.ts | 2 +- src/controllers/api/timelines.ts | 4 +-- src/interfaces/DittoEvent.ts | 1 + src/storages/hydrate.ts | 48 +++++++++++++++++++++++++++++ src/views.ts | 2 +- src/views/mastodon/notifications.ts | 2 +- src/views/mastodon/statuses.ts | 17 ++++++++-- 10 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 852c2b6..7762783 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -163,7 +163,7 @@ const accountStatusesController: AppController = async (c) => { return events; }); - const statuses = await Promise.all(events.map((event) => renderStatus(event, c.get('pubkey')))); + const statuses = await Promise.all(events.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); return paginated(c, events, statuses); }; @@ -310,7 +310,7 @@ const favouritesController: AppController = async (c) => { hydrateEvents({ events, relations: ['author', 'event_stats', 'author_stats'], storage: eventsDB, signal }) ); - const statuses = await Promise.all(events1.map((event) => renderStatus(event, c.get('pubkey')))); + const statuses = await Promise.all(events1.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); return paginated(c, events1, statuses); }; diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index ec85192..fba315a 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -52,7 +52,7 @@ const searchController: AppController = async (c) => { Promise.all( results .filter((event) => event.kind === 1) - .map((event) => renderStatus(event, c.get('pubkey'))), + .map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') })), ), ]); diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 7e4a4c3..45f1209 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -40,12 +40,12 @@ const statusController: AppController = async (c) => { const event = await getEvent(id, { kind: 1, - relations: ['author', 'event_stats', 'author_stats'], + relations: ['author', 'event_stats', 'author_stats', 'quote_repost'], signal: AbortSignal.timeout(1500), }); if (event) { - return c.json(await renderStatus(event, c.get('pubkey'))); + return c.json(await renderStatus(event, { viewerPubkey: c.get('pubkey') })); } return c.json({ error: 'Event not found.' }, 404); @@ -130,7 +130,7 @@ const createStatusController: AppController = async (c) => { }, c); const author = await getAuthor(event.pubkey); - return c.json(await renderStatus({ ...event, author }, c.get('pubkey'))); + return c.json(await renderStatus({ ...event, author }, { viewerPubkey: c.get('pubkey') })); }; const deleteStatusController: AppController = async (c) => { @@ -147,7 +147,7 @@ const deleteStatusController: AppController = async (c) => { }, c); const author = await getAuthor(event.pubkey); - return c.json(await renderStatus({ ...event, author }, pubkey)); + return c.json(await renderStatus({ ...event, author }, { viewerPubkey: pubkey })); } else { return c.json({ error: 'Unauthorized' }, 403); } @@ -161,7 +161,7 @@ const contextController: AppController = async (c) => { const event = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] }); async function renderStatuses(events: NostrEvent[]) { - const statuses = await Promise.all(events.map((event) => renderStatus(event, c.get('pubkey')))); + const statuses = await Promise.all(events.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); return statuses.filter(Boolean); } @@ -191,7 +191,7 @@ const favouriteController: AppController = async (c) => { ], }, c); - const status = await renderStatus(target, c.get('pubkey')); + const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') }); if (status) { status.favourited = true; @@ -259,7 +259,7 @@ const unreblogStatusController: AppController = async (c) => { tags: [['e', repostedEvent.id]], }, c); - return c.json(await renderStatus(event)); + return c.json(await renderStatus(event, {})); }; const rebloggedByController: AppController = (c) => { @@ -285,7 +285,7 @@ const bookmarkController: AppController = async (c) => { c, ); - const status = await renderStatus(event, pubkey); + const status = await renderStatus(event, { viewerPubkey: pubkey }); if (status) { status.bookmarked = true; } @@ -312,7 +312,7 @@ const unbookmarkController: AppController = async (c) => { c, ); - const status = await renderStatus(event, pubkey); + const status = await renderStatus(event, { viewerPubkey: pubkey }); if (status) { status.bookmarked = false; } @@ -339,7 +339,7 @@ const pinController: AppController = async (c) => { c, ); - const status = await renderStatus(event, pubkey); + const status = await renderStatus(event, { viewerPubkey: pubkey }); if (status) { status.pinned = true; } @@ -368,7 +368,7 @@ const unpinController: AppController = async (c) => { c, ); - const status = await renderStatus(event, pubkey); + const status = await renderStatus(event, { viewerPubkey: pubkey }); if (status) { status.pinned = false; } @@ -411,7 +411,7 @@ const zapController: AppController = async (c) => { ], }, c); - const status = await renderStatus(target, c.get('pubkey')); + const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') }); status.zapped = true; return c.json(status); diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 643ac01..8239645 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -79,7 +79,7 @@ const streamingController: AppController = (c) => { } continue; } - const status = await renderStatus(event, pubkey); + const status = await renderStatus(event, { viewerPubkey: pubkey }); if (status) { send('update', status); } diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index c54abd4..467b115 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -51,7 +51,7 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { .then((events) => hydrateEvents({ events, - relations: ['author', 'author_stats', 'event_stats', 'repost'], + relations: ['author', 'author_stats', 'event_stats', 'repost', 'quote_repost'], storage: eventsDB, signal, }) @@ -65,7 +65,7 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) { if (event.kind === 6) { return renderReblog(event); } - return renderStatus(event, c.get('pubkey')); + return renderStatus(event, { viewerPubkey: c.get('pubkey') }); }))).filter((boolean) => boolean); if (!statuses.length) { diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index ca38a42..c1d85ae 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -23,4 +23,5 @@ export interface DittoEvent extends NostrEvent { d_author?: DittoEvent; user?: DittoEvent; repost?: NostrEvent; + quote_repost?: NostrEvent; } diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 7e2b1da..be3b8bd 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -36,6 +36,9 @@ async function hydrateEvents(opts: HydrateEventOpts): Promise { case 'repost': await hydrateRepostEvents({ events, storage, signal }); break; + case 'quote_repost': + await hydrateQuoteRepostEvents({ events, storage, signal }); + break; } } @@ -144,6 +147,51 @@ async function hydrateRepostEvents(opts: Omit): P return events; } +async function hydrateQuoteRepostEvents(opts: Omit): Promise { + const { events, storage, signal } = opts; + + const results = await storage.query([{ + kinds: [1], + ids: events.map((event) => { + if (event.kind === 1) { + const originalPostId = event.tags.find(([name]) => name === 'q')?.[1]; + if (!originalPostId) return event.id; + else return originalPostId; + } + return event.id; + }), + }], { signal }); + + for (const event of events) { + if (event.kind === 1) { + const originalPostId = event.tags.find(([name]) => name === 'q')?.[1]; + if (!originalPostId) continue; + + const originalPostEvent = events.find((event) => event.id === originalPostId); + if (!originalPostEvent) { + const originalPostEvent = results.find((event) => event.id === originalPostId); + if (!originalPostEvent) continue; + + await hydrateEvents({ events: [originalPostEvent], storage: storage, signal: signal, relations: ['author'] }); + + event.quote_repost = originalPostEvent; + continue; + } + + if (!originalPostEvent.author) { + await hydrateEvents({ events: [originalPostEvent], storage: storage, signal: signal, relations: ['author'] }); + + event.quote_repost = originalPostEvent; + continue; + } + + event.quote_repost = originalPostEvent; + } + } + + return events; +} + /** Return a normalized event without any non-standard keys. */ function purifyEvent(event: NostrEvent): NostrEvent { return { diff --git a/src/views.ts b/src/views.ts index f9e84fc..0ccab9f 100644 --- a/src/views.ts +++ b/src/views.ts @@ -62,7 +62,7 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal const sortedEvents = [...events].sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); const statuses = await Promise.all( - sortedEvents.map((event) => renderStatus(event, c.get('pubkey'))), + sortedEvents.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') })), ); // TODO: pagination with min_id and max_id based on the order of `ids`. diff --git a/src/views/mastodon/notifications.ts b/src/views/mastodon/notifications.ts index 8ec7bf4..cb91dec 100644 --- a/src/views/mastodon/notifications.ts +++ b/src/views/mastodon/notifications.ts @@ -13,7 +13,7 @@ function renderNotification(event: NostrEvent, viewerPubkey?: string) { async function renderNotificationMention(event: NostrEvent, viewerPubkey?: string) { const author = await getAuthor(event.pubkey); - const status = await renderStatus({ ...event, author }, viewerPubkey); + const status = await renderStatus({ ...event, author }, { viewerPubkey: viewerPubkey }); if (!status) return; return { diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 19ce381..989bdef 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -14,7 +14,16 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts'; -async function renderStatus(event: DittoEvent, viewerPubkey?: string) { +interface statusOpts { + viewerPubkey?: string; + depth?: number; +} + +async function renderStatus(event: DittoEvent, opts: statusOpts): Promise { + const { viewerPubkey, depth = 1 } = opts; + + if (depth > 2 || depth < 0) return null; + const note = nip19.noteEncode(event.id); const account = event.author @@ -67,6 +76,8 @@ async function renderStatus(event: DittoEvent, viewerPubkey?: string) { const media = [...mediaLinks, ...mediaTags]; + const quoteStatus = !event.quote_repost ? null : await renderStatus(event.quote_repost, { depth: depth + 1 }); + return { id: event.id, account, @@ -94,6 +105,8 @@ async function renderStatus(event: DittoEvent, viewerPubkey?: string) { tags: [], emojis: renderEmojis(event), poll: null, + quote: quoteStatus, + quote_id: quoteStatus ? quoteStatus.id : null, uri: Conf.external(note), url: Conf.external(note), zapped: Boolean(zapEvent), @@ -108,7 +121,7 @@ async function renderReblog(event: DittoEvent) { if (!event.repost) return; - const reblog = await renderStatus(event.repost); + const reblog = await renderStatus(event.repost, {}); reblog.reblogged = true; return {