mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
feat: make notifications great again
it works the same as before, but with way less code
This commit is contained in:
parent
9b66499df3
commit
a18b049eb7
3 changed files with 15 additions and 82 deletions
|
|
@ -4,10 +4,9 @@ import { z } from 'zod';
|
||||||
import { AppContext, AppController } from '@/app.ts';
|
import { AppContext, AppController } from '@/app.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { DittoPagination } from '@/interfaces/DittoPagination.ts';
|
import { DittoPagination } from '@/interfaces/DittoPagination.ts';
|
||||||
import { getAmount } from '@/utils/bolt11.ts';
|
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { paginated } from '@/utils/api.ts';
|
import { paginated } from '@/utils/api.ts';
|
||||||
import { renderNotification, RenderNotificationOpts } from '@/views/mastodon/notifications.ts';
|
import { renderNotification } from '@/views/mastodon/notifications.ts';
|
||||||
|
|
||||||
/** Set of known notification types across backends. */
|
/** Set of known notification types across backends. */
|
||||||
const notificationTypes = new Set([
|
const notificationTypes = new Set([
|
||||||
|
|
@ -86,54 +85,17 @@ async function renderNotifications(
|
||||||
const { signal } = c.req.raw;
|
const { signal } = c.req.raw;
|
||||||
const opts = { signal, limit: params.limit, timeout: Conf.db.timeouts.timelines };
|
const opts = { signal, limit: params.limit, timeout: Conf.db.timeouts.timelines };
|
||||||
|
|
||||||
const zapsRelatedFilter: NostrFilter[] = [];
|
|
||||||
|
|
||||||
const events = await store
|
const events = await store
|
||||||
.query(filters, opts)
|
.query(filters, opts)
|
||||||
.then((events) =>
|
.then((events) => events.filter((event) => event.pubkey !== pubkey))
|
||||||
events.filter((event) => {
|
|
||||||
if (event.kind === 9735) {
|
|
||||||
const zappedEventId = event.tags.find(([name]) => name === 'e')?.[1];
|
|
||||||
if (zappedEventId) zapsRelatedFilter.push({ kinds: [1], ids: [zappedEventId] });
|
|
||||||
const zapSender = event.tags.find(([name]) => name === 'P')?.[1];
|
|
||||||
if (zapSender) zapsRelatedFilter.push({ kinds: [0], authors: [zapSender] });
|
|
||||||
}
|
|
||||||
|
|
||||||
return event.pubkey !== pubkey;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.then((events) => hydrateEvents({ events, store, signal }));
|
.then((events) => hydrateEvents({ events, store, signal }));
|
||||||
|
|
||||||
if (!events.length) {
|
if (!events.length) {
|
||||||
return c.json([]);
|
return c.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zapSendersAndPosts = await store
|
|
||||||
.query(zapsRelatedFilter, opts)
|
|
||||||
.then((events) => hydrateEvents({ events, store, signal }));
|
|
||||||
|
|
||||||
const notifications = (await Promise.all(events.map((event) => {
|
const notifications = (await Promise.all(events.map((event) => {
|
||||||
const opts: RenderNotificationOpts = { viewerPubkey: pubkey };
|
return renderNotification(event, { viewerPubkey: pubkey });
|
||||||
if (event.kind === 9735) {
|
|
||||||
const zapRequestString = event?.tags?.find(([name]) => name === 'description')?.[1];
|
|
||||||
const zapRequest = n.json().pipe(n.event()).optional().catch(undefined).parse(zapRequestString);
|
|
||||||
// By getting the pubkey from the zap request we guarantee who is the sender
|
|
||||||
// some clients don't put the P tag in the zap receipt...
|
|
||||||
const zapSender = zapRequest?.pubkey;
|
|
||||||
const zappedPost = event.tags.find(([name]) => name === 'e')?.[1];
|
|
||||||
|
|
||||||
const amountSchema = z.coerce.number().int().nonnegative().catch(0);
|
|
||||||
// amount in millisats
|
|
||||||
const amount = amountSchema.parse(getAmount(event?.tags.find(([name]) => name === 'bolt11')?.[1]));
|
|
||||||
|
|
||||||
opts['zap'] = {
|
|
||||||
zapSender: zapSendersAndPosts.find(({ pubkey, kind }) => kind === 0 && pubkey === zapSender) ?? zapSender,
|
|
||||||
zappedPost: zapSendersAndPosts.find(({ id }) => id === zappedPost),
|
|
||||||
amount,
|
|
||||||
message: zapRequest?.content,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return renderNotification(event, opts);
|
|
||||||
})))
|
})))
|
||||||
.filter((notification) => notification && types.has(notification.type));
|
.filter((notification) => notification && types.has(notification.type));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import TTLCache from '@isaacs/ttlcache';
|
import TTLCache from '@isaacs/ttlcache';
|
||||||
import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
|
import { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
||||||
import Debug from '@soapbox/stickynotes/debug';
|
import Debug from '@soapbox/stickynotes/debug';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
|
@ -11,10 +11,8 @@ import { getFeedPubkeys } from '@/queries.ts';
|
||||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import { bech32ToPubkey, Time } from '@/utils.ts';
|
import { bech32ToPubkey, Time } from '@/utils.ts';
|
||||||
import { getAmount } from '@/utils/bolt11.ts';
|
|
||||||
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
import { renderNotification } from '@/views/mastodon/notifications.ts';
|
import { renderNotification } from '@/views/mastodon/notifications.ts';
|
||||||
import { RenderNotificationOpts } from '@/views/mastodon/notifications.ts';
|
|
||||||
|
|
||||||
const debug = Debug('ditto:streaming');
|
const debug = Debug('ditto:streaming');
|
||||||
|
|
||||||
|
|
@ -157,28 +155,7 @@ const streamingController: AppController = async (c) => {
|
||||||
if (['user', 'user:notification'].includes(stream) && pubkey) {
|
if (['user', 'user:notification'].includes(stream) && pubkey) {
|
||||||
sub([{ '#p': [pubkey] }], async (event) => {
|
sub([{ '#p': [pubkey] }], async (event) => {
|
||||||
if (event.pubkey === pubkey) return; // skip own events
|
if (event.pubkey === pubkey) return; // skip own events
|
||||||
|
const payload = await renderNotification(event, { viewerPubkey: pubkey });
|
||||||
const opts: RenderNotificationOpts = { viewerPubkey: pubkey };
|
|
||||||
|
|
||||||
if (event.kind === 9735) {
|
|
||||||
const zapRequestString = event?.tags?.find(([name]) => name === 'description')?.[1];
|
|
||||||
const zapRequest = n.json().pipe(n.event()).optional().catch(undefined).parse(zapRequestString);
|
|
||||||
// By getting the pubkey from the zap request we guarantee who is the sender
|
|
||||||
// some clients don't put the P tag in the zap receipt...
|
|
||||||
const zapSender = zapRequest?.pubkey;
|
|
||||||
|
|
||||||
const amountSchema = z.coerce.number().int().nonnegative().catch(0);
|
|
||||||
// amount in millisats
|
|
||||||
const amount = amountSchema.parse(getAmount(event?.tags.find(([name]) => name === 'bolt11')?.[1]));
|
|
||||||
|
|
||||||
opts['zap'] = {
|
|
||||||
zapSender,
|
|
||||||
amount,
|
|
||||||
message: zapRequest?.content,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await renderNotification(event, opts);
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
return {
|
return {
|
||||||
event: 'notification',
|
event: 'notification',
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,8 @@ import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
import { nostrDate } from '@/utils.ts';
|
import { nostrDate } from '@/utils.ts';
|
||||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
|
|
||||||
export interface RenderNotificationOpts {
|
interface RenderNotificationOpts {
|
||||||
viewerPubkey: string;
|
viewerPubkey: string;
|
||||||
zap?: {
|
|
||||||
zapSender?: NostrEvent | NostrEvent['pubkey']; // kind 0 or pubkey
|
|
||||||
zappedPost?: NostrEvent;
|
|
||||||
amount?: number;
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNotification(event: DittoEvent, opts: RenderNotificationOpts) {
|
function renderNotification(event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
|
|
@ -120,23 +114,23 @@ async function renderNameGrant(event: DittoEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderZap(event: DittoEvent, opts: RenderNotificationOpts) {
|
async function renderZap(event: DittoEvent, opts: RenderNotificationOpts) {
|
||||||
if (!opts.zap?.zapSender) return;
|
if (!event.zap_sender) return;
|
||||||
|
|
||||||
const { amount = 0, message = '' } = opts.zap;
|
const { zap_amount = 0, zap_message = '' } = event;
|
||||||
if (amount < 1) return;
|
if (zap_amount < 1) return;
|
||||||
|
|
||||||
const account = typeof opts.zap.zapSender !== 'string'
|
const account = typeof event.zap_sender !== 'string'
|
||||||
? await renderAccount(opts.zap.zapSender)
|
? await renderAccount(event.zap_sender)
|
||||||
: await accountFromPubkey(opts.zap.zapSender);
|
: await accountFromPubkey(event.zap_sender);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: notificationId(event),
|
id: notificationId(event),
|
||||||
type: 'ditto:zap',
|
type: 'ditto:zap',
|
||||||
amount,
|
amount: zap_amount,
|
||||||
message,
|
message: zap_message,
|
||||||
created_at: nostrDate(event.created_at).toISOString(),
|
created_at: nostrDate(event.created_at).toISOString(),
|
||||||
account,
|
account,
|
||||||
...(opts.zap?.zappedPost ? { status: await renderStatus(opts.zap?.zappedPost, opts) } : {}),
|
...(event.zapped ? { status: await renderStatus(event.zapped, opts) } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue