Skip to content

Commit

Permalink
Merge pull request #228 from mobeigi/comment-notify-on-reply-feature
Browse files Browse the repository at this point in the history
Email comment author when somebody replies to them
  • Loading branch information
mobeigi authored Jan 27, 2025
2 parents 880472a + 206e70b commit 6a79073
Show file tree
Hide file tree
Showing 13 changed files with 333 additions and 60 deletions.
1 change: 1 addition & 0 deletions app/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ PAYLOAD_SECRET="YOUR_SECRET_HERE"
PAYLOAD_SYSTEM_USER_API_KEY="example"

PAYLOAD_FROM_EMAIL_ADDRESS="[email protected]"
PAYLOAD_FROM_NAME="MoBeigi.com"
PAYLOAD_TO_EMAIL_ADDRESS="[email protected]"
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { NextResponse } from 'next/server';
import { getPayload } from 'payload';
import config from '@payload-config';
import { createPayloadHmac256 } from '@/utils/crypto/hmac256';
import { headers } from 'next/headers';

export const GET = async (request: Request, { params: paramsPromise }: { params: Promise<{ commentId: string }> }) => {
try {
const params = await paramsPromise;

const { commentId } = params;

const url = new URL(request.url);
const token = url.searchParams.get('token');

if (!token) {
return NextResponse.json({ error: 'Token is required.' }, { status: 400 });
}

const payload = await getPayload({
config,
});

const comment = await payload.findByID({
collection: 'comments',
id: commentId,
disableErrors: true,
});

if (!comment) {
return NextResponse.json({ error: `Comment with id '${commentId}' does not exist.` }, { status: 404 });
}

// Validate token
const commentIdHmac256 = createPayloadHmac256({ data: commentId });
if (commentIdHmac256 !== token) {
return NextResponse.json({ error: 'Token is invalid.' }, { status: 401 });
}

if (!comment.notifyOnReply) {
return NextResponse.json({ error: 'You have already unsubscribed from this comment.' }, { status: 400 });
}

const headerList = await headers();
const result = await payload.auth({ headers: headerList });

await payload.update({
collection: 'comments',
id: commentId,
data: {
notifyOnReply: false,
},
overrideAccess: true,
user: result.user,
});

return NextResponse.json({ message: 'You have successfully unsubscribed from this comment.' }, { status: 200 });
} catch (error) {
console.error('Failed to unsubscribe from comment.', error);
return NextResponse.json({ error: 'Failed to unsubscribe from comment.' }, { status: 500 });
}
};
18 changes: 7 additions & 11 deletions app/src/components/Emails/NewCommentEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ interface NewCommentEmailProps {
postTitle: string;
commentUrl: string;
displayName: string;
author: string;
email: string;
ipAddress: string;
createdAt: Date;
Expand All @@ -18,24 +17,22 @@ export const NewCommentEmail = ({
postTitle,
commentUrl,
displayName,
author,
email,
ipAddress,
createdAt,
commentTextContent,
}: NewCommentEmailProps) => {
const createdAtDateString = formatDate(createdAt, "d MMMM yyyy 'at' hh:mm a");
// TODO: Use timezone library to include server timezone
const createdAtDateString = formatDate(createdAt, "d MMMM yyyy 'at' hh:mm a '(Sydney, Australia time)'");

return (
<Html>
<Body>
<HeaderSection />
<Section>
<Heading as="h2">New comment on blog post</Heading>
<Heading as="h2">New comment on blog post:</Heading>
<Text>
{postTitle}
<br />
<Link href={commentUrl}>{commentUrl}</Link>
<Link href={commentUrl}>{postTitle}</Link>
</Text>
</Section>
<Section>
Expand All @@ -46,15 +43,15 @@ export const NewCommentEmail = ({
</span>
<br />
<span>
<strong>Author:</strong> {author}
<strong>Email:</strong> {email}
</span>
<br />
<span>
<strong>Email:</strong> {email}
<strong>IP Address:</strong> {ipAddress}
</span>
<br />
<span>
<strong>IP Address:</strong> {ipAddress}
<strong>Comment URL:</strong> {commentUrl}
</span>
<br />
<span>
Expand All @@ -63,7 +60,6 @@ export const NewCommentEmail = ({
</Text>
</Section>
<Section>
<Heading as="h2">Comment</Heading>
<Text>{commentTextContent}</Text>
</Section>
</Body>
Expand Down
90 changes: 90 additions & 0 deletions app/src/components/Emails/NewReplyToCommentEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Html, Body, Heading, Text, Link, Section } from '@react-email/components';
import { render } from '@react-email/render';
import { HeaderSection } from './common/HeaderSection';
import { format as formatDate } from 'date-fns';

interface NewReplyToCommentEmailProps {
postTitle: string;
commentUrl: string;
unsubscribeUrl: string;
replyCommentDisplayName: string;
replyCommentUrl: string;
replyCommentCreatedAt: Date;
replyCommentTextContent: string;
}

export const NewReplyToCommentEmail = ({
postTitle,
commentUrl,
unsubscribeUrl,
replyCommentDisplayName,
replyCommentUrl,
replyCommentCreatedAt,
replyCommentTextContent,
}: NewReplyToCommentEmailProps) => {
// TODO: Use timezone library to include server timezone
const replyCommentCreatedAtDateString = formatDate(
replyCommentCreatedAt,
"d MMMM yyyy 'at' hh:mm a '(Sydney, Australia time)'",
);

return (
<Html>
<Body>
<HeaderSection />
<Section>
<Heading as="h2">New reply to your comment on:</Heading>
<Text>
<Link href={commentUrl}>{postTitle}</Link>
</Text>
</Section>
<Section>
<Heading as="h2">Comment Reply Details</Heading>
<Text>
<span>
<strong>Display Name:</strong> {replyCommentDisplayName}
</span>
<br />
<span>
<strong>Comment URL:</strong> {replyCommentUrl}
</span>
<br />
<span>
<strong>Created at:</strong> {replyCommentCreatedAtDateString}
</span>
</Text>
</Section>
<Section>
<Text>{replyCommentTextContent}</Text>
</Section>
<Section>
<Text
style={{
color: '#6c757d',
fontSize: '12px',
textAlign: 'center',
}}
>
<span>
<Link
href={unsubscribeUrl}
style={{
color: '#6c757d',
textDecoration: 'underline',
}}
>
Unsubscribe here
</Link>{' '}
to stop receiving emails for replies to this comment.
</span>
</Text>
</Section>
</Body>
</Html>
);
};

export const newReplyToCommentEmailHtml = async (props: NewReplyToCommentEmailProps): Promise<string> => {
const component = <NewReplyToCommentEmail {...props} />;
return render(component);
};
5 changes: 5 additions & 0 deletions app/src/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ export interface Comment {
* The browser user agent at the time the comment was submitted.
*/
userAgent: string;
/**
* Determines whether the comment author receives notifications for replies to this comment.
*/
notifyOnReply: boolean;
parent?: (number | null) | Comment;
breadcrumbs?:
| {
Expand Down Expand Up @@ -538,6 +542,7 @@ export interface CommentsSelect<T extends boolean = true> {
author?: T;
ipAddress?: T;
userAgent?: T;
notifyOnReply?: T;
parent?: T;
breadcrumbs?:
| T
Expand Down
2 changes: 1 addition & 1 deletion app/src/payload.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default buildConfig({
}),
email: nodemailerAdapter({
defaultFromAddress: requireEnvVar(process.env.PAYLOAD_FROM_EMAIL_ADDRESS, 'PAYLOAD_FROM_EMAIL_ADDRESS'),
defaultFromName: 'Payload',
defaultFromName: requireEnvVar(process.env.PAYLOAD_FROM_NAME, 'PAYLOAD_FROM_NAME'),
transportOptions: {
host: requireEnvVar(process.env.SMTP_HOST, 'SMTP_HOST'),
port: requireEnvVar(process.env.SMTP_PORT, 'SMTP_PORT'),
Expand Down

This file was deleted.

Loading

0 comments on commit 6a79073

Please sign in to comment.