Merge branch 'feature-quote-repost' into 'main'
Add quote repost feature See merge request soapbox-pub/ditto!159
This commit is contained in:
commit
97d2fa1b79
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"id": "6bc9ca44feb5a261841873def54a81cc328737391dc10f7eada31173a399517d",
|
||||||
|
"pubkey": "47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4",
|
||||||
|
"created_at": 1712851917,
|
||||||
|
"kind": 0,
|
||||||
|
"tags": [],
|
||||||
|
"content": "{\"name\":\"patrickReiis\",\"picture\":\"https://void.cat/d/EMs8Qdn5wsAMrZ5T9T44sz.webp\"}",
|
||||||
|
"sig": "cedbd2585c18c9ee8cbafa4e3b1fefbe68cc15deeabcb0519791c6d715f92d1439ca9ac7584185a94d521709f9023fcbafab47a074a7ce8a247d3ce4dfce8af3"
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"id": "e0c2b45143717d62f85880aa7e26f2c3f4b10ada9ef547ae2479cfdd94ea2ce6",
|
||||||
|
"pubkey": "47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4",
|
||||||
|
"created_at": 1713217672,
|
||||||
|
"kind": 1,
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"q",
|
||||||
|
"826b28986cbe8196faaddb502bbcfb9d5521981d24e858ad1924a178e18f570f",
|
||||||
|
"wss://relay.mostr.pub/"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"content": "I like this lottery.\nnostr:nevent1qqsgy6egnpktaqvkl2kak5pthnae64fpnqwjf6zc45vjfgtcux84wrcpzemhxue69uhhyetvv9ujumt0wd68ytnsw43z7q3q08pv4cg5ag52nq082kd5leu9ffrn2gdg6g4xdwatn73y36uzplmqxpqqqqqqzrvwgz7",
|
||||||
|
"sig": "5a40475e719ad4cf98dd685a268158995c25050057632564d38789ce39a66e9d34b2d4ec9bef650b60bcfe8106415385f28ba291e168a1d02e32e092b8b86615"
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"id": "826b28986cbe8196faaddb502bbcfb9d5521981d24e858ad1924a178e18f570f",
|
||||||
|
"pubkey": "79c2cae114ea28a981e7559b4fe7854a473521a8d22a66bbab9fa248eb820ff6",
|
||||||
|
"created_at": 1711675519,
|
||||||
|
"kind": 1,
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"zap",
|
||||||
|
"79c2cae114ea28a981e7559b4fe7854a473521a8d22a66bbab9fa248eb820ff6",
|
||||||
|
"wss://relay.mostr.pub",
|
||||||
|
"0.915"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"zap",
|
||||||
|
"6be38f8c63df7dbf84db7ec4a6e6fbbd8d19dca3b980efad18585c46f04b26f9",
|
||||||
|
"wss://relay.mostr.pub",
|
||||||
|
"0.085"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"proxy",
|
||||||
|
"https://gleasonator.com/objects/66216159-a709-431b-81e9-e4e1f86e20e4",
|
||||||
|
"activitypub"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"content": "The Bitcoin Lottery is free to play, and you can win millions! Unlimited tries!\n\nJust guess 12 words mnemonic seed phrase words.",
|
||||||
|
"sig": "b76264f9a7ec0860a9dd3b72f94e81ed6c0d848eee2bc5cc89b78b1cb1b4e00243f0f354c0185824fe16eb16cfcab511275388b6acd29e0d05d97dea1564d5be"
|
||||||
|
}
|
|
@ -163,7 +163,7 @@ const accountStatusesController: AppController = async (c) => {
|
||||||
return events;
|
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);
|
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 })
|
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);
|
return paginated(c, events1, statuses);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ const searchController: AppController = async (c) => {
|
||||||
Promise.all(
|
Promise.all(
|
||||||
results
|
results
|
||||||
.filter((event) => event.kind === 1)
|
.filter((event) => event.kind === 1)
|
||||||
.map((event) => renderStatus(event, c.get('pubkey'))),
|
.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') })),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -40,12 +40,12 @@ const statusController: AppController = async (c) => {
|
||||||
|
|
||||||
const event = await getEvent(id, {
|
const event = await getEvent(id, {
|
||||||
kind: 1,
|
kind: 1,
|
||||||
relations: ['author', 'event_stats', 'author_stats'],
|
relations: ['author', 'event_stats', 'author_stats', 'quote_repost'],
|
||||||
signal: AbortSignal.timeout(1500),
|
signal: AbortSignal.timeout(1500),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (event) {
|
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);
|
return c.json({ error: 'Event not found.' }, 404);
|
||||||
|
@ -130,7 +130,7 @@ const createStatusController: AppController = async (c) => {
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
const author = await getAuthor(event.pubkey);
|
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) => {
|
const deleteStatusController: AppController = async (c) => {
|
||||||
|
@ -147,7 +147,7 @@ const deleteStatusController: AppController = async (c) => {
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
const author = await getAuthor(event.pubkey);
|
const author = await getAuthor(event.pubkey);
|
||||||
return c.json(await renderStatus({ ...event, author }, pubkey));
|
return c.json(await renderStatus({ ...event, author }, { viewerPubkey: pubkey }));
|
||||||
} else {
|
} else {
|
||||||
return c.json({ error: 'Unauthorized' }, 403);
|
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'] });
|
const event = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'] });
|
||||||
|
|
||||||
async function renderStatuses(events: NostrEvent[]) {
|
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);
|
return statuses.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ const favouriteController: AppController = async (c) => {
|
||||||
],
|
],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
const status = await renderStatus(target, c.get('pubkey'));
|
const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') });
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
status.favourited = true;
|
status.favourited = true;
|
||||||
|
@ -259,7 +259,7 @@ const unreblogStatusController: AppController = async (c) => {
|
||||||
tags: [['e', repostedEvent.id]],
|
tags: [['e', repostedEvent.id]],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
return c.json(await renderStatus(event));
|
return c.json(await renderStatus(event, {}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rebloggedByController: AppController = (c) => {
|
const rebloggedByController: AppController = (c) => {
|
||||||
|
@ -285,7 +285,7 @@ const bookmarkController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = await renderStatus(event, pubkey);
|
const status = await renderStatus(event, { viewerPubkey: pubkey });
|
||||||
if (status) {
|
if (status) {
|
||||||
status.bookmarked = true;
|
status.bookmarked = true;
|
||||||
}
|
}
|
||||||
|
@ -312,7 +312,7 @@ const unbookmarkController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = await renderStatus(event, pubkey);
|
const status = await renderStatus(event, { viewerPubkey: pubkey });
|
||||||
if (status) {
|
if (status) {
|
||||||
status.bookmarked = false;
|
status.bookmarked = false;
|
||||||
}
|
}
|
||||||
|
@ -339,7 +339,7 @@ const pinController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = await renderStatus(event, pubkey);
|
const status = await renderStatus(event, { viewerPubkey: pubkey });
|
||||||
if (status) {
|
if (status) {
|
||||||
status.pinned = true;
|
status.pinned = true;
|
||||||
}
|
}
|
||||||
|
@ -368,7 +368,7 @@ const unpinController: AppController = async (c) => {
|
||||||
c,
|
c,
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = await renderStatus(event, pubkey);
|
const status = await renderStatus(event, { viewerPubkey: pubkey });
|
||||||
if (status) {
|
if (status) {
|
||||||
status.pinned = false;
|
status.pinned = false;
|
||||||
}
|
}
|
||||||
|
@ -411,7 +411,7 @@ const zapController: AppController = async (c) => {
|
||||||
],
|
],
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
const status = await renderStatus(target, c.get('pubkey'));
|
const status = await renderStatus(target, { viewerPubkey: c.get('pubkey') });
|
||||||
status.zapped = true;
|
status.zapped = true;
|
||||||
|
|
||||||
return c.json(status);
|
return c.json(status);
|
||||||
|
|
|
@ -79,7 +79,7 @@ const streamingController: AppController = (c) => {
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const status = await renderStatus(event, pubkey);
|
const status = await renderStatus(event, { viewerPubkey: pubkey });
|
||||||
if (status) {
|
if (status) {
|
||||||
send('update', status);
|
send('update', status);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
||||||
.then((events) =>
|
.then((events) =>
|
||||||
hydrateEvents({
|
hydrateEvents({
|
||||||
events,
|
events,
|
||||||
relations: ['author', 'author_stats', 'event_stats', 'repost'],
|
relations: ['author', 'author_stats', 'event_stats', 'repost', 'quote_repost'],
|
||||||
storage: eventsDB,
|
storage: eventsDB,
|
||||||
signal,
|
signal,
|
||||||
})
|
})
|
||||||
|
@ -65,7 +65,7 @@ async function renderStatuses(c: AppContext, filters: NostrFilter[]) {
|
||||||
if (event.kind === 6) {
|
if (event.kind === 6) {
|
||||||
return renderReblog(event);
|
return renderReblog(event);
|
||||||
}
|
}
|
||||||
return renderStatus(event, c.get('pubkey'));
|
return renderStatus(event, { viewerPubkey: c.get('pubkey') });
|
||||||
}))).filter((boolean) => boolean);
|
}))).filter((boolean) => boolean);
|
||||||
|
|
||||||
if (!statuses.length) {
|
if (!statuses.length) {
|
||||||
|
|
|
@ -23,4 +23,5 @@ export interface DittoEvent extends NostrEvent {
|
||||||
d_author?: DittoEvent;
|
d_author?: DittoEvent;
|
||||||
user?: DittoEvent;
|
user?: DittoEvent;
|
||||||
repost?: NostrEvent;
|
repost?: NostrEvent;
|
||||||
|
quote_repost?: NostrEvent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,69 +1,162 @@
|
||||||
import { assertEquals } from '@/deps-test.ts';
|
import { assertEquals } from '@/deps-test.ts';
|
||||||
import { EventsDB } from '@/storages/events-db.ts';
|
|
||||||
import { db } from '@/db.ts';
|
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
|
import { NCache } from 'jsr:@nostrify/nostrify';
|
||||||
|
|
||||||
import event0 from '~/fixtures/events/event-0.json' with { type: 'json' };
|
import event0 from '~/fixtures/events/event-0.json' with { type: 'json' };
|
||||||
import event0madePost from '~/fixtures/events/event-0-the-one-who-post-and-users-repost.json' with { type: 'json' };
|
import event0madePost from '~/fixtures/events/event-0-the-one-who-post-and-users-repost.json' with { type: 'json' };
|
||||||
import event0madeRepost from '~/fixtures/events/event-0-the-one-who-repost.json' with { type: 'json' };
|
import event0madeRepost from '~/fixtures/events/event-0-the-one-who-repost.json' with { type: 'json' };
|
||||||
|
import event0madeQuoteRepost from '~/fixtures/events/event-0-the-one-who-quote-repost.json' with { type: 'json' };
|
||||||
import event1 from '~/fixtures/events/event-1.json' with { type: 'json' };
|
import event1 from '~/fixtures/events/event-1.json' with { type: 'json' };
|
||||||
|
import event1quoteRepost from '~/fixtures/events/event-1-quote-repost.json' with { type: 'json' };
|
||||||
|
import event1willBeQuoteReposted from '~/fixtures/events/event-1-that-will-be-quote-reposted.json' with {
|
||||||
|
type: 'json',
|
||||||
|
};
|
||||||
import event1reposted from '~/fixtures/events/event-1-reposted.json' with { type: 'json' };
|
import event1reposted from '~/fixtures/events/event-1-reposted.json' with { type: 'json' };
|
||||||
import event6 from '~/fixtures/events/event-6.json' with { type: 'json' };
|
import event6 from '~/fixtures/events/event-6.json' with { type: 'json' };
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
|
|
||||||
const eventsDB = new EventsDB(db);
|
|
||||||
|
|
||||||
Deno.test('hydrate author', async () => {
|
Deno.test('hydrate author', async () => {
|
||||||
// Save events to database
|
const db = new NCache({ max: 100 });
|
||||||
await eventsDB.event(event0);
|
|
||||||
await eventsDB.event(event1);
|
|
||||||
|
|
||||||
assertEquals((event1 as DittoEvent).author, undefined, "Event hasn't been hydrated yet");
|
const event0copy = structuredClone(event0);
|
||||||
|
const event1copy = structuredClone(event1);
|
||||||
|
|
||||||
|
// Save events to database
|
||||||
|
await db.event(event0copy);
|
||||||
|
await db.event(event1copy);
|
||||||
|
|
||||||
|
assertEquals((event1copy as DittoEvent).author, undefined, "Event hasn't been hydrated yet");
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [event1],
|
events: [event1copy],
|
||||||
relations: ['author'],
|
relations: ['author'],
|
||||||
storage: eventsDB,
|
storage: db,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedEvent = { ...event1, author: event0 };
|
const expectedEvent = { ...event1copy, author: event0copy };
|
||||||
assertEquals(event1, expectedEvent);
|
assertEquals(event1copy, expectedEvent);
|
||||||
|
|
||||||
await eventsDB.remove([{ kinds: [0, 1] }]);
|
await db.remove([{ kinds: [0, 1] }]);
|
||||||
assertEquals(await eventsDB.query([{ kinds: [0, 1] }]), []);
|
assertEquals(await db.query([{ kinds: [0, 1] }]), []);
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('hydrate repost', async () => {
|
Deno.test('hydrate repost', async () => {
|
||||||
// Save events to database
|
const db = new NCache({ max: 100 });
|
||||||
await eventsDB.event(event0madePost);
|
|
||||||
await eventsDB.event(event0madeRepost);
|
|
||||||
await eventsDB.event(event1reposted);
|
|
||||||
await eventsDB.event(event6);
|
|
||||||
|
|
||||||
assertEquals((event6 as DittoEvent).author, undefined, "Event hasn't been hydrated author yet");
|
const event0madePostCopy = structuredClone(event0madePost);
|
||||||
assertEquals((event6 as DittoEvent).repost, undefined, "Event hasn't been hydrated repost yet");
|
const event0madeRepostCopy = structuredClone(event0madeRepost);
|
||||||
|
const event1repostedCopy = structuredClone(event1reposted);
|
||||||
|
const event6copy = structuredClone(event6);
|
||||||
|
|
||||||
|
// Save events to database
|
||||||
|
await db.event(event0madePostCopy);
|
||||||
|
await db.event(event0madeRepostCopy);
|
||||||
|
await db.event(event1repostedCopy);
|
||||||
|
await db.event(event6copy);
|
||||||
|
|
||||||
|
assertEquals((event6copy as DittoEvent).author, undefined, "Event hasn't been hydrated author yet");
|
||||||
|
assertEquals((event6copy as DittoEvent).repost, undefined, "Event hasn't been hydrated repost yet");
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
||||||
|
|
||||||
await hydrateEvents({
|
await hydrateEvents({
|
||||||
events: [event6],
|
events: [event6copy],
|
||||||
relations: ['repost', 'author'],
|
relations: ['repost', 'author'],
|
||||||
storage: eventsDB,
|
storage: db,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedEvent6 = { ...event6, author: event0madeRepost, repost: { ...event1reposted, author: event0madePost } };
|
const expectedEvent6 = {
|
||||||
assertEquals(event6, expectedEvent6);
|
...event6copy,
|
||||||
|
author: event0madeRepostCopy,
|
||||||
|
repost: { ...event1repostedCopy, author: event0madePostCopy },
|
||||||
|
};
|
||||||
|
assertEquals(event6copy, expectedEvent6);
|
||||||
|
|
||||||
await eventsDB.remove([{ kinds: [0, 1, 6] }]);
|
await db.remove([{ kinds: [0, 1, 6] }]);
|
||||||
assertEquals(await eventsDB.query([{ kinds: [0, 1, 6] }]), []);
|
assertEquals(await db.query([{ kinds: [0, 1, 6] }]), []);
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('hydrate quote repost with hydrate author', async () => {
|
||||||
|
const db = new NCache({ max: 100 });
|
||||||
|
|
||||||
|
const event0madeQuoteRepostCopy = structuredClone(event0madeQuoteRepost);
|
||||||
|
const event0copy = structuredClone(event0);
|
||||||
|
const event1quoteRepostCopy = structuredClone(event1quoteRepost);
|
||||||
|
const event1willBeQuoteRepostedCopy = structuredClone(event1willBeQuoteReposted);
|
||||||
|
|
||||||
|
// Save events to database
|
||||||
|
await db.event(event0madeQuoteRepostCopy);
|
||||||
|
await db.event(event0copy);
|
||||||
|
await db.event(event1quoteRepostCopy);
|
||||||
|
await db.event(event1willBeQuoteRepostedCopy);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
||||||
|
|
||||||
|
await hydrateEvents({
|
||||||
|
events: [event1quoteRepostCopy],
|
||||||
|
relations: ['author', 'quote_repost'], // if author is called first the performance will be better
|
||||||
|
storage: db,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedEvent1quoteRepost = {
|
||||||
|
...event1quoteRepostCopy,
|
||||||
|
author: event0madeQuoteRepostCopy,
|
||||||
|
quote_repost: { ...event1willBeQuoteRepostedCopy, author: event0copy },
|
||||||
|
};
|
||||||
|
|
||||||
|
assertEquals(event1quoteRepostCopy, expectedEvent1quoteRepost);
|
||||||
|
|
||||||
|
await db.remove([{ kinds: [0, 1] }]);
|
||||||
|
assertEquals(await db.query([{ kinds: [0, 1] }]), []);
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('hydrate quote repost WITHOUT hydrate author', async () => {
|
||||||
|
const db = new NCache({ max: 100 });
|
||||||
|
|
||||||
|
const event0madeQuoteRepostCopy = structuredClone(event0madeQuoteRepost);
|
||||||
|
const event0copy = structuredClone(event0);
|
||||||
|
const event1quoteRepostCopy = structuredClone(event1quoteRepost);
|
||||||
|
const event1willBeQuoteRepostedCopy = structuredClone(event1willBeQuoteReposted);
|
||||||
|
|
||||||
|
// Save events to database
|
||||||
|
await db.event(event0madeQuoteRepostCopy);
|
||||||
|
await db.event(event0copy);
|
||||||
|
await db.event(event1quoteRepostCopy);
|
||||||
|
await db.event(event1willBeQuoteRepostedCopy);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
||||||
|
|
||||||
|
await hydrateEvents({
|
||||||
|
events: [event1quoteRepostCopy, event1willBeQuoteRepostedCopy],
|
||||||
|
relations: ['quote_repost'],
|
||||||
|
storage: db,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedEvent1quoteRepost = {
|
||||||
|
...event1quoteRepost,
|
||||||
|
quote_repost: { ...event1willBeQuoteRepostedCopy, author: event0copy },
|
||||||
|
};
|
||||||
|
|
||||||
|
assertEquals(event1quoteRepostCopy, expectedEvent1quoteRepost);
|
||||||
|
|
||||||
|
await db.remove([{ kinds: [0, 1] }]);
|
||||||
|
assertEquals(await db.query([{ kinds: [0, 1] }]), []);
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,6 +36,9 @@ async function hydrateEvents(opts: HydrateEventOpts): Promise<DittoEvent[]> {
|
||||||
case 'repost':
|
case 'repost':
|
||||||
await hydrateRepostEvents({ events, storage, signal });
|
await hydrateRepostEvents({ events, storage, signal });
|
||||||
break;
|
break;
|
||||||
|
case 'quote_repost':
|
||||||
|
await hydrateQuoteRepostEvents({ events, storage, signal });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +129,7 @@ async function hydrateRepostEvents(opts: Omit<HydrateEventOpts, 'relations'>): P
|
||||||
}
|
}
|
||||||
return event.id;
|
return event.id;
|
||||||
}),
|
}),
|
||||||
}]);
|
}], { signal });
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (event.kind === 6) {
|
if (event.kind === 6) {
|
||||||
|
@ -144,6 +147,49 @@ async function hydrateRepostEvents(opts: Omit<HydrateEventOpts, 'relations'>): P
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hydrateQuoteRepostEvents(opts: Omit<HydrateEventOpts, 'relations'>): Promise<DittoEvent[]> {
|
||||||
|
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. */
|
/** Return a normalized event without any non-standard keys. */
|
||||||
function purifyEvent(event: NostrEvent): NostrEvent {
|
function purifyEvent(event: NostrEvent): NostrEvent {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -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 sortedEvents = [...events].sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
|
||||||
|
|
||||||
const statuses = await Promise.all(
|
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`.
|
// TODO: pagination with min_id and max_id based on the order of `ids`.
|
||||||
|
|
|
@ -13,7 +13,7 @@ function renderNotification(event: NostrEvent, viewerPubkey?: string) {
|
||||||
|
|
||||||
async function renderNotificationMention(event: NostrEvent, viewerPubkey?: string) {
|
async function renderNotificationMention(event: NostrEvent, viewerPubkey?: string) {
|
||||||
const author = await getAuthor(event.pubkey);
|
const author = await getAuthor(event.pubkey);
|
||||||
const status = await renderStatus({ ...event, author }, viewerPubkey);
|
const status = await renderStatus({ ...event, author }, { viewerPubkey: viewerPubkey });
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -14,7 +14,16 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts';
|
import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts';
|
||||||
import { renderEmojis } from '@/views/mastodon/emojis.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<any> {
|
||||||
|
const { viewerPubkey, depth = 1 } = opts;
|
||||||
|
|
||||||
|
if (depth > 2 || depth < 0) return null;
|
||||||
|
|
||||||
const note = nip19.noteEncode(event.id);
|
const note = nip19.noteEncode(event.id);
|
||||||
|
|
||||||
const account = event.author
|
const account = event.author
|
||||||
|
@ -67,6 +76,8 @@ async function renderStatus(event: DittoEvent, viewerPubkey?: string) {
|
||||||
|
|
||||||
const media = [...mediaLinks, ...mediaTags];
|
const media = [...mediaLinks, ...mediaTags];
|
||||||
|
|
||||||
|
const quoteStatus = !event.quote_repost ? null : await renderStatus(event.quote_repost, { depth: depth + 1 });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
account,
|
account,
|
||||||
|
@ -94,6 +105,8 @@ async function renderStatus(event: DittoEvent, viewerPubkey?: string) {
|
||||||
tags: [],
|
tags: [],
|
||||||
emojis: renderEmojis(event),
|
emojis: renderEmojis(event),
|
||||||
poll: null,
|
poll: null,
|
||||||
|
quote: quoteStatus,
|
||||||
|
quote_id: quoteStatus ? quoteStatus.id : null,
|
||||||
uri: Conf.external(note),
|
uri: Conf.external(note),
|
||||||
url: Conf.external(note),
|
url: Conf.external(note),
|
||||||
zapped: Boolean(zapEvent),
|
zapped: Boolean(zapEvent),
|
||||||
|
@ -108,7 +121,7 @@ async function renderReblog(event: DittoEvent) {
|
||||||
|
|
||||||
if (!event.repost) return;
|
if (!event.repost) return;
|
||||||
|
|
||||||
const reblog = await renderStatus(event.repost);
|
const reblog = await renderStatus(event.repost, {});
|
||||||
reblog.reblogged = true;
|
reblog.reblogged = true;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in New Issue