diff --git a/server/emails/templates/BaseEmail.tsx b/server/emails/templates/BaseEmail.tsx index eb0516705..4477ec158 100644 --- a/server/emails/templates/BaseEmail.tsx +++ b/server/emails/templates/BaseEmail.tsx @@ -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 { } 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 { 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 { 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 ; + } + /** * Returns the subject of the email. * diff --git a/server/models/Notification.ts b/server/models/Notification.ts index 9c27c9907..52cb5db35 100644 --- a/server/models/Notification.ts +++ b/server/models/Notification.ts @@ -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; diff --git a/server/routes/api/notifications/notifications.ts b/server/routes/api/notifications/notifications.ts index e2ef80a5b..f65c617f2 100644 --- a/server/routes/api/notifications/notifications.ts +++ b/server/routes/api/notifications/notifications.ts @@ -120,21 +120,24 @@ router.post( router.get( "notifications.pixel", + validate(T.NotificationsPixelSchema), transaction(), async (ctx: APIContext) => { 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;