mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 18:04:55 +10:00
feat: add inbox counter (#1849)
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
import { type HTMLAttributes, useEffect, useState } from 'react';
|
import { type HTMLAttributes, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { ReadStatus } from '@prisma/client';
|
||||||
import { InboxIcon, MenuIcon, SearchIcon } from 'lucide-react';
|
import { InboxIcon, MenuIcon, SearchIcon } from 'lucide-react';
|
||||||
import { Link, useParams } from 'react-router';
|
import { Link, useParams } from 'react-router';
|
||||||
|
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||||
import { getRootHref } from '@documenso/lib/utils/params';
|
import { getRootHref } from '@documenso/lib/utils/params';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
@@ -28,6 +30,15 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
|||||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||||
const [scrollY, setScrollY] = useState(0);
|
const [scrollY, setScrollY] = useState(0);
|
||||||
|
|
||||||
|
const { data: unreadCountData } = trpc.document.inbox.getCount.useQuery(
|
||||||
|
{
|
||||||
|
readStatus: ReadStatus.NOT_OPENED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// refetchInterval: 30000, // Refetch every 30 seconds
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onScroll = () => {
|
const onScroll = () => {
|
||||||
setScrollY(window.scrollY);
|
setScrollY(window.scrollY);
|
||||||
@@ -61,12 +72,11 @@ export const Header = ({ className, ...props }: HeaderProps) => {
|
|||||||
<Link to="/inbox" className="relative block h-10 w-10">
|
<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" />
|
<InboxIcon className="text-muted-foreground hover:text-foreground h-5 w-5 flex-shrink-0 transition-colors" />
|
||||||
|
|
||||||
{/* Todo: Add counter. */}
|
{unreadCountData && unreadCountData.count > 0 && (
|
||||||
{/* {unreadCount > 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">
|
||||||
<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]">
|
{unreadCountData.count > 99 ? '99+' : unreadCountData.count}
|
||||||
1
|
|
||||||
</span>
|
</span>
|
||||||
)} */}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { ReadStatus } from '@prisma/client';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
import LogoImage from '@documenso/assets/logo.png';
|
import LogoImage from '@documenso/assets/logo.png';
|
||||||
import { authClient } from '@documenso/auth/client';
|
import { authClient } from '@documenso/auth/client';
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
||||||
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
|
||||||
|
|
||||||
@@ -25,6 +27,15 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
|
|||||||
|
|
||||||
const currentTeam = useOptionalCurrentTeam();
|
const currentTeam = useOptionalCurrentTeam();
|
||||||
|
|
||||||
|
const { data: unreadCountData } = trpc.document.inbox.getCount.useQuery(
|
||||||
|
{
|
||||||
|
readStatus: ReadStatus.NOT_OPENED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// refetchInterval: 30000, // Refetch every 30 seconds
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const handleMenuItemClick = () => {
|
const handleMenuItemClick = () => {
|
||||||
onMenuOpenChange?.(false);
|
onMenuOpenChange?.(false);
|
||||||
};
|
};
|
||||||
@@ -52,11 +63,11 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
href: `/t/${teamUrl}/documents`,
|
href: `/t/${teamUrl}/documents`,
|
||||||
label: t`Documents`,
|
text: t`Documents`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: `/t/${teamUrl}/templates`,
|
href: `/t/${teamUrl}/templates`,
|
||||||
label: t`Templates`,
|
text: t`Templates`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/inbox',
|
href: '/inbox',
|
||||||
@@ -86,11 +97,16 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
|
|||||||
{menuNavigationLinks.map(({ href, text }) => (
|
{menuNavigationLinks.map(({ href, text }) => (
|
||||||
<Link
|
<Link
|
||||||
key={href}
|
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}
|
to={href}
|
||||||
onClick={() => handleMenuItemClick()}
|
onClick={() => handleMenuItemClick()}
|
||||||
>
|
>
|
||||||
{text}
|
{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>
|
</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 { authenticatedProcedure, procedure, router } from '../trpc';
|
||||||
import { findInboxRoute } from './find-inbox';
|
import { findInboxRoute } from './find-inbox';
|
||||||
|
import { getInboxCountRoute } from './get-inbox-count';
|
||||||
import {
|
import {
|
||||||
ZCreateDocumentRequestSchema,
|
ZCreateDocumentRequestSchema,
|
||||||
ZCreateDocumentV2RequestSchema,
|
ZCreateDocumentV2RequestSchema,
|
||||||
@@ -59,6 +60,7 @@ import { updateDocumentRoute } from './update-document';
|
|||||||
export const documentRouter = router({
|
export const documentRouter = router({
|
||||||
inbox: {
|
inbox: {
|
||||||
find: findInboxRoute,
|
find: findInboxRoute,
|
||||||
|
getCount: getInboxCountRoute,
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user