feat: Add tracking pixel to notifications for mark-as-read functionality (#5626)

This commit is contained in:
Tom Moor
2023-07-31 18:01:50 -04:00
committed by GitHub
parent a13f2c7311
commit 91585ee09d
3 changed files with 37 additions and 19 deletions

View File

@@ -1,4 +1,5 @@
import Bull from "bull";
import * as React from "react";
import mailer from "@server/emails/mailer";
import Logger from "@server/logging/Logger";
import Metrics from "@server/logging/Metrics";
@@ -84,6 +85,9 @@ export default abstract class BaseEmail<T extends EmailProps, S = unknown> {
}
const data = { ...this.props, ...(bsResponse ?? ({} as S)) };
const notification = this.metadata?.notificationId
? await Notification.unscoped().findByPk(this.metadata?.notificationId)
: undefined;
try {
await mailer.sendMail({
@@ -91,7 +95,12 @@ export default abstract class BaseEmail<T extends EmailProps, S = unknown> {
fromName: this.fromName?.(data),
subject: this.subject(data),
previewText: this.preview(data),
component: this.render(data),
component: (
<>
{this.render(data)}
{notification ? this.pixel(notification) : null}
</>
),
text: this.renderAsText(data),
headCSS: this.headCSS?.(data),
});
@@ -105,24 +114,20 @@ export default abstract class BaseEmail<T extends EmailProps, S = unknown> {
throw err;
}
if (this.metadata?.notificationId) {
if (notification) {
try {
await Notification.update(
{
emailedAt: new Date(),
},
{
where: {
id: this.metadata.notificationId,
},
}
);
notification.emailedAt = new Date();
await notification.save();
} catch (err) {
Logger.error(`Failed to update notification`, err, this.metadata);
}
}
}
private pixel(notification: Notification) {
return <img src={notification.pixelUrl} width="1" height="1" />;
}
/**
* Returns the subject of the email.
*

View File

@@ -196,6 +196,16 @@ class Notification extends Model {
hash.update(`${this.id}-${env.SECRET_KEY}`);
return hash.digest("hex");
}
/**
* Returns a URL that can be used to mark this notification as read
* without being logged in.
*
* @returns A URL
*/
public get pixelUrl() {
return `${env.URL}/api/notifications.pixel?token=${this.pixelToken}&id=${this.id}`;
}
}
export default Notification;

View File

@@ -120,21 +120,24 @@ router.post(
router.get(
"notifications.pixel",
validate(T.NotificationsPixelSchema),
transaction(),
async (ctx: APIContext<T.NotificationsPixelReq>) => {
const { id, token } = ctx.input.query;
const notification = await Notification.findByPk(id);
const notification = await Notification.unscoped().findByPk(id);
if (!notification || !safeEqual(token, notification.pixelToken)) {
throw AuthenticationError();
}
await notificationUpdater({
notification,
viewedAt: new Date(),
ip: ctx.request.ip,
transaction: ctx.state.transaction,
});
if (!notification.viewedAt) {
await notificationUpdater({
notification,
viewedAt: new Date(),
ip: ctx.request.ip,
transaction: ctx.state.transaction,
});
}
ctx.response.set("Content-Type", "image/gif");
ctx.body = pixel;