feat: add inbox counter (#1849)

This commit is contained in:
Ephraim Duncan
2025-06-18 03:30:01 +00:00
committed by GitHub
parent 039cd7d449
commit 29a03d4ec7
5 changed files with 86 additions and 8 deletions
@@ -1,11 +1,13 @@
import { type HTMLAttributes, useEffect, useState } from 'react';
import { ReadStatus } from '@prisma/client';
import { InboxIcon, MenuIcon, SearchIcon } from 'lucide-react';
import { Link, useParams } from 'react-router';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
import { getRootHref } from '@documenso/lib/utils/params';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -28,6 +30,15 @@ export const Header = ({ className, ...props }: HeaderProps) => {
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
const [scrollY, setScrollY] = useState(0);
const { data: unreadCountData } = trpc.document.inbox.getCount.useQuery(
{
readStatus: ReadStatus.NOT_OPENED,
},
{
// refetchInterval: 30000, // Refetch every 30 seconds
},
);
useEffect(() => {
const onScroll = () => {
setScrollY(window.scrollY);
@@ -61,12 +72,11 @@ export const Header = ({ className, ...props }: HeaderProps) => {
<Link to="/inbox" className="relative block h-10 w-10">
<InboxIcon className="text-muted-foreground hover:text-foreground h-5 w-5 flex-shrink-0 transition-colors" />
{/* Todo: Add counter. */}
{/* {unreadCount > 0 && (
<span className="bg-muted text-muted-foreground absolute -right-2 -top-2 flex h-5 w-5 items-center justify-center rounded-full text-[10px]">
1
{unreadCountData && unreadCountData.count > 0 && (
<span className="bg-primary text-primary-foreground absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold">
{unreadCountData.count > 99 ? '99+' : unreadCountData.count}
</span>
)} */}
)}
</Link>
</Button>
@@ -2,12 +2,14 @@ import { useMemo } from 'react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { ReadStatus } from '@prisma/client';
import { Link } from 'react-router';
import LogoImage from '@documenso/assets/logo.png';
import { authClient } from '@documenso/auth/client';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
import { trpc } from '@documenso/trpc/react';
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
@@ -25,6 +27,15 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
const currentTeam = useOptionalCurrentTeam();
const { data: unreadCountData } = trpc.document.inbox.getCount.useQuery(
{
readStatus: ReadStatus.NOT_OPENED,
},
{
// refetchInterval: 30000, // Refetch every 30 seconds
},
);
const handleMenuItemClick = () => {
onMenuOpenChange?.(false);
};
@@ -52,11 +63,11 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
return [
{
href: `/t/${teamUrl}/documents`,
label: t`Documents`,
text: t`Documents`,
},
{
href: `/t/${teamUrl}/templates`,
label: t`Templates`,
text: t`Templates`,
},
{
href: '/inbox',
@@ -86,11 +97,16 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
{menuNavigationLinks.map(({ href, text }) => (
<Link
key={href}
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
className="text-foreground hover:text-foreground/80 flex items-center gap-2 text-2xl font-semibold"
to={href}
onClick={() => handleMenuItemClick()}
>
{text}
{href === '/inbox' && unreadCountData && unreadCountData.count > 0 && (
<span className="bg-primary text-primary-foreground flex h-6 min-w-[1.5rem] items-center justify-center rounded-full px-1.5 text-xs font-semibold">
{unreadCountData.count > 99 ? '99+' : unreadCountData.count}
</span>
)}
</Link>
))}
@@ -0,0 +1,35 @@
import { DocumentStatus, RecipientRole } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import { ZGetInboxCountRequestSchema, ZGetInboxCountResponseSchema } from './get-inbox-count.types';
export const getInboxCountRoute = authenticatedProcedure
.input(ZGetInboxCountRequestSchema)
.output(ZGetInboxCountResponseSchema)
.query(async ({ input, ctx }) => {
const { readStatus } = input ?? {};
const userEmail = ctx.user.email;
const count = await prisma.recipient.count({
where: {
email: userEmail,
readStatus,
role: {
not: RecipientRole.CC,
},
document: {
status: {
not: DocumentStatus.DRAFT,
},
deletedAt: null,
},
},
});
return {
count,
};
});
@@ -0,0 +1,15 @@
// import type { OpenApiMeta } from 'trpc-to-openapi';
import { ReadStatus } from '@prisma/client';
import { z } from 'zod';
export const ZGetInboxCountRequestSchema = z
.object({
readStatus: z.nativeEnum(ReadStatus).optional(),
})
.optional();
export const ZGetInboxCountResponseSchema = z.object({
count: z.number(),
});
export type TGetInboxCountResponse = z.infer<typeof ZGetInboxCountResponseSchema>;
@@ -28,6 +28,7 @@ import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { authenticatedProcedure, procedure, router } from '../trpc';
import { findInboxRoute } from './find-inbox';
import { getInboxCountRoute } from './get-inbox-count';
import {
ZCreateDocumentRequestSchema,
ZCreateDocumentV2RequestSchema,
@@ -59,6 +60,7 @@ import { updateDocumentRoute } from './update-document';
export const documentRouter = router({
inbox: {
find: findInboxRoute,
getCount: getInboxCountRoute,
},
/**