Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

구독 스페이스 새 포스트 알림 #2749

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/website/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1321,6 +1331,7 @@ enum UserNotificationCategory {
COMMENT
DONATE
EMOJI_REACTION
NEW_POST
PURCHASE
REPLY
SUBSCRIBE
Expand Down
1 change: 1 addition & 0 deletions apps/website/src/lib/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions apps/website/src/lib/notification/maker/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -8,4 +9,5 @@ export const notificationMaker = {
COMMENT: commentNotificationMaker,
PURCHASE: purchaseNotificationMaker,
SUBSCRIBE: subscribeNotificationMaker,
NEW_POST: spaceNewPostNotificationMaker,
};
60 changes: 60 additions & 0 deletions apps/website/src/lib/notification/maker/new-post.ts
Original file line number Diff line number Diff line change
@@ -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}`,
});
}),
);
};
15 changes: 15 additions & 0 deletions apps/website/src/lib/server/graphql/schemas/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ IUserNotification.implement({
.with('SUBSCRIBE', () => 'SubscribeNotification')
.with('COMMENT', () => 'CommentNotification')
.with('EMOJI_REACTION', () => 'EmojiReactionNotification')
.with('NEW_POST', () => 'NewPostNotification')
.run(),
});

Expand Down Expand Up @@ -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
*/
Expand Down
7 changes: 7 additions & 0 deletions apps/website/src/lib/server/graphql/schemas/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
}),
Expand Down
13 changes: 13 additions & 0 deletions apps/website/src/lib/server/rest/routes/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
},
});
Expand Down
9 changes: 9 additions & 0 deletions apps/website/src/lib/utils/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
2 changes: 2 additions & 0 deletions apps/website/src/routes/(default)/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
`),
);

$: console.log('$query', _query);

const createPost = graphql(`
mutation DefaultLayout_Header_CreatePost_Mutation($input: CreatePostInput!) {
createPost(input: $input) {
Expand Down
107 changes: 107 additions & 0 deletions apps/website/src/routes/(default)/NewPostNotification.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script lang="ts">
import dayjs from 'dayjs';
import ky from 'ky';
import IconCoin from '~icons/tabler/coin';
import { goto } from '$app/navigation';
import { fragment, graphql } from '$glitch';
import { mixpanel } from '$lib/analytics';
import { Icon } from '$lib/components';
import { css } from '$styled-system/css';
import type { NewPostNotification_newPostNotification } from '$glitch';

let _newPostNotification: NewPostNotification_newPostNotification;
export { _newPostNotification as $newPostNotification };

$: newPostNotification = fragment(
_newPostNotification,
graphql(`
fragment NewPostNotification_newPostNotification on NewPostNotification {
id
category
state
createdAt

actor {
id
name
}

post {
id

publishedRevision {
id
title
}

space {
id
slug
name
}
}
}
`),
);

const markNotificationAsRead = graphql(`
mutation NewPostNotification_MarkNotificationAsRead_Mutation($input: MarkNotificationAsReadInput!) {
markNotificationAsRead(input: $input) {
id
state
}
}
`);

const redirect = async (notification: typeof $newPostNotification) => {
if (notification.state === 'UNREAD') {
await markNotificationAsRead({ notificationId: notification.id });
mixpanel.track('user:notification-state:read');
}

const resp = await ky.get(`/api/notification/${notification.id}`);
await goto(resp.url);
};
</script>

<button
class={css(
{
display: 'flex',
alignItems: 'flex-start',
gap: '4px',
borderBottomWidth: '1px',
borderBottomColor: 'gray.100',
paddingX: '16px',
paddingY: '20px',
width: 'full',
transition: 'common',
_hover: { backgroundColor: 'gray.100' },
},
$newPostNotification.state === 'UNREAD' && { backgroundColor: 'gray.50' },
)}
type="button"
on:click={() => redirect($newPostNotification)}
>
<Icon style={css.raw({ marginTop: '3px' })} icon={IconCoin} size={12} />

<div class={css({ flexGrow: '1', textAlign: 'left' })}>
<div class={css({ fontSize: '13px', color: 'gray.500' })}>새 포스트</div>
<div class={css({ fontSize: '14px', fontWeight: 'medium' })}>
{$newPostNotification.post.space?.name} 스페이스에서 {$newPostNotification.post.publishedRevision?.title ??
'(제목 없음)'} 포스트가 발행되었어요
</div>
<time
class={css({
display: 'inline-block',
width: 'full',
fontSize: '11px',
textAlign: 'right',
color: 'gray.400',
})}
datetime={$newPostNotification.createdAt}
>
{dayjs($newPostNotification.createdAt).fromNow()}
</time>
</div>
</button>
4 changes: 4 additions & 0 deletions apps/website/src/routes/(default)/NotificationMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +45,7 @@
...SubscribeNotification_subscribeNotification
...PurchaseNotification_purchaseNotification
...EmojiReactionNotification_emojiReactionNotification
...NewPostNotification_newPostNotification
}
}
`),
Expand Down Expand Up @@ -229,6 +231,8 @@
<PurchaseNotification $purchaseNotification={notification} />
{:else if notification.__typename === 'EmojiReactionNotification'}
<EmojiReactionNotification $emojiReactionNotification={notification} via="menu" />
{:else if notification.__typename === 'NewPostNotification'}
<NewPostNotification $newPostNotification={notification} />
{/if}
</li>
{:else}
Expand Down