From 6957c0416eff89f98a88384ff1522ca9dce8ab26 Mon Sep 17 00:00:00 2001 From: robin-maki <8201350+robin-maki@users.noreply.github.com> Date: Thu, 11 Jul 2024 09:34:37 +0000 Subject: [PATCH] =?UTF-8?q?=EA=B5=AC=EB=8F=85=20=EC=8A=A4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=83=88=20=ED=8F=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20(#2749)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/website/schema.graphql | 11 ++ apps/website/src/lib/enums.ts | 1 + .../src/lib/notification/maker/index.ts | 2 + .../src/lib/notification/maker/new-post.ts | 60 ++++++++++ .../server/graphql/schemas/notification.ts | 15 +++ .../src/lib/server/graphql/schemas/post.ts | 7 ++ .../lib/server/rest/routes/notification.ts | 13 +++ apps/website/src/lib/utils/notification.ts | 9 ++ .../src/routes/(default)/Header.svelte | 2 + .../(default)/NewPostNotification.svelte | 107 ++++++++++++++++++ .../routes/(default)/NotificationMenu.svelte | 4 + 11 files changed, 231 insertions(+) create mode 100644 apps/website/src/lib/notification/maker/new-post.ts create mode 100644 apps/website/src/routes/(default)/NewPostNotification.svelte diff --git a/apps/website/schema.graphql b/apps/website/schema.graphql index d1d4867279..0f4d2cba9e 100644 --- a/apps/website/schema.graphql +++ b/apps/website/schema.graphql @@ -435,6 +435,16 @@ input MuteTagInput { tagId: ID! } +type NewPostNotification implements IUserNotification { + actor: Profile + category: UserNotificationCategory! + createdAt: DateTime! + data: JSON! + id: ID! + post: Post! + state: UserNotificationState! +} + enum PaymentMethod { BANK_ACCOUNT CREDIT_CARD @@ -1321,6 +1331,7 @@ enum UserNotificationCategory { COMMENT DONATE EMOJI_REACTION + NEW_POST PURCHASE REPLY SUBSCRIBE diff --git a/apps/website/src/lib/enums.ts b/apps/website/src/lib/enums.ts index 156922bf8c..96c15d98f6 100644 --- a/apps/website/src/lib/enums.ts +++ b/apps/website/src/lib/enums.ts @@ -238,6 +238,7 @@ export const UserNotificationCategory = { COMMENT: 'COMMENT', DONATE: 'DONATE', EMOJI_REACTION: 'EMOJI_REACTION', + NEW_POST: 'NEW_POST', PURCHASE: 'PURCHASE', REPLY: 'REPLY', SUBSCRIBE: 'SUBSCRIBE', diff --git a/apps/website/src/lib/notification/maker/index.ts b/apps/website/src/lib/notification/maker/index.ts index 2997fc9de7..03837b4cce 100644 --- a/apps/website/src/lib/notification/maker/index.ts +++ b/apps/website/src/lib/notification/maker/index.ts @@ -1,5 +1,6 @@ import { commentNotificationMaker } from './comment'; import { emojiReactionNotificationMaker } from './emoji-reaction'; +import { spaceNewPostNotificationMaker } from './new-post'; import { purchaseNotificationMaker } from './purchase'; import { subscribeNotificationMaker } from './subscribe'; @@ -8,4 +9,5 @@ export const notificationMaker = { COMMENT: commentNotificationMaker, PURCHASE: purchaseNotificationMaker, SUBSCRIBE: subscribeNotificationMaker, + NEW_POST: spaceNewPostNotificationMaker, }; diff --git a/apps/website/src/lib/notification/maker/new-post.ts b/apps/website/src/lib/notification/maker/new-post.ts new file mode 100644 index 0000000000..4c7ef79381 --- /dev/null +++ b/apps/website/src/lib/notification/maker/new-post.ts @@ -0,0 +1,60 @@ +import { and, eq } from 'drizzle-orm'; +import { database, PostRevisions, Posts, Profiles, SpaceFollows, SpaceMembers, Spaces } from '$lib/server/database'; +import { createNotification, useFirstRow } from '$lib/server/utils'; +import type { NotificationMaker } from './type'; + +export const spaceNewPostNotificationMaker: NotificationMaker = async (postId: string) => { + const post = await database + .select({ + permalink: Posts.permalink, + title: PostRevisions.title, + userId: Posts.userId, + memberProfileId: SpaceMembers.profileId, + memberProfileName: Profiles.name, + spaceId: Spaces.id, + spaceSlug: Spaces.slug, + spaceName: Spaces.name, + }) + .from(Posts) + .innerJoin(Spaces, eq(Posts.spaceId, Spaces.id)) + .innerJoin(PostRevisions, eq(Posts.publishedRevisionId, PostRevisions.id)) + .innerJoin( + SpaceMembers, + and( + eq(SpaceMembers.spaceId, Posts.spaceId), + eq(SpaceMembers.userId, Posts.userId), + eq(SpaceMembers.state, 'ACTIVE'), + ), + ) + .innerJoin(Profiles, eq(SpaceMembers.profileId, Profiles.id)) + .where(and(eq(Posts.id, postId), eq(Posts.state, 'PUBLISHED'), eq(Posts.visibility, 'PUBLIC'))) + .then(useFirstRow); + + if (!post) { + return; + } + + const followerIds = await database + .select({ + userId: SpaceFollows.userId, + }) + .from(SpaceFollows) + .where(and(eq(SpaceFollows.spaceId, post.spaceId))) + .then((rows) => rows.map((row) => row.userId)); + + await Promise.all( + followerIds.map(async (userId) => { + await createNotification({ + userId, + category: 'NEW_POST', + actorId: post.memberProfileId, + data: { + postId, + }, + pushTitle: post.spaceName, + pushBody: `${post.title} 글이 올라왔어요.`, + pushPath: `/${post.spaceSlug}/${post.permalink}`, + }); + }), + ); +}; diff --git a/apps/website/src/lib/server/graphql/schemas/notification.ts b/apps/website/src/lib/server/graphql/schemas/notification.ts index 6800bf0fad..1ed1e4a923 100644 --- a/apps/website/src/lib/server/graphql/schemas/notification.ts +++ b/apps/website/src/lib/server/graphql/schemas/notification.ts @@ -36,6 +36,7 @@ IUserNotification.implement({ .with('SUBSCRIBE', () => 'SubscribeNotification') .with('COMMENT', () => 'CommentNotification') .with('EMOJI_REACTION', () => 'EmojiReactionNotification') + .with('NEW_POST', () => 'NewPostNotification') .run(), }); @@ -131,6 +132,20 @@ EmojiReactionNotification.implement({ }), }); +export const NewPostNotification = createObjectRef('NewPostNotification', UserNotifications); +NewPostNotification.implement({ + interfaces: [IUserNotification], + fields: (t) => ({ + post: t.field({ + type: Post, + resolve: async (notification) => { + const data = notification.data as { postId: string }; + return data.postId; + }, + }), + }), +}); + /** * * Inputs */ diff --git a/apps/website/src/lib/server/graphql/schemas/post.ts b/apps/website/src/lib/server/graphql/schemas/post.ts index ce7bf1d135..ab0c0b14c5 100644 --- a/apps/website/src/lib/server/graphql/schemas/post.ts +++ b/apps/website/src/lib/server/graphql/schemas/post.ts @@ -1625,6 +1625,13 @@ builder.mutationFields((t) => ({ await enqueueJob('indexPost', input.postId); await enqueueJob('notifyIndexNow', input.postId); + if (post.state !== 'PUBLISHED') { + await enqueueJob('createNotification', { + category: 'NEW_POST', + targetId: input.postId, + }); + } + return input.postId; }, }), diff --git a/apps/website/src/lib/server/rest/routes/notification.ts b/apps/website/src/lib/server/rest/routes/notification.ts index f5ff70f1ee..904f54246f 100644 --- a/apps/website/src/lib/server/rest/routes/notification.ts +++ b/apps/website/src/lib/server/rest/routes/notification.ts @@ -71,6 +71,19 @@ notification.get('/notification/:notificationId', async (request) => { return `/${posts[0].space.slug}/${posts[0].permalink}`; }) + .with({ category: 'NEW_POST' }, async ({ data }) => { + const posts = await database + .select({ permalink: Posts.permalink, space: { slug: Spaces.slug } }) + .from(Posts) + .innerJoin(Spaces, eq(Spaces.id, Posts.spaceId)) + .where(eq(Posts.id, data.postId)); + + if (posts.length === 0) { + return `/404`; + } + + return `/${posts[0].space.slug}/${posts[0].permalink}`; + }) .exhaustive(), }, }); diff --git a/apps/website/src/lib/utils/notification.ts b/apps/website/src/lib/utils/notification.ts index 3efafa6a41..750d170dc9 100644 --- a/apps/website/src/lib/utils/notification.ts +++ b/apps/website/src/lib/utils/notification.ts @@ -31,11 +31,20 @@ type EmojiReactionNotification = { }; }; +type NewPostNotification = { + category: 'NEW_POST'; + actorId: string; + data: { + postId: string; + }; +}; + export type Notification = ( | PurchaseNotification | SubscribeNotification | CommentNotification | EmojiReactionNotification + | NewPostNotification ) & { userId: string; }; diff --git a/apps/website/src/routes/(default)/Header.svelte b/apps/website/src/routes/(default)/Header.svelte index cf346312a4..f2cf5bb6e4 100644 --- a/apps/website/src/routes/(default)/Header.svelte +++ b/apps/website/src/routes/(default)/Header.svelte @@ -39,6 +39,8 @@ `), ); + $: console.log('$query', _query); + const createPost = graphql(` mutation DefaultLayout_Header_CreatePost_Mutation($input: CreatePostInput!) { createPost(input: $input) { diff --git a/apps/website/src/routes/(default)/NewPostNotification.svelte b/apps/website/src/routes/(default)/NewPostNotification.svelte new file mode 100644 index 0000000000..ce51f2a501 --- /dev/null +++ b/apps/website/src/routes/(default)/NewPostNotification.svelte @@ -0,0 +1,107 @@ + + + redirect($newPostNotification)} +> + + + + 새 포스트 + + {$newPostNotification.post.space?.name} 스페이스에서 {$newPostNotification.post.publishedRevision?.title ?? + '(제목 없음)'} 포스트가 발행되었어요 + + + {dayjs($newPostNotification.createdAt).fromNow()} + + + diff --git a/apps/website/src/routes/(default)/NotificationMenu.svelte b/apps/website/src/routes/(default)/NotificationMenu.svelte index cd9acd00cb..d3436d23e6 100644 --- a/apps/website/src/routes/(default)/NotificationMenu.svelte +++ b/apps/website/src/routes/(default)/NotificationMenu.svelte @@ -10,6 +10,7 @@ import { center, circle, flex } from '$styled-system/patterns'; import CommentNotification from './CommentNotification.svelte'; import EmojiReactionNotification from './EmojiReactionNotification.svelte'; + import NewPostNotification from './NewPostNotification.svelte'; import PurchaseNotification from './PurchaseNotification.svelte'; import SubscribeNotification from './SubscribeNotification.svelte'; import type { DefaultLayout_NotificationMenu_user } from '$glitch'; @@ -44,6 +45,7 @@ ...SubscribeNotification_subscribeNotification ...PurchaseNotification_purchaseNotification ...EmojiReactionNotification_emojiReactionNotification + ...NewPostNotification_newPostNotification } } `), @@ -229,6 +231,8 @@ {:else if notification.__typename === 'EmojiReactionNotification'} + {:else if notification.__typename === 'NewPostNotification'} + {/if} {:else}