diff --git a/apps/recnet-api/package.json b/apps/recnet-api/package.json index 7aa23d46..128486fb 100644 --- a/apps/recnet-api/package.json +++ b/apps/recnet-api/package.json @@ -1,4 +1,4 @@ { "name": "recnet-api", - "version": "1.8.2" + "version": "1.8.3" } diff --git a/apps/recnet-api/src/database/repository/user.repository.ts b/apps/recnet-api/src/database/repository/user.repository.ts index 1811137c..60439d59 100644 --- a/apps/recnet-api/src/database/repository/user.repository.ts +++ b/apps/recnet-api/src/database/repository/user.repository.ts @@ -107,7 +107,7 @@ export default class UserRepository { select: user.select, }); - await prisma.inviteCode.update({ + const inviteCode = await prisma.inviteCode.update({ where: { code: createUserInput.inviteCode }, data: { usedById: userInTransaction.id, @@ -115,6 +115,14 @@ export default class UserRepository { }, }); + // follow the person who gave the invite code + await prisma.followingRecord.create({ + data: { + followedById: userInTransaction.id, + followingId: inviteCode.ownerId, + }, + }); + return userInTransaction; }); } diff --git a/apps/recnet-api/src/modules/slack/slack.service.ts b/apps/recnet-api/src/modules/slack/slack.service.ts index 3a5913d6..886359c8 100644 --- a/apps/recnet-api/src/modules/slack/slack.service.ts +++ b/apps/recnet-api/src/modules/slack/slack.service.ts @@ -24,12 +24,16 @@ export class SlackService { ): Promise { let result; try { - const slackMessage = weeklyDigestSlackTemplate( + const weeklyDigest = weeklyDigestSlackTemplate( cutoff, content, this.appConfig.nodeEnv ); - result = await this.transporter.sendDirectMessage(user, slackMessage); + result = await this.transporter.sendDirectMessage( + user, + weeklyDigest.messageBlocks, + weeklyDigest.notificationText + ); } catch (e) { return { success: false, userId: user.id }; } diff --git a/apps/recnet-api/src/modules/slack/slack.type.ts b/apps/recnet-api/src/modules/slack/slack.type.ts index e2224a0e..9db7a02e 100644 --- a/apps/recnet-api/src/modules/slack/slack.type.ts +++ b/apps/recnet-api/src/modules/slack/slack.type.ts @@ -1,5 +1,9 @@ +import { SlackBlockDto } from "slack-block-builder"; + export type SendSlackResult = { success: boolean; skip?: boolean; userId?: string; }; + +export type SlackMessageBlocks = Readonly[]; diff --git a/apps/recnet-api/src/modules/slack/templates/weekly-digest.template.ts b/apps/recnet-api/src/modules/slack/templates/weekly-digest.template.ts index 67d0240f..3b539eda 100644 --- a/apps/recnet-api/src/modules/slack/templates/weekly-digest.template.ts +++ b/apps/recnet-api/src/modules/slack/templates/weekly-digest.template.ts @@ -1,19 +1,79 @@ +import groupBy from "lodash.groupby"; +import { BlockCollection, Md, Blocks, BlockBuilder } from "slack-block-builder"; + import { WeeklyDigestContent } from "@recnet-api/modules/subscription/subscription.type"; import { formatDate } from "@recnet/recnet-date-fns"; +import type { SlackMessageBlocks } from "../slack.type"; + +export type WeeklyDigestDto = { + notificationText?: string; + messageBlocks: SlackMessageBlocks; +}; + export const weeklyDigestSlackTemplate = ( cutoff: Date, content: WeeklyDigestContent, nodeEnv: string -): string => { - const subject = `${nodeEnv !== "production" && "[DEV] "}๐Ÿ“ฌ Your Weekly Digest for ${formatDate(cutoff)}`; - const unusedInviteCodes = `You have ${content.numUnusedInviteCodes} unused invite codes! Share the love โค๏ธ`; - const latestAnnouncement = content.latestAnnouncement - ? `๐Ÿ“ข ${content.latestAnnouncement.title} \n ${content.latestAnnouncement.content}` - : ""; - const recsUrls = content.recs.map( - (rec) => `[${rec.article.title}](https://recnet.io/rec/${rec.id})` +): WeeklyDigestDto => { + const { recs, numUnusedInviteCodes, latestAnnouncement } = content; + + const recsGroupByTitle = groupBy(recs, (rec) => { + const titleLowercase = rec.article.title.toLowerCase(); + const words = titleLowercase.split(" ").filter((w) => w.length > 0); + return words.join(""); + }); + const recSection = Object.values(recsGroupByTitle).map((recs) => { + const article = recs[0].article; + return [ + Blocks.Section({ + text: `${Md.bold(Md.link(article.link, article.title))}\n${Md.italic(article.author)} - ${article.year}`, + }), + ...recs.map((rec) => + Blocks.Section({ + text: `${Md.link(`https://recnet.io/${rec.user.handle}`, rec.user.displayName)}${rec.isSelfRec ? Md.italic("(Self-Rec)") : ""}: ${rec.description} (${Md.link(`https://recnet.io/rec/${rec.id}`, "view")})`, + }) + ), + Blocks.Divider(), + ]; + }); + + const footer: BlockBuilder[] = []; + if (numUnusedInviteCodes > 0) { + footer.push( + Blocks.Section({ + text: `โค๏ธ You have ${Md.bold(`${numUnusedInviteCodes}`)} unused invite codes. Share the love!`, + }) + ); + } + if (latestAnnouncement) { + footer.push( + Blocks.Section({ + text: `๐Ÿ“ข Announcement - ${latestAnnouncement.title}: ${latestAnnouncement.content}`, + }) + ); + } + + const messageBlocks = BlockCollection( + Blocks.Header({ + text: `${nodeEnv !== "production" && "[DEV] "}๐Ÿ“ฌ Your Weekly Digest for ${formatDate(cutoff)}`, + }), + Blocks.Section({ + text: `You have ${Md.bold(`${recs.length}`)} recommendations this week!`, + }), + Blocks.Section({ + text: "Check out these rec'd paper for you from your network!", + }), + Blocks.Divider(), + ...recSection.flat(), + ...footer, + Blocks.Section({ + text: `๐Ÿ‘€ Any interesting read this week? ${Md.link("https://recnet.io", "Share with your network!")}`, + }) ); - return `${subject}\nYou have ${content.recs.length} recommendations this week!\nCheck out these rec'd paper for you from your network!\n${unusedInviteCodes}\n${latestAnnouncement}\n${recsUrls.join("\n")} \n\nAny interesting read this week? ๐Ÿ‘€\nShare with your network: https://recnet.io/`; + return { + notificationText: `๐Ÿ“ฌ Your RecNet weekly digest has arrived!`, + messageBlocks, + }; }; diff --git a/apps/recnet-api/src/modules/slack/transporters/slack.transporter.ts b/apps/recnet-api/src/modules/slack/transporters/slack.transporter.ts index 95af04f5..647ccb3e 100644 --- a/apps/recnet-api/src/modules/slack/transporters/slack.transporter.ts +++ b/apps/recnet-api/src/modules/slack/transporters/slack.transporter.ts @@ -13,7 +13,7 @@ import { SLACK_RETRY_DURATION_MS, SLACK_RETRY_LIMIT, } from "../slack.const"; -import { SendSlackResult } from "../slack.type"; +import { SendSlackResult, SlackMessageBlocks } from "../slack.type"; @Injectable() export class SlackTransporter { @@ -31,7 +31,8 @@ export class SlackTransporter { public async sendDirectMessage( user: DbUser, - message: string + message: SlackMessageBlocks, + notificationText?: string ): Promise { if ( this.appConfig.nodeEnv !== "production" && @@ -45,7 +46,7 @@ export class SlackTransporter { while (retryCount < SLACK_RETRY_LIMIT) { try { const slackId = await this.getUserSlackId(user); - await this.postDirectMessage(slackId, message); + await this.postDirectMessage(slackId, message, notificationText); return { success: true }; } catch (error) { retryCount++; @@ -82,7 +83,8 @@ export class SlackTransporter { private async postDirectMessage( userSlackId: string, - message: string + message: SlackMessageBlocks, + notificationText?: string ): Promise { // Open a direct message conversation const conversationResp = await this.client.conversations.open({ @@ -100,7 +102,8 @@ export class SlackTransporter { // Send the message await this.client.chat.postMessage({ channel: conversationId, - text: message, + text: notificationText, + blocks: message, }); } } diff --git a/apps/recnet/CHANGELOG.md b/apps/recnet/CHANGELOG.md index c27feee8..253cb639 100644 --- a/apps/recnet/CHANGELOG.md +++ b/apps/recnet/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [1.16.2](https://github.com/lil-lab/recnet/compare/recnet-web-v1.16.1...recnet-web-v1.16.2) (2024-11-18) + + +### Features + +* add new slack msg template ([7807854](https://github.com/lil-lab/recnet/commit/780785445461f437b93ec1e9f325470120f38d98)) +* finish update slack msg template ([3860d5e](https://github.com/lil-lab/recnet/commit/3860d5e2ac7b92cb162aa5fc3b10073a85fbc518)) +* follow the person who invites the user ([2f213ae](https://github.com/lil-lab/recnet/commit/2f213ae8c5cf992281ebfc8ccac8f1fc9399fb92)) +* install slack block builder ([4d771a6](https://github.com/lil-lab/recnet/commit/4d771a61da05415a78078cb4abd0528b73c7f0b7)) + + +### Bug Fixes + +* use prisma instance inside the transaction ([75e5bcd](https://github.com/lil-lab/recnet/commit/75e5bcde05941e9d5dd20dca1d6e5b214e593395)) + ## [1.16.1](https://github.com/lil-lab/recnet/compare/recnet-web-v1.16.0...recnet-web-v1.16.1) (2024-11-08) diff --git a/apps/recnet/package.json b/apps/recnet/package.json index 84688f14..2892f969 100644 --- a/apps/recnet/package.json +++ b/apps/recnet/package.json @@ -1,6 +1,6 @@ { "name": "recnet", - "version": "1.16.1", + "version": "1.16.2", "commit-and-tag-version": { "skip": { "commit": true diff --git a/apps/recnet/src/app/rec/[id]/opengraph-image.tsx b/apps/recnet/src/app/rec/[id]/opengraph-image.tsx index 11a341ab..fc6d19c5 100644 --- a/apps/recnet/src/app/rec/[id]/opengraph-image.tsx +++ b/apps/recnet/src/app/rec/[id]/opengraph-image.tsx @@ -14,6 +14,9 @@ export const size = { export const contentType = "image/png"; +const fallbackImage = + "https://recnet.io/_next/image?url=%2Frecnet-logo.webp&w=64&q=100"; + /** Styling: You can write Tailwind CSS via "tw" prop here. However, our radix-theme preset won't be available here. @@ -25,19 +28,22 @@ export default async function Image({ params }: { params: { id: string } }) { const { rec } = await serverClient.getRecById({ id, }); + const linkPreviewMetadata = await serverClient.getLinkPreviewMetadata({ + url: rec.article.link, + }); return new ImageResponse( ( // ImageResponse JSX element
-
+
{/* eslint-disable-next-line */}

RecNet

-
{rec.article.title}
-

{rec.description}

-

{rec.article.author}

-
- {/* eslint-disable-next-line */} - {rec.user.displayName} - {`${rec.user.displayName} ยท ${formatDate(new Date(rec.cutoff))}`} - {rec.isSelfRec ? ( -
- Self Rec +
+
+ {/* eslint-disable-next-line */} + {rec.user.displayName} +

+ {`${rec.user.displayName}`} + {formatDate(new Date(rec.cutoff))} +

+
+

+ {rec.description} +

+
+
+ {/* eslint-disable-next-line */} + {"link-metadata-logo"} +
+
+

{rec.article.title}

+

{rec.article.author}

+

{rec.article.link.replace("https://", "").split("/")[0]}

- ) : null} +
), diff --git a/package.json b/package.json index 1bd91729..bd127b45 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", "server-only": "^0.0.1", + "slack-block-builder": "^2.8.0", "sonner": "^1.4.32", "tailwind-merge": "^2.2.1", "tailwindcss-radix": "^3.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb488afd..ab08fde3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,6 +193,9 @@ dependencies: server-only: specifier: ^0.0.1 version: 0.0.1 + slack-block-builder: + specifier: ^2.8.0 + version: 2.8.0 sonner: specifier: ^1.4.32 version: 1.4.32(react-dom@18.2.0)(react@18.2.0) @@ -18416,6 +18419,10 @@ packages: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true + /slack-block-builder@2.8.0: + resolution: {integrity: sha512-iisM+j99iKRuQFVfdWo0FiszDAl3r8Snq704oZH6C0RbDqvoVQStiptt6Y7kc6RX/5hSAqTqjhgvZ/di8cvaIA==} + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'}