feat: Add tracking pixel to notifications for mark-as-read functionality (#5626)
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user