global push notifications toggle, inline status change on post detail

This commit is contained in:
2026-03-22 07:29:08 +02:00
parent 9f4b92cc36
commit 6b110a6d90
5 changed files with 176 additions and 4 deletions

View File

@@ -9,7 +9,7 @@ import { firePluginEvent } from "../services/webhooks.js";
import { decrypt } from "../services/encryption.js";
import { masterKey } from "../config.js";
import { shouldCount } from "../lib/view-tracker.js";
import { notifyBoardSubscribers } from "../services/push.js";
import { notifyBoardSubscribers, notifyGlobalSubscribers } from "../services/push.js";
const INVISIBLE_RE = /[\u200B\u200C\u200D\uFEFF\u200E\u200F\u202A-\u202E\u2066-\u2069\u00AD\u2060\u180E]/g;
@@ -556,6 +556,12 @@ export default async function postRoutes(app: FastifyInstance) {
url: `/b/${board.slug}/post/${post.id}`,
tag: `board-${board.id}-new`,
});
notifyGlobalSubscribers({
title: `New post in ${board.name}`,
body: cleanTitle.slice(0, 100),
url: `/b/${board.slug}/post/${post.id}`,
tag: `global-new-post`,
});
reply.status(201).send({
id: post.id, type: post.type, title: post.title, description: post.description,

View File

@@ -139,4 +139,60 @@ export default async function pushRoutes(app: FastifyInstance) {
reply.send(subs);
}
);
// global subscription status
app.get(
"/push/global-subscription",
{ preHandler: [app.requireUser], config: { rateLimit: { max: 30, timeWindow: "1 minute" } } },
async (req, reply) => {
const sub = await prisma.pushSubscription.findFirst({
where: { userId: req.user!.id, boardId: null, postId: null },
select: { id: true },
});
reply.send({ subscribed: !!sub });
}
);
// global subscribe
app.post(
"/push/global-subscribe",
{ preHandler: [app.requireUser], config: { rateLimit: { max: 10, timeWindow: "1 minute" } } },
async (req, reply) => {
const body = subscribeBody.parse(req.body);
const endpointIdx = blindIndex(body.endpoint, blindIndexKey);
const existing = await prisma.pushSubscription.findUnique({ where: { endpointIdx } });
if (existing) {
await prisma.pushSubscription.update({
where: { endpointIdx },
data: { userId: req.user!.id, boardId: null, postId: null },
});
} else {
await prisma.pushSubscription.create({
data: {
endpoint: encrypt(body.endpoint, masterKey),
endpointIdx,
keysP256dh: encrypt(body.keys.p256dh, masterKey),
keysAuth: encrypt(body.keys.auth, masterKey),
userId: req.user!.id,
boardId: null,
postId: null,
},
});
}
reply.send({ subscribed: true });
}
);
// global unsubscribe
app.delete(
"/push/global-subscribe",
{ preHandler: [app.requireUser], config: { rateLimit: { max: 10, timeWindow: "1 minute" } } },
async (req, reply) => {
await prisma.pushSubscription.deleteMany({
where: { userId: req.user!.id, boardId: null, postId: null },
});
reply.send({ subscribed: false });
}
);
}

View File

@@ -82,6 +82,11 @@ export async function notifyBoardSubscribers(boardId: string, event: PushPayload
await processResults(subs, event);
}
export async function notifyGlobalSubscribers(event: PushPayload) {
const subs = await prisma.pushSubscription.findMany({ where: { boardId: null, postId: null } });
await processResults(subs, event);
}
export async function notifyUserReply(userId: string, event: PushPayload) {
const subs = await prisma.pushSubscription.findMany({ where: { userId } });
await processResults(subs, event);