Skip to content

Commit

Permalink
feat(Project): email notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
awinogradov committed Jul 25, 2023
1 parent ff8da02 commit a28a804
Show file tree
Hide file tree
Showing 2 changed files with 305 additions and 19 deletions.
155 changes: 155 additions & 0 deletions src/utils/worker/mail/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,158 @@ ${renderFooter()}`);
text: subject,
};
};

interface childProjectCreatedProps {
to: SendMailProps['to'];
childKey: string;
childTitle: string;
projectKey: string;
projectTitle: string;
author?: string;
}

export const childProjectCreated = async ({
to,
childKey,
childTitle,
projectKey,
projectTitle,
author = 'Somebody',
}: childProjectCreatedProps) => {
const subject = `🎉 New child project in #${projectKey}: ${projectTitle}`;
const html = md.render(`
🧑‍💻 **${author}** created new project **[${childKey}: ${childTitle}](${absUrl(
`/projects/${childKey}`,
)})** in **[#${projectKey}: ${projectTitle}](${absUrl(`/projects/${projectKey}`)})**.
${renderNotice()}
${renderFooter()}`);

return {
to,
subject,
html: withBaseTmplStyles(html),
text: subject,
};
};

interface childProjectDeletedProps {
to: SendMailProps['to'];
childKey: string;
childTitle: string;
projectKey: string;
projectTitle: string;
author?: string;
}

export const childProjectDeleted = async ({
to,
childKey,
childTitle,
projectKey,
projectTitle,
author = 'Somebody',
}: childProjectDeletedProps) => {
const subject = `🎉 Child project was removed from #${projectKey}: ${projectTitle}`;
const html = md.render(`
🧑‍💻 **${author}** removed project **[${childKey}: ${childTitle}](${absUrl(
`/projects/${childKey}`,
)})** from **[#${projectKey}: ${projectTitle}](${absUrl(`/projects/${projectKey}`)})**.
${renderNotice()}
${renderFooter()}`);

return {
to,
subject,
html: withBaseTmplStyles(html),
text: subject,
};
};

interface ProjectUpdatedEmailProps {
to: SendMailProps['to'];
key: string;
title: string;
updatedFields: {
title?: FieldDiff;
description?: FieldDiff;
};
author?: string;
}

export const projectUpdated = async ({
to,
key,
title,
updatedFields,
author = 'Somebody',
}: ProjectUpdatedEmailProps) => {
const subject = `ℹ️ Project #${key}: ${title} was updated`;
const html = md.render(`
🧑‍💻 **${author}** updated project **[#${key}: ${title}](${absUrl(`/projects/${key}`)})**.
${
updatedFields.title
? `
Title:
\`\`\` diff
- ${updatedFields.title[0]}
+ ${updatedFields.title[1]}
\`\`\`
`
: ''
}
${
updatedFields.description
? `
Description:
\`\`\` diff
- ${updatedFields.description[0]}
+ ${updatedFields.description[1]}
\`\`\`
`
: ''
}
}
${renderNotice()}
${renderFooter()}`);

return {
to,
subject,
html: withBaseTmplStyles(html),
text: subject,
};
};

interface ProjectTransferedProps {
to: SendMailProps['to'];
key: string;
title: string;
author?: string;
}

export const projectTransfered = async ({ to, key, title, author = 'Somebody' }: ProjectTransferedProps) => {
const subject = `Project #${key}: ${title} was transfered`;
const html = md.render(`
🧑‍💻 **${author}** transfered project **[${key}: ${title}](${absUrl(
`/projects/${key}`,
)})** to you. You are new owner. Congrats! 🎉
${notice}
${footer}`);

return {
to,
subject,
html: withBaseTmplStyles(html),
text: subject,
};
};
169 changes: 150 additions & 19 deletions trpc/router/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { ToggleSubscriptionSchema, queryWithFiltersSchema } from '../../src/sche
import { connectionMap } from '../queries/connections';
import { getProjectSchema } from '../queries/project';
import { fillProject, sqlGoalsFilter } from '../queries/sqlProject';
import { createEmailJob } from '../../src/utils/worker/create';
import { FieldDiff } from '../../src/types/common';

type WithId = { id: string };

Expand Down Expand Up @@ -364,23 +366,18 @@ export const project = router({
},
},
});

// await mailServer.sendMail({
// from: `"Fred Foo 👻" < ${ process.env.MAIL_USER }> `,
// to: '[email protected], [email protected]',
// subject: 'Hello ✔',
// text: `new post '${title}'`,
// html: `new post < b > ${ title } </>`,
// });
} catch (error: any) {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: String(error.message), cause: error });
}
}),
update: protectedProcedure.input(projectUpdateSchema).mutation(async ({ input: { id, parent, ...data } }) => {
update: protectedProcedure.input(projectUpdateSchema).mutation(async ({ input: { id, parent, ...data }, ctx }) => {
const project = await prisma.project.findUnique({
where: { id },
include: {
parent: true,
activity: { include: { user: true, ghost: true } },
participants: { include: { user: true, ghost: true } },
watchers: { include: { user: true, ghost: true } },
},
});

Expand All @@ -392,7 +389,7 @@ export const project = router({
const parentsToDisconnect = project.parent.filter((p) => !parent?.some((pr) => p.id === pr.id));

try {
return prisma.project.update({
const updatedProject = await prisma.project.update({
where: { id },
data: {
...data,
Expand All @@ -406,13 +403,117 @@ export const project = router({
},
});

// await mailServer.sendMail({
// from: `"Fred Foo 👻" <${process.env.MAIL_USER}>`,
// to: '[email protected], [email protected]',
// subject: 'Hello ✔',
// text: `new post '${title}'`,
// html: `new post <b>${title}</b>`,
// });
if (parentsToConnect) {
const newParents = await prisma.project.findMany({
where: {
id: {
in: parentsToConnect?.map(({ id }) => id),
},
},
include: {
activity: { include: { user: true, ghost: true } },
participants: { include: { user: true, ghost: true } },
watchers: { include: { user: true, ghost: true } },
},
});

await Promise.all(
newParents.map((parent) => {
const recipients = Array.from(
new Set(
[parent.activity, ...parent.participants, ...parent.watchers]
.filter(Boolean)
.filter((p) => p.user?.email !== ctx.session.user.email)
.map((r) => r.user?.email),
),
);

return recipients.length
? createEmailJob('childProjectCreated', {
to: recipients,
childKey: updatedProject.id,
childTitle: updatedProject.title,
projectKey: parent.id,
projectTitle: parent.title,
author: ctx.session.user.name || ctx.session.user.email,
})
: null;
}),
);
}

if (parentsToDisconnect) {
const oldParents = await prisma.project.findMany({
where: {
id: {
in: parentsToDisconnect?.map(({ id }) => id),
},
},
include: {
activity: { include: { user: true, ghost: true } },
participants: { include: { user: true, ghost: true } },
watchers: { include: { user: true, ghost: true } },
},
});

await Promise.all(
oldParents.map((parent) => {
const recipients = Array.from(
new Set(
[parent.activity, ...parent.participants, ...parent.watchers]
.filter(Boolean)
.filter((p) => p.user?.email !== ctx.session.user.email)
.map((r) => r.user?.email),
),
);

return recipients.length
? createEmailJob('childProjectDeleted', {
to: recipients,
childKey: updatedProject.id,
childTitle: updatedProject.title,
projectKey: parent.id,
projectTitle: parent.title,
author: ctx.session.user.name || ctx.session.user.email,
})
: null;
}),
);
}

const updatedFields: {
title?: FieldDiff;
description?: FieldDiff;
} = {};

if (updatedProject.title !== project.title) {
updatedFields.title = [project.title, updatedProject.title];
}

if (updatedProject.description !== project.description) {
updatedFields.description = [project.description, updatedProject.description];
}

const recipients = Array.from(
new Set(
[...project.participants, ...project.watchers, project.activity]
.filter(Boolean)
.filter((p) => p.user?.email !== ctx.session.user.email)
.map((r) => r.user?.email),
),
);

if (recipients.length) {
await createEmailJob('projectUpdated', {
to: recipients,
key: project.id,
title: project.title,
updatedFields,
author: ctx.session.user.name || ctx.session.user.email,
});
}

return updatedProject;
} catch (error: any) {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: String(error.message), cause: error });
}
Expand Down Expand Up @@ -460,14 +561,44 @@ export const project = router({
}),
transferOwnership: protectedProcedure
.input(projectTransferOwnershipSchema)
.mutation(({ input: { id, activityId } }) => {
.mutation(async ({ input: { id, activityId }, ctx }) => {
const [project, newOwner] = await Promise.all([
prisma.project.findUnique({
where: { id },
}),
prisma.activity.findUnique({
where: { id: activityId },
include: {
user: true,
ghost: true,
},
}),
]);

if (!project) {
return null;
}

if (!newOwner) {
return null;
}

try {
return prisma.project.update({
const transferedProject = await prisma.project.update({
where: { id },
data: {
activityId,
},
});

await createEmailJob('projectTransfered', {
to: [newOwner.user?.email],
key: project.id,
title: project.title,
author: ctx.session.user.name || ctx.session.user.email,
});

return transferedProject;
} catch (error: any) {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: String(error.message), cause: error });
}
Expand Down

0 comments on commit a28a804

Please sign in to comment.