[web] workspace/discussions: add notifications popover with API integration (#1693)
This commit is contained in:
parent
d99fb9643b
commit
3801daf11c
|
|
@ -0,0 +1,337 @@
|
|||
import { InterviewsDiscussionCommentDomain } from '@prisma/client';
|
||||
import { useUser } from '@supabase/auth-helpers-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import RelativeTimestamp from '~/components/common/datetime/RelativeTimestamp';
|
||||
import { FormattedMessage } from '~/components/intl';
|
||||
import Anchor from '~/components/ui/Anchor';
|
||||
import Avatar from '~/components/ui/Avatar';
|
||||
import RichText from '~/components/ui/RichTextEditor/RichText';
|
||||
import Text from '~/components/ui/Text';
|
||||
import { themeTextColor } from '~/components/ui/theme';
|
||||
|
||||
import type {
|
||||
InterviewsActivityExtended,
|
||||
InterviewsCommentActivity,
|
||||
InterviewsUpvoteActivity,
|
||||
} from './types';
|
||||
|
||||
type Props = Readonly<{
|
||||
activity: InterviewsActivityExtended;
|
||||
variant?: 'full' | 'minimal';
|
||||
}>;
|
||||
|
||||
export default function InterviewsActivityItem({
|
||||
activity,
|
||||
variant = 'full',
|
||||
}: Props) {
|
||||
const { actor, createdAt, question } = activity;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'relative flex gap-x-3',
|
||||
'px-4 py-5',
|
||||
'transition-colors',
|
||||
'hover:bg-neutral-50 dark:hover:bg-neutral-800/40',
|
||||
)}>
|
||||
<Anchor className="absolute inset-0" href={question.href} />
|
||||
<Avatar
|
||||
alt={actor.name ?? actor.username}
|
||||
size="xs"
|
||||
src={actor.avatarUrl ?? ''}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<Text color="secondary" size={variant === 'full' ? 'body2' : 'body3'}>
|
||||
{activity.category === 'DISCUSSION_UPVOTE' ? (
|
||||
<UpvoteActivityMessage activity={activity} />
|
||||
) : (
|
||||
<CommentActivityMessage activity={activity} />
|
||||
)}
|
||||
{` • `}
|
||||
<RelativeTimestamp timestamp={createdAt} />
|
||||
</Text>
|
||||
<RichText
|
||||
className={clsx(variant === 'minimal' && 'text-xs')}
|
||||
color="body"
|
||||
size={variant === 'full' ? 'sm' : 'custom'}
|
||||
value={
|
||||
activity.category === 'DISCUSSION_UPVOTE'
|
||||
? activity.vote.comment.body
|
||||
: activity.comment.body
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommentActivityMessage({
|
||||
activity,
|
||||
}: Readonly<{
|
||||
activity: InterviewsCommentActivity;
|
||||
}>) {
|
||||
const user = useUser();
|
||||
const { comment, question } = activity;
|
||||
const bold = (chunks: React.ReactNode) => (
|
||||
<Text size="inherit" weight="medium">
|
||||
{chunks}
|
||||
</Text>
|
||||
);
|
||||
const questionLink = (chunks: React.ReactNode) => (
|
||||
<Anchor
|
||||
className={clsx('relative', themeTextColor)}
|
||||
href={question.href}
|
||||
variant="flat">
|
||||
{chunks}
|
||||
</Anchor>
|
||||
);
|
||||
|
||||
// Check if the current user is the actor of the activity
|
||||
if (user?.id === activity.actorId) {
|
||||
if (comment.parentComment != null) {
|
||||
// User commented on their own reply
|
||||
if (
|
||||
comment.parentComment.author.id === activity.actorId ||
|
||||
comment.repliedTo?.author.id === activity.actorId
|
||||
) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>You</bold> replied to your comment on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for commenting on own reply"
|
||||
id="GVJ/Mh"
|
||||
values={{
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.comment.domain,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// User commented on others reply
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>You</bold> replied to <bold>{recipientName}</bold>'s comment on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for commenting on others reply"
|
||||
id="PDrcW2"
|
||||
values={{
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.comment.domain,
|
||||
),
|
||||
recipientName: comment.repliedTo
|
||||
? comment.repliedTo.author.name ??
|
||||
comment.repliedTo.author.username
|
||||
: comment.parentComment.author.name ??
|
||||
comment.parentComment.author.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// User left a comment
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>You</bold> left a comment on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for commenting on own comment"
|
||||
id="IKmNeN"
|
||||
values={{
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.comment.domain,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Other commented on user's reply
|
||||
if (comment.parentCommentId != null) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>{actorName}</bold> replied to your comment on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for replying to someone else's reply"
|
||||
id="tRaX9t"
|
||||
values={{
|
||||
actorName: activity.actor.name ?? activity.actor.username,
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.comment.domain,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Other commented on user's comment
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>{actorName}</bold> replied to your comment on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for replying to someone else's comment"
|
||||
id="mO2nTD"
|
||||
values={{
|
||||
actorName: activity.actor.name ?? activity.actor.username,
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.comment.domain,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UpvoteActivityMessage({
|
||||
activity,
|
||||
}: Readonly<{ activity: InterviewsUpvoteActivity }>) {
|
||||
const user = useUser();
|
||||
const { question, vote } = activity;
|
||||
const { comment } = vote;
|
||||
const bold = (chunks: React.ReactNode) => (
|
||||
<Text size="inherit" weight="medium">
|
||||
{chunks}
|
||||
</Text>
|
||||
);
|
||||
const questionLink = (chunks: React.ReactNode) => (
|
||||
<Anchor className={themeTextColor} href={question.href} variant="flat">
|
||||
{chunks}
|
||||
</Anchor>
|
||||
);
|
||||
|
||||
// Check if the current user is the actor of the activity
|
||||
if (user?.id === activity.actorId) {
|
||||
if (comment.parentCommentId != null) {
|
||||
// User upvoted their own reply
|
||||
if (comment.author.id === activity.actorId) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>You</bold> upvoted your reply on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for upvoting own reply"
|
||||
id="1Gm4Mr"
|
||||
values={{
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.vote.comment.domain,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// User upvoted to others reply
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>You</bold> upvoted <bold>{recipientName}</bold>'s reply on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for upvoting others reply"
|
||||
id="ZvWM/t"
|
||||
values={{
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.vote.comment.domain,
|
||||
),
|
||||
recipientName: comment.author.name ?? comment.author.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// User upvoted their own comment
|
||||
if (comment.author.id === activity.actorId) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>You</bold> upvoted your comment on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for upvoting own comment"
|
||||
id="CxKzGg"
|
||||
values={{
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.vote.comment.domain,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>You</bold> upvoted <bold>{recipientName}</bold>'s comment on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for upvoting others comment"
|
||||
id="ulVzMu"
|
||||
values={{
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.vote.comment.domain,
|
||||
),
|
||||
recipientName: comment.author.name ?? comment.author.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// Other upvoted user's reply
|
||||
if (activity.vote.comment.parentCommentId != null) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>{actorName}</bold> upvoted your reply on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for upvoting someone else's reply"
|
||||
id="bErRk/"
|
||||
values={{
|
||||
actorName: activity.actor.name ?? activity.actor.username,
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.vote.comment.domain,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Other upvoted user's comment
|
||||
return (
|
||||
<FormattedMessage
|
||||
defaultMessage="<bold>{actorName}</bold> upvoted your comment on <questionLink>{questionTitle}</questionLink>"
|
||||
description="Activity message for upvoting someone else's comment"
|
||||
id="QfATOE"
|
||||
values={{
|
||||
actorName: activity.actor.name ?? activity.actor.username,
|
||||
bold,
|
||||
questionLink,
|
||||
questionTitle: getQuestionTitle(
|
||||
activity.question.title,
|
||||
activity.vote.comment.domain,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getQuestionTitle(
|
||||
title: string,
|
||||
domain: InterviewsDiscussionCommentDomain,
|
||||
) {
|
||||
if (domain === InterviewsDiscussionCommentDomain.OFFICIAL_SOLUTION) {
|
||||
return `${title} (Official solution)`;
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import type {
|
||||
InterviewsActivity,
|
||||
InterviewsDiscussionComment,
|
||||
} from '@prisma/client';
|
||||
|
||||
import type { QuestionFormat } from '../questions/common/QuestionsTypes';
|
||||
|
||||
type InterviewsCommonActivity = Readonly<{
|
||||
actor: {
|
||||
avatarUrl: string | null;
|
||||
id: string;
|
||||
name: string | null;
|
||||
username: string;
|
||||
};
|
||||
question: {
|
||||
format: QuestionFormat;
|
||||
href: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
};
|
||||
recipient?: {
|
||||
avatarUrl: string | null;
|
||||
id: string;
|
||||
name: string | null;
|
||||
username: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type InterviewsCommentActivity = InterviewsCommonActivity &
|
||||
Omit<InterviewsActivity, 'category'> &
|
||||
Readonly<{
|
||||
category: 'DISCUSSION';
|
||||
comment: InterviewsDiscussionComment & {
|
||||
author: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
username: string;
|
||||
};
|
||||
parentComment?: {
|
||||
author: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
username: string;
|
||||
};
|
||||
};
|
||||
repliedTo?: {
|
||||
author: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
username: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
export type InterviewsUpvoteActivity = InterviewsCommonActivity &
|
||||
InterviewsCommonActivity &
|
||||
Omit<InterviewsActivity, 'category'> &
|
||||
Readonly<{
|
||||
actor: {
|
||||
avatarUrl: string | null;
|
||||
id: string;
|
||||
name: string | null;
|
||||
username: string;
|
||||
};
|
||||
category: 'DISCUSSION_UPVOTE';
|
||||
vote: {
|
||||
comment: InterviewsDiscussionComment & {
|
||||
author: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
username: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
export type InterviewsActivityExtended =
|
||||
| InterviewsCommentActivity
|
||||
| InterviewsUpvoteActivity;
|
||||
|
|
@ -26,7 +26,11 @@ export default function InterviewsNotification() {
|
|||
return (
|
||||
<Popover
|
||||
align="start"
|
||||
className={clsx('overflow-y-auto', 'mr-6 w-[464px]')}
|
||||
className={clsx(
|
||||
'thin-scrollbar max-h-[50vh] overflow-y-auto',
|
||||
'mr-6 w-[464px]',
|
||||
'!p-3',
|
||||
)}
|
||||
open={showNotification}
|
||||
trigger={
|
||||
<div className="relative">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import { trpc } from '~/hooks/trpc';
|
||||
|
||||
import InterviewsActivityItem from '../activity/InterviewsActivityItem';
|
||||
import type { InterviewsActivityExtended } from '../activity/types';
|
||||
import InterviewsNotificationUnreadIndicator from './InterviewsNotificationUnreadIndicator';
|
||||
|
||||
type Props = Readonly<{
|
||||
activity: InterviewsActivityExtended;
|
||||
closeNotification: () => void;
|
||||
variant?: 'full' | 'minimal';
|
||||
}>;
|
||||
|
||||
export default function InterviewsNotificationItem({
|
||||
activity,
|
||||
closeNotification,
|
||||
variant = 'full',
|
||||
}: Props) {
|
||||
const trcUtils = trpc.useUtils();
|
||||
const { read } = activity;
|
||||
const markAsRead = trpc.notifications.markAsRead.useMutation({
|
||||
onSuccess: () => {
|
||||
trcUtils.notifications.list.invalidate();
|
||||
trcUtils.notifications.getUnreadCount.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
function onClick() {
|
||||
if (!read) {
|
||||
markAsRead.mutate({
|
||||
id: activity.id,
|
||||
});
|
||||
}
|
||||
closeNotification();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative" onClick={onClick}>
|
||||
<InterviewsActivityItem activity={activity} variant={variant} />
|
||||
{!read && (
|
||||
<InterviewsNotificationUnreadIndicator className="absolute right-1.5 top-1.5" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,19 +1,36 @@
|
|||
import clsx from 'clsx';
|
||||
import { RiNotification3Line } from 'react-icons/ri';
|
||||
import { RiArrowRightLine, RiNotification3Line } from 'react-icons/ri';
|
||||
import url from 'url';
|
||||
|
||||
import { trpc } from '~/hooks/trpc';
|
||||
|
||||
import { FormattedMessage } from '~/components/intl';
|
||||
import { useIntl } from '~/components/intl';
|
||||
import Button from '~/components/ui/Button';
|
||||
import Spinner from '~/components/ui/Spinner';
|
||||
import Text from '~/components/ui/Text';
|
||||
import {
|
||||
themeDivideEmphasizeColor,
|
||||
themeDivideColor,
|
||||
themeTextSubtitleColor,
|
||||
} from '~/components/ui/theme';
|
||||
|
||||
import InterviewsNotificationItem from './InterviewsNotificationItem';
|
||||
|
||||
const MAX_ITEMS_TO_SHOW = 4;
|
||||
|
||||
type Props = Readonly<{ closeNotification: () => void }>;
|
||||
|
||||
export default function InterviewsNotificationPopoverContent(_: Props) {
|
||||
const isLoading = false;
|
||||
const notifications = [];
|
||||
export default function InterviewsNotificationPopoverContent({
|
||||
closeNotification,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const { data, isLoading } = trpc.notifications.list.useQuery({
|
||||
pagination: {
|
||||
limit: MAX_ITEMS_TO_SHOW + 1,
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
const { notifications } = data ?? {};
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -48,8 +65,39 @@ export default function InterviewsNotificationPopoverContent(_: Props) {
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={clsx('divide-y', themeDivideEmphasizeColor)}>
|
||||
Notifications
|
||||
<div className={clsx('divide-y', themeDivideColor)}>
|
||||
{notifications
|
||||
?.slice(0, MAX_ITEMS_TO_SHOW)
|
||||
.map((notification) => (
|
||||
<InterviewsNotificationItem
|
||||
key={notification.id}
|
||||
activity={notification}
|
||||
closeNotification={closeNotification}
|
||||
variant="minimal"
|
||||
/>
|
||||
))}
|
||||
|
||||
{(notifications?.length ?? 0) > MAX_ITEMS_TO_SHOW && (
|
||||
<div className={clsx('w-full py-4', 'flex justify-center')}>
|
||||
<Button
|
||||
href={url.format({
|
||||
pathname: '/profile',
|
||||
query: {
|
||||
tab: 'notifications',
|
||||
},
|
||||
})}
|
||||
icon={RiArrowRightLine}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: 'See all notifications',
|
||||
description: 'Label for view all notifications button',
|
||||
id: 'yvKbxT',
|
||||
})}
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
onClick={closeNotification}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type {
|
||||
InterviewsCommentActivity,
|
||||
InterviewsUpvoteActivity,
|
||||
} from '~/components/interviews/activity/types';
|
||||
|
||||
import { fetchQuestion } from '~/db/QuestionsListReader';
|
||||
import { unhashQuestion } from '~/db/QuestionsUtils';
|
||||
import prisma from '~/server/prisma';
|
||||
|
||||
import { router, userProcedure } from '../trpc';
|
||||
|
|
@ -11,4 +20,151 @@ export const notificationsRouter = router({
|
|||
},
|
||||
});
|
||||
}),
|
||||
list: userProcedure
|
||||
.input(
|
||||
z.object({
|
||||
pagination: z.object({
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.transform((val) => Math.min(30, val)),
|
||||
page: z.number().int().positive(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx: { viewer }, input: { pagination } }) => {
|
||||
const { limit, page } = pagination;
|
||||
const [totalCount, notifications] = await Promise.all([
|
||||
prisma.interviewsActivity.count({
|
||||
where: {
|
||||
recipientId: viewer.id,
|
||||
},
|
||||
}),
|
||||
prisma.interviewsActivity.findMany({
|
||||
include: {
|
||||
actor: {
|
||||
select: {
|
||||
avatarUrl: true,
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
comment: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vote: {
|
||||
include: {
|
||||
comment: {
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
where: {
|
||||
recipientId: viewer.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (notifications.length === 0) {
|
||||
return {
|
||||
notifications: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const finalNotifications = await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
if (
|
||||
notification.category === 'DISCUSSION_UPVOTE' &&
|
||||
notification.vote
|
||||
) {
|
||||
const [format, slug] = unhashQuestion(
|
||||
notification.vote.comment.entityId,
|
||||
);
|
||||
const { question: questionMetadata } = await fetchQuestion({
|
||||
format,
|
||||
slug,
|
||||
});
|
||||
|
||||
return {
|
||||
...notification,
|
||||
question: {
|
||||
format: questionMetadata.format,
|
||||
href: questionMetadata.href,
|
||||
slug: questionMetadata.slug,
|
||||
title: questionMetadata.title,
|
||||
},
|
||||
} as InterviewsUpvoteActivity;
|
||||
}
|
||||
if (notification.category === 'DISCUSSION' && notification.comment) {
|
||||
const [format, slug] = unhashQuestion(
|
||||
notification.comment.entityId,
|
||||
);
|
||||
const { question: questionMetadata } = await fetchQuestion({
|
||||
format,
|
||||
slug,
|
||||
});
|
||||
|
||||
return {
|
||||
...notification,
|
||||
question: {
|
||||
format: questionMetadata.format,
|
||||
href: questionMetadata.href,
|
||||
slug: questionMetadata.slug,
|
||||
title: questionMetadata.title,
|
||||
},
|
||||
} as InterviewsCommentActivity;
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
notifications: finalNotifications.flatMap((notification) =>
|
||||
notification != null ? [notification] : [],
|
||||
),
|
||||
totalCount,
|
||||
};
|
||||
}),
|
||||
markAsRead: userProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input: { id } }) => {
|
||||
await prisma.interviewsActivity.update({
|
||||
data: {
|
||||
read: true,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue